当前位置: 代码网 > it编程>前端脚本>Python > Python中三种返回None的写法与避坑指南

Python中三种返回None的写法与避坑指南

2026年09月17日 Python 我要评论
全部结论均在 cpython 3.11.9 / macos (apple silicon) 上真实运行验证。字节码、行号、内存、错误码均为解释器与工具的原样输出。类型检查使用 mypy 2.3.1,覆

全部结论均在 cpython 3.11.9 / macos (apple silicon) 上真实运行验证。字节码、行号、内存、错误码均为解释器与工具的原样输出。
类型检查使用 mypy 2.3.1,覆盖率使用 coverage 7.16.1,两者均已在本机实际执行。

0. 起点:三个看起来一样的函数

def foo1(value):
    if value:
        return value
    else:
        return none

def foo2(value):
    """bare return statement implies 'return none'"""
    if value:
        return value
    else:
        return

def foo3(value):
    """missing return statement implies 'return none'"""
    if value:
        return value

type(foo1(0))    # <class 'nonetype'>
type(foo2(0))    # <class 'nonetype'>
type(foo3(0))    # <class 'nonetype'>

三行输出完全一样。单看结果,会得出"三种写法等价"的结论——这个结论对,但只在运行时成立

真正的信息量在于:它们连编译产物都一模一样,唯一的差异出现在"行号归属"上,而这个差异又在类型检查器那里被放大成三种完全不同的错误。

1. 运行时:三者返回的是同一个对象

type() 相同还不够硬,因为 ==isnone 上是有区别的。实测:

foo1(0) is none    # true
foo2(0) is none    # true
foo3(0) is none    # true

id(foo1(0))        # 4338437944
id(foo2(0))        # 4338437944
id(foo3(0))        # 4338437944
id(none)           # 4338437944

foo1(0) is foo2(0) is foo3(0)   # true

三个 idid(none) 完全相同——它们返回的不是"三个值为 none 的对象",而是同一个单例对象。所以判断应始终用 is none,永远不要用 == none(后者可能被自定义 __eq__ 劫持)。

跨参数验证(5 个变体 × 3 种参数组合,全部一致):

foo1    :foo(x=0)->none  foo(x=1)->1  foo(x=none)->none
foo1b   :foo(x=0)->none  foo(x=1)->1  foo(x=none)->none
foo2    :foo(x=0)->none  foo(x=1)->1  foo(x=none)->none
foo2b   :foo(x=0)->none  foo(x=1)->1  foo(x=none)->none
foo3b   :foo(x=0)->none  foo(x=1)->1  foo(x=none)->none

none的三个硬事实

type(none)                    # <class 'nonetype'>
types.nonetype                # <class 'nonetype'>   (3.10 起 types 里正式暴露此名字)
type(none) is types.nonetype  # true

exec("none = 1")              # syntaxerror: cannot assign to none
exec("def f():\n    none = 1")# syntaxerror: cannot assign to none
事实实测
none 是单例,全局唯一三个函数的返回值 id 全部等于 id(none)
类型名 nonetype 已可直接引用types.nonetype 可用(3.10+),此前只能用 type(none)
none 不可被赋值/遮蔽模块级与函数内都抛 syntaxerror: cannot assign to none(3.x 里 none 是关键字,不是普通常量)

第三条值得强调:nonetruefalse 从 python 3 起都是关键字,不能被重新绑定。所以上面三个函数的 return none 中,none 一定指向那个唯一的单例,不存在被覆盖的可能。

2. 字节码:三种写法编译产物逐字节一致

这是本篇最反直觉的一节。先看三个函数的反汇编(均为实测原文,为排除行号干扰,此处使用三份不带 docstring 的等价版本 n1 / n2 / n3):

----------------------------------------------------
n1(显式 return none,co_firstlineno=6)
   偏移 0   行号 6     resume
   偏移 2   行号 7     load_fast value
   偏移 4   行号 none  pop_jump_forward_if_false to 10
   偏移 6   行号 8     load_fast value
   偏移 8   行号 none  return_value
   偏移 10  行号 10    load_const none
   偏移 12  行号 none  return_value
