暗无天日

=============>DarkSun的个人博客

TIL:Python 3.15 的 sentinel() 内置函数

写 Python 时偶尔会遇到一个需求:函数的某个参数有个默认值,但你得区分"调用者没传这个参数"和"调用者传了 None"。典型的一个应用场景是做配置查询时,若没传 key 则抛异常,传的是 None 则返回默认值。

这种场景需要的是一个"哨兵值"(sentinel):一个独一无二的占位符,保证参数的默认值跟其他任何值都不相等。

老写法: object()

Python 里最常用的做法是用 object() 做哨兵:

MISSING = object()

def get_config(key, default=MISSING):
    if default is MISSING:
        return f"fetching {key}, no default"
    return f"fetching {key}, fallback to {default}"

print(get_config("timeout"))       # → fetching timeout, no default
print(get_config("timeout", None)) # → fetching timeout, fallback to None
print(get_config("timeout", 30))   # → fetching timeout, fallback to 30

这个模式能工作,但有几个问题。

三个问题

repr 不友好

MISSING 的 repr 长这样:

<object object at 0x7f1ed6980210>

调试时打印出来毫无意义,你根本看不出这是个哨兵值。

pickle 后 identity 断裂

哨兵模式靠的是 is 比较身份。pickle 序列化再反序列化后, object() 变成了一个新对象, is 比较就失效了:

import pickle

MISSING = object()
MISSING_loaded = pickle.loads(pickle.dumps(MISSING))

print(MISSING is MISSING_loaded)  # → False

如果你的代码涉及多进程通信或缓存序列化,这个坑迟早踩到。

没法做类型标注

object() 的类型就是 object ,没法区分"这是一个哨兵类型"。你写 Optional[object] 的时候,类型检查器分不清这个地方是要一个哨兵还是一个 object 对象。

Python 3.15 的 sentinel()

3.15 新增的 sentinel() 内置函数解决了上面所有问题:

# Python 3.15+
MISSING = sentinel("MISSING")
print(MISSING)  # → MISSING

它返回的哨兵值带了名字,类型也是专用的 sentinel 。具体来说:

  • repr 就是你自己起的名字,调试时一目了然
  • pickle 后再 unpickle 仍然保持同一 identity
  • 可直接用作类型标注

PEP 661 对这个提案有详细的讨论,解释了为什么之前的各种方案( object() 、私用 _MISSING 、自定义类)都不够好。

小结

维度 object() sentinel()
repr <object at 0x...> 你起的名字
pickle 后 identity 断裂 保持
类型标注 object ,无区分度 sentinel ,精确
最低版本 任意版本 Python 3.15

如果你还在用 3.14 及以下, object() 凑合着用;升到 3.15 后,把 MISSING = object() 换成 MISSING = sentinel("MISSING") 就行。

Python : 编程 : 函数式