page contents

Python 3.15 上线 sentinel():一行分清没传和 None

写一个函数,参数有个默认值是 None。结果用户传进来 None 的时候,函数就懵了——这是用户"没传",还是"故意传了一个 None"?

attachments-2026-06-WI223MBT6a41cea926197.png你有没有遇到过这种情况?

写一个函数,参数有个默认值是 None。结果用户传进来 None 的时候,函数就懵了——这是用户"没传",还是"故意传了一个 None"?

为了区分这两种情况,你不得不写一个看起来特别玄学的 MISSING = object(),然后再写一堆判断。

说实话,这个坑我刚学 Python 的时候也踩过。当时搜遍全网也没找到一个既简单又优雅的解法。直到看到 Python 3.15 新增的 sentinel() 内置函数,我才反应过来:原来这个问题,官方一直都知道,只是解法来得晚了十几年。

一、那个经典坑:怎么区分"没传"和"传了 None"?

咱们先把这个痛点说清楚。假设你写了一个配置查询函数:

def get_config(key, default=None):
  if default is None:
    return f"查不到 {key},请检查配置"
  return f"{key} 的值是 {default}"

看着挺合理对吧?但是用户这么调就出问题了:

get_config("timeout")     # 期望:报"没传配置"
get_config("timeout", None) # 期望:用户故意传了 None
get_config("timeout", 30)  # 期望:用 30 当默认值

但实际上,get_config("timeout") 和 get_config("timeout", None) 的行为是一模一样的——因为函数定义时 default=None,你没传参数时它也是 None。

这就是经典的"哨兵值"问题。Python 社区喊了十几年要个官方解法,最近终于来了。

二、Python 3.15 之前,大家都在用 object() 这个怪招

在 3.15 之前,老程序员们用的是一个看起来很黑魔法的写法:

MISSING = object()

def get_config(key, default=MISSING):
  if default is MISSING:
    return f"查不到 {key},请检查配置"
  return f"{key} 的值是 {default}"

原理很简单:object() 每次调用都生成一个全新对象,这个对象跟你传入的任何值都不可能相等(除了它自己)。

这样写能解决问题,但有三个让人难受的副作用:

第一,调试时看不懂。如果你打印 MISSING 变量,看到的是 <object object at 0x7f1ed6980210>。这是啥?看不出来。

第二,pickle 之后会失效。用 pickle 序列化再反序列化后,新对象和原对象就不再是同一个了,is 比较会返回 False。一旦你的代码涉及多进程通信或者缓存,迟早踩这个坑。

第三,没法做类型标注。object() 的类型就是 object,类型检查器分不清"这是个哨兵"还是"这就是个普通对象"。

正是因为这三个问题,社区里关于"搞个官方哨兵值"的讨论一直没断过。从 2021 年的 PEP 661 提案,到 2026 年终于在 3.15 里落地,足足等了 5 年。

三、Python 3.15 的 sentinel(),到底新在哪?

3.15 新增的 sentinel() 内置函数,一次性解决了上面三个问题。用法极其简单:

# Python 3.15+
MISSING = sentinel("MISSING")
print(MISSING) # 输出:MISSING

就这一行。它返回的哨兵值自带名字,类型也是专用的 sentinel 类型。

具体来说,对比一下:

维度 object()sentinel()

repr 显示 <object at 0x...> 你起的名字(比如 MISSING)

pickle 之后 identity 断裂 保持

类型标注 object(无区分度) sentinel(精确)

最低版本 任意版本 Python 3.15

看到没?同样是"造一个独一无二的占位符",新写法在每一个维度上都更好用。

四、三个新手最该会的实战场景

光看 API 没用,咱们得知道这东西到底能用在哪些地方。我挑了 3 个新手最容易遇到的场景:

场景 1:配置查询函数

这就是开头那个例子。用 sentinel() 改写一下:

MISSING = sentinel("MISSING")

def get_config(key, default=MISSING):
  if default is MISSING:
    return f"查不到 {key},请检查配置"
  return f"{key} 的值是 {default}"

差别看着不大对吧?但当你调试时打印 default 变量,看到的是清清楚楚的 MISSING,而不是 <object at 0x...>。这就是调试体验的差别。

场景 2:字典的 get/setdefault 行为区分

新手写缓存的时候,经常会这么写:

value = cache.get(key, None)
if value is None:
  value = compute_expensive(key)
  cache[key] = value

问题来了:如果缓存里某个 key 的值本身就是合法的 None,上面这段代码就会触发一次额外的 compute_expensive。用 sentinel 就能避免:

MISSING = sentinel("MISSING")
value = cache.get(key, MISSING)
if value is MISSING:
  value = compute_expensive(key)
  cache[key] = value

这下能精确区分"键不存在"和"键的值是 None"了。

场景 3:多进程通信

如果你用过 multiprocessing 或者 PySpark 这种跨进程通信的场景,就知道 pickle 是个绕不过去的坎。前面说过 object() 在 pickle 后会失效,sentinel() 就不存在这个问题——它反序列化后 identity 保持不变。这一点对新手来说可能感知不强,但你哪天真碰到多进程 bug 的时候,会回来感谢这个特性。

五、我现在能不能用?升级建议给你整理好了

最后说说实操建议。

如果你还在用 3.14 及以下:object() 凑合着用也没事,别为了一个新特性贸然升级。生产环境优先稳定。

如果你已经在用 3.15 beta:可以放心用。Beta 阶段这个特性已经稳定,社区没有发现需要改动的设计问题。

如果你想尝鲜:3.15 正式版预计今年 10 月发布。在那之前,CI 跑得动就用 beta,跑不动就等正式版。

说句题外话,Python 这几年加新特性越来越克制了。3.15 的几个新东西——sentinel()、frozendict、lazy_import——都是社区喊了很久、讨论了很长时间才加进来的。这种"小而美"的改进,对新手其实最友好:学一个就解决一个具体的痛点,不用理解一整套新概念。

 更多相关技术内容咨询欢迎前往并持续关注好学星城论坛了解详情。

想高效系统的学习Python编程语言,推荐大家关注一个微信公众号:Python编程学习圈。每天分享行业资讯、技术干货供大家阅读,关注即可免费领取整套Python入门到进阶的学习资料以及教程,感兴趣的小伙伴赶紧行动起来吧。

attachments-2022-05-rLS4AIF8628ee5f3b7e12.jpg

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

2179 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 2228 文章
  3. Pack 2179 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章