----------------------------------------------------
n2(裸 return,co_firstlineno=12)
   偏移 0   行号 12    resume
   偏移 2   行号 13    load_fast value
   偏移 4   行号 none  pop_jump_forward_if_false to 10
   偏移 6   行号 14    load_fast value
   偏移 8   行号 none  return_value
   偏移 10  行号 16    load_const none      <-- 行号不同,指令相同
   偏移 12  行号 none  return_value
----------------------------------------------------
n3(隐式 return,co_firstlineno=18)
   偏移 0   行号 18    resume
   偏移 2   行号 19    load_fast value
   偏移 4   行号 none  pop_jump_forward_if_false to 10
   偏移 6   行号 20    load_fast value
   偏移 8   行号 none  return_value
   偏移 10  行号 19    load_const none      <-- 行号回落到了 if 那一行
   偏移 12  行号 none  return_value

三种写法的指令序列完全相同

resume
load_fast      value
pop_jump_forward_if_false  (to 10)
load_fast      value
return_value
load_const     none
return_value

return none、裸 return、函数末尾什么都不写——cpython 把它们编译成了同一段代码。编译器对"隐式返回"的处理就是自动补上 load_const none; return_value

逐字节比较(co_code)

n1.__code__.co_code == n2.__code__.co_code   # true
n1.__code__.co_code == n3.__code__.co_code   # true

而在带 docstring 的一组里同样全为 true

d1.__code__.co_code == d2.__code__.co_code   # true
d1.__code__.co_code == d3.__code__.co_code   # true

但跨组时不等:

n1.__code__.co_code == d1.__code__.co_code   # false

这不是语义差异,而是常量表下标差异。 原因在于 docstring 会占据 co_consts[0]

n1.__code__.co_consts    # (none,)
d1.__code__.co_consts    # ('doc', none)

于是反汇编里出现 load_const 0(无 docstring)与 load_const 1(有 docstring)的区别——操作数的那个字节变了,机器码自然不同,但两者加载的都是同一个 none

结论:"三种写法编译结果一致"是严格成立的,只要把 docstring 这个无关变量固定住,co_code 完全相同。

3. 唯一的可观测差异:隐式 return 的行号归属

既然指令相同,差异只能来自附加在指令上的行号表(line table)。看三者最后那条 load_const none 的行号:

函数返回 none 的写法load_const none 归属行号含义
n1return none10return none 所在行
n2return16return 所在行
n3什么都不写19if value: 所在行(函数体最后一行)

n3 的返回动作没有独立行号,它被挂到了函数体最后一行的行号上。

sys.settrace 记录实际执行到的行号,差异立刻显形:

n1(0) 实际执行行号序列: [3, 6]     最后一行 = 6    (if 行 + return none 行)
n2(0) 实际执行行号序列: [10, 13]   最后一行 = 13   (if 行 + return 行)
n3(0) 实际执行行号序列: [17]       最后一行 = 17   (只有 if 行!)

0(走"返回 none"路径)时:

函数执行到的行数说明
n12 行有独立的 return none 步骤
n22 行有独立的 return 步骤
n31 行返回动作不可见

传真值 1 时三者都是 2 行(n1[3, 4]n2[10, 11]n3[17, 18]),差异只在"返回 none"这条路径上。

这一条差异的实际后果

  • 调试器:单步执行 n3(0) 时,调试器不会在"返回"处停留一步,你看到的是"执行完 if 就跳出了函数"。审查代码时看不到返回动作。
  • trace / profilern3 的返回路径在调用轨迹上只留下一行。
  • 覆盖率工具:见下一节。

4. 覆盖率视角

对三个函数各调用一次 f(0)(只走假值路径),用 coverage 7.16.1 实测:

===== 语句覆盖率 =====
name              stmts   miss  cover   missing
-----------------------------------------------
cov_ret_none.py      15      3    80%   6, 13, 20
total                15      3    80%

===== 分支覆盖率 =====
name              stmts   miss  branch brpart  cover   missing
-------------------------------------------------------------
cov_ret_none.py      15      3      8      4    70%   6, 13, 20, 23->exit
total                15      3      8      4    70%

三个 miss 行 6, 13, 20 分别是三个函数里的 return value(真值分支),三者一致。

