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

 更新时间:2026年09月17日 08:37:12   作者:Bruce_xiaowei  
本文深入剖析了Python函数返回None的三种写法(return None、裸return、隐式返回)在运行时、字节码、行号、覆盖率、mypy类型检查上的真实差异,并揭示if value:假值判断的陷阱,帮你写出更严谨的代码,需要的朋友可以参考下

全部结论均在 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写法的资料请关注脚本之家其它相关文章!

相关文章

  • python not运算符的实例用法

    python not运算符的实例用法

    在本篇文章里小编给大家整理了一篇关于python not运算符的实例用法,有需要的朋友们可以学习参考下。
    2021-06-06
  • python3 读取Excel表格中的数据

    python3 读取Excel表格中的数据

    这篇文章主要介绍了python3 读取Excel表格中的数据的相关资料,需要的朋友可以参考下
    2018-10-10
  • mac安装python3后使用pip和pip3的区别说明

    mac安装python3后使用pip和pip3的区别说明

    这篇文章主要介绍了mac安装python3后使用pip和pip3的区别说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-09-09
  • Python 文件操作实现代码

    Python 文件操作实现代码

    文件操作是程序设计中不可或缺的重要部分。Python通过一个内置函数open来打开文件。
    2009-10-10
  • Jupyter Notebook切换虚拟环境的三种方法

    Jupyter Notebook切换虚拟环境的三种方法

    本文主要介绍了Jupyter Notebook切换虚拟环境的三种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-07-07
  • python 多线程与多进程效率测试

    python 多线程与多进程效率测试

    这篇文章主要介绍了python 多线程与多进程效率测试,在Python中,计算密集型任务适用于多进程,IO密集型任务适用于多线程、接下来看看文章得实例吧,需要的朋友可以参考一下哟
    2021-10-10
  • Python实现读取文本文件并转换为pdf

    Python实现读取文本文件并转换为pdf

    这篇文章主要为大家详细介绍了如何使用Python简便快捷地完成TXT文件到PDF文档的转换,满足多样化的文档处理需求,感兴趣的小伙伴可以参考下
    2024-04-04
  • 安装PyTorch的详细过程记录

    安装PyTorch的详细过程记录

    PyTorch是一个基于Python的科学计算框架,用于进行深度学习相关研究,下面这篇文章主要给大家介绍了关于安装PyTorch的详细过程,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2022-03-03
  • Python上下文管理器进阶之巧用contextlib简化with语法

    Python上下文管理器进阶之巧用contextlib简化with语法

    本文介绍了Python中两种实现上下文管理器的方法,重点讲解了如何使用contextlib模块的contextmanager装饰器简化with语法,本文对比了两种方案的优缺点,大家可以根据需要进行选择
    2026-07-07
  • 详解Python+Pyecharts实现漏斗图的绘制

    详解Python+Pyecharts实现漏斗图的绘制

    漏斗图是一个简单的散点图,反映研究在一定样本量或精确性下单个研究的干预效应估计值。本文将用Python Pyecharts实现漏斗图的绘制,需要的可以参考一下
    2022-06-06

最新评论