值得注意的是:n3 的隐式 return 从来不会出现在 missing 列表里——因为它没有独立行号,覆盖率工具根本没有"这一行"可以统计。换句话说:

一个函数是否真的走到了"返回"这一步,在 n3 这种写法下,覆盖率工具无法观测

分支覆盖率还额外暴露了一处:23->exitif __name__ == "__main__": 条件为假的分支未走),brpart 4 = 三处 if 的未覆盖分支 + 该处。

5. 最大的差异:mypy 给出三种不同错误码

同一个运行时行为,加上返回类型标注后,静态检查器的判定截然不同。以下为 mypy 2.3.1 的实测输出(分号后为错误码):

ret_none_types.py:9: error: incompatible return value type (got "none", expected "int")  [return-value]
ret_none_types.py:16: error: return value expected  [return-value]
ret_none_types.py:19: error: missing return statement  [return]
found 5 errors in 1 file (checked 1 source file)

对应关系:

写法报错行报错内容错误码
foo1return none第 9 行(return none 那行)incompatible return value type (got "none", expected "int")return-value
foo2:裸 return第 16 行(return 那行)return value expectedreturn-value
foo3:隐式返回第 19 行(def 那一行missing return statementreturn

三种写法,三种描述,甚至两种错误码。而且注意报错位置:

  • foo1 / foo2 报在具体语句上(值不对 / 缺值);
  • foo3 报在函数定义行上(整个函数缺少返回语句)。

一个更尖锐的对照:标注成-> int | none也救不了后两种

把返回类型放宽到允许 none,再测(同一文件 30–41 行):

ret_none_types.py:36: error: return value expected  [return-value]
ret_none_types.py:39: error: missing return statement  [return]
写法标注 -> int标注 -> int | none
return noneincompatible return value type✅ 通过
returnreturn value expected仍然报错
隐式返回missing return statement仍然报错

这是一个非常实用的结论:在 mypy 的规则下,裸 return 与隐式返回即使返回类型合法地包含 none,依然会被判为问题。前者要求显式写出 return none,后者要求函数末尾有显式 return

不加标注时都不报

完全不加类型标注的三个版本(bar1/bar2/bar3),默认模式与 --check-untyped-defs 模式下均无任何报错,且两种模式输出完全相同。也就是说,这套差异只在"声明了返回类型"的前提下才会被检查器捕捉到——没有标注时,静态检查完全帮不上忙。

精确抑制

三种报错都能被封到单个错误码上,实测(每处加上对应的 # type: ignore[...]):

success: no issues found in 1 source file

对照去掉 ignore 的版本:

mypy_no_ignore_demo.py:8: error: incompatible return value type (got "none", expected "int")  [return-value]
mypy_no_ignore_demo.py:15: error: return value expected  [return-value]
mypy_no_ignore_demo.py:18: error: missing return statement  [return]
写法抑制注释放置位置
return none# type: ignore[return-value]return 所在行
return# type: ignore[return-value]return 所在行
隐式返回# type: ignore[return]def 所在行

注意最后一行的位置特殊性:由于隐式返回的报错挂在 def 行,# type: ignore[return] 也必须写在 def 行,写在别处无效。

6. ast:mypy 判定差异的结构依据

把三个函数解析成 ast,差异一目了然:

n1 函数体顶层节点: ['if']
  if 的 orelse 分支: ['return']
  if 体 中的 return.value: name(id='value', ctx=load())
  else 体 中的 return.value: constant(none)     <-- 显式 none 常量
--------------------------------------------------------
n2 函数体顶层节点: ['if']
  if 的 orelse 分支: ['return']
  if 体 中的 return.value: name(id='value', ctx=load())
  else 体 中的 return.value: none               <-- 缺值返回
--------------------------------------------------------
n3 函数体顶层节点: ['if']
  if 的 orelse 分支: (无 orelse)               <-- 缺少分支
  if 体 中的 return.value: name(id='value', ctx=load())

对应到前三节的现象,整条因果链就通了:

写法ast 特征编译产物检查器判定
return nonereturn.valueconstant(none)load_const none值类型不符(return-value
returnreturn.valuenone(空)同上缺返回值(return-value
隐式返回orelse,函数体末尾无 return同上缺返回语句(return

三种写法在 ast 层就分了岔,编译器把它们归一化成同样的字节码,而类型检查器保留了这个分歧。 这就是"运行等价、静态不等价"的全部原因。

7. 这三个函数真正做的事:假值归一化

抛开 none 的话题,这三个函数实际做的是:真值原样返回,假值一律转成 none。实测 23 个样本,与 value or none 逐项比对:

foo1 / foo2 / foo3 返回值 is 同一对象: true
三者与 (v or none) 是否一致: true

也就是说,三个函数等价于:

lambda value: value or none

假值判定全集(实测全部返回 none):

0, 0.0, 0j, '', [], {}, set(), (), none, false, b''

真值(原样返回)——这里有三个容易判断错的:

foo1(nan      ) -> nan          <-- float('nan') 是真值!
foo1('0'      ) -> '0'          <-- 非空字符串,真值
foo1('false'  ) -> 'false'      <-- 非空字符串,真值
foo1([0]      ) -> [0]          <-- 非空列表,真值
foo1({'k': 0} ) -> {'k': 0}     <-- 非空字典,真值

float('nan') 是真值这一条尤其容易踩:if value:nan 判定为真,nan 会被原样返回,而不是被归一化成 none。同理,'0''false' 作为非空字符串也是真值——从 json 或表单里读到的字符串 "0""false" 都会绕过归一化。

8. 这个设计埋下的真实 bug 模式

def get_count(n):
    """本意:返回计数,没有数据时返回 none。"""
    if n:
        return n
    # 忘了写 else 分支 —— 计数为 0 时静默返回 none

实测:

真实计数 0  -> get_count 返回 none  -> 误判为『无数据』
真实计数 5  -> get_count 返回 5     -> 正常拿到 5

0 是一个合法计数,却被当成"没有数据"。 这是隐式返回 none 最典型的误伤,而且它极难被发现——因为函数"看起来是对的",返回 none 也"符合文档描述",只有调用方的 if x is none 分支会在生产环境里悄悄走错。

三种写法在可维护性上的成本对比:

写法源码行数读代码时的信号风险
return none5 行“这里明确要返回 none”,意图清晰
return5 行return 这个动作,"故意返回"可辨认
隐式返回3 行与"忘写 return"在视觉上完全一致

第三种写法的核心问题不是"能不能这么写",而是它把"故意返回 none"和"忘记写 return"写成了同一个样子。代码审查时,你无法从源码上区分 foo3 是刻意设计还是漏写分支——三个函数里恰好都写了 docstring 来说明意图,这本身就是在为这个歧义打补丁。

9. 生成器中的return:换了个代价

同样的三种写法放进生成器,运行结果仍然一致,但 return 在生成器里有了新的含义(return value 会变成 stopiteration.value):

def g_bare():
    yield 1
    return

def g_none():
    yield 1
    return none

def g_value():
    yield 1
    return 42

实测:

g_bare     首个产出=1  stopiteration.value=none
g_none     首个产出=1  stopiteration.value=none
g_value    首个产出=1  stopiteration.value=42

g_bareg_nonestopiteration.value 都是 none,仍然等价;而 g_value42 藏进了 stopiteration.value——这个值只有用 next() 手动捕获异常或 yield from 才能拿到,日常 for 循环会直接丢掉。在生成器里写 return none 与裸 return,效果一致;写 return x 则是另一种语义。

10. 该选哪种写法

场景推荐写法理由
有返回值,但可能返回 none显式 return nonemypy 在 -> t 下能精确报出值类型问题;意图无歧义
早退分支(guard clause)return单行、无副作用语义,配合函数末尾统一返回即可
函数作为过程使用(本来就不该有返回值)什么都不写这是唯一适合隐式返回的场景,且不要标注返回类型
需要区分"假值"与"无值"不要用这套写法改用 return value if value is not none else none 或直接返回 value 并保留 none 语义

最后一行是关键:这三个函数把 0''[]none 统统混成了 none。如果你的业务需要区分"计数为 0"和"没有数据",if value: 这个判断本身就是错的——应该写成 if value is not none:

三个函数里最稳的是 foo1(显式 return none);foo2 可以接受;foo3 建议只在"函数是过程、不返回任何东西"时使用,并且不要给它加返回类型标注(否则 mypy 会要求你补上显式 return)。

11. 陷阱清单

#陷阱实测证据规避
1== none 判空三者返回的都是 id(none) 同一对象始终用 is none
2以为三种写法有运行时差异co_code 逐字节相同无需纠结,选可读性最好的
3以为隐式 return “没有开销”编译产物完全一致,无差异真正的代价在可维护性,不在性能
4float('nan') 被当作假值实测 foo1(nan) -> nan,nan 是真值数值场景单独判 is none
5'0' / 'false' 字符串被当作假值实测原样返回字符串先解析再判断
6计数 0 被误判为"无数据"实测 get_count(0) -> none判断改为 is not none
7隐式返回让调试器少一步trace 行号 n3(0) -> [17],仅 1 行返回 none 时写显式 return none
8覆盖率无法观测隐式返回missing 列表里永不出现该行需要审计返回路径时写显式 return
9mypy 报错位置在 deffoo3missing return statementdefignore 注释也要写在 def
10以为 -> int | none 能消除告警裸 return / 隐式 return 仍报错补显式 return none
11无类型标注时检查器不介入bar1/bar2/bar3 零报错想被检查就必须标注返回类型
12生成器里 return x 丢值stopiteration.value=42for 循环取不到需要传出值就 yield

12. 一页速查

# 三种写法,运行时完全等价(返回同一个 none 单例)
return none      # 显式:mypy 报"值类型不符"(return-value),可精确抑制
return           # 裸返回:mypy 报"缺值"(return-value),即 -> t | none 也报
# 什么都不写      # 隐式:mypy 报"缺返回语句"(return),报在 def 行

# 编译产物
#   return none / return / 隐式  ->  co_code 完全相同,均补 load_const none + return_value
#   co_code 不同的唯一原因:docstring 占据 co_consts[0],导致 load_const 操作数 0→1

# 行号归属(唯一可观测差异)
#   return none  -> 归属 return 语句行
#   return       -> 归属 return 语句行
#   隐式         -> 归属函数体最后一行,trace 上不可见

# 判空
x is none            # 正确
x == none            # 错误

# 需要区分"假值"和"无值"时,别用 if x:
if x is not none:    # 正确:0 / '' / [] 会走进来
if x:                # 会把 0 / 0.0 / '' / [] / {} / () / none / false 一起过滤

# 抑制类型检查告警
return none   # type: ignore[return-value]
return        # type: ignore[return-value]
def f() -> t: # type: ignore[return]        <- 隐式返回的 ignore 写在 def 行

13. 小结

这段代码给出了一个非常干净的结论样本:

  1. 运行时不区分:三种写法返回同一个 none 单例(id 相同),连字节码都逐字节一致。选哪种,运行时都看不出来。
  2. 编译期归一化:cpython 对"没有返回值的路径"一律补 load_const none; return_value,隐式返回就是编译器替你写的那一句。
  3. 行号是唯一的裂缝:隐式返回没有独立行号,导致 trace/调试器/覆盖率都"看不见"这次返回——这也是三种写法在工具链上唯一的客观差异。
  4. 静态检查把它放大了:同一份运行时行为,mypy 给出三种描述、两种错误码,且报错位置分别落在语句行与 def 行。运行等价 ≠ 检查等价
  5. 真正该警惕的是业务语义if value: 会把 0''[]none 混为一谈,0 被当成"没有数据"是这个模式最典型的误伤;而 float('nan') 又是真值,会绕过归一化。

一句话:写代码时按"运行时等价"选择没问题,但把类型标注加上——检查器会立刻告诉你,这三种写法在读代码的人眼里,从来就不是一回事。

本文所有代码均在 cpython 3.11.9 (macos, apple silicon) 上实际执行;类型检查为 mypy 2.3.1,覆盖率为 coverage 7.16.1。字节码、行号、trace 序列均为原样输出。

以上就是python中三种返回none的写法与避坑指南的详细内容,更多关于python返回none写法的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com