在Python解释器里敲一行 setattr(builtins, 'True', 67),再打印 True,你会看到它纹丝不动——还是那个 True。但对 NotImplemented 做同样的操作,它就真的变成了67。

这个反差最近被一篇技术博客翻了出来。作者对着Python文档里并列的六个“常量”——TrueFalseNone__debug__EllipsisNotImplemented——做了一圈REPL实验,发现它们赋值报错的类型不一样,能不能被篡改也不一样,文中反复写“我很好奇设计初衷是什么”。答案其实一直都在:分散在PEP 3100PEP 285和一条CPython官方issue里,只是没人把它们拼在一起讲。

六个常量,四种待遇

先把差异摆清楚。TrueFalseNone是Python 3里真正的关键字,连当属性访问都会直接报SyntaxError__debug__不是关键字,却是全语言唯一“不能被赋值”的普通标识符,连x.__debug__ = 67这种属性赋值都被特判拦截。EllipsisNotImplemented则完全是普通内置名字,可以被一个赋值语句就地覆盖。

结论:六个“常量”里,真正名副其实的只有四个。

六个“常量”,谁真的锁死了 真常量 True / False / None 关键字,赋值即SyntaxError __debug__ 普通标识符,唯一禁止赋值 setattr(builtins,...)也改不了取值 不受builtins篡改影响 伪常量 Ellipsis 普通内置名,可被赋值覆盖 NotImplemented 同上,3.14起误当布尔用会报TypeError setattr(builtins,...)可直接篡改

“纯词法token”这个说法要打个折扣

原文认定 TrueFalseNone 是“纯粹的词法token”,和其他标识符不在一个层级上处理。这个判断需要修正一下。

CPython的tokenize模块显示,这三个词和EllipsisNotImplemented__debug__一样,在词法分析阶段全部被记成普通的NAME token——和你随手写的变量名没有区别。真正把它们区分成“关键字”的动作,发生在词法分析之后的语法解析阶段,而不是原文说的词法层。唯一在词法层就被单独标记的,只有 ...,它对应专门的ELLIPSIS token,Ellipsis这个名字反而没有这个待遇。

这个细节很小,但解释了一个反直觉现象:...不能被覆盖,Ellipsis却能——因为保护它们的根本不是同一层机制。

关键字化不是意外,是Python 3的明文决定

TrueFalseNone能变成关键字,不是历史遗留的巧合。PEP 3100把“将True、False、None提升为关键字”列为Python 3的既定变更之一,和整个Python 2到3的不兼容升级绑在一起写进了设计文档。更早的PEP 285里也说明,当初给布尔值选大写拼法True/False,就是刻意去对齐已经存在的NoneEllipsisNotImplemented这套命名惯例。

也就是说,原文困惑的“为什么这三个词地位特殊”,答案不是玄学,是一条写在PEP里、公开可查的语言演进决策。EllipsisNotImplemented没有跟着一起“转正”,不是被遗忘,而是没人认为值得为两个使用频率低得多的哨兵对象,再去动一次不向后兼容的语法升级。

一致性听起来理所当然,但对语言设计者来说,它是要花兼容性成本去买的

官方其实早就发现了这个不对称

原文里有一段“tangent”专门提到:__debug__被赋值会报SyntaxError,Ellipsis却不会,这种不对称很奇怪。这个疑问对应的是CPython官方一条真实存在的issue——#88439,专门讨论“对Ellipsis赋值应该像对__debug__赋值一样被拦截”,并且已经通过对应的补丁做了部分修复。

  • 结论.原文列出的每一处“为什么”,几乎都能在PEP 3100、PEP 285或CPython issue追踪器里找到有据可查的答案,不是无解的语言怪癖。
  • 风险.开发者如果在库代码里意外shadow了NotImplementedEllipsis这类“伪常量”,解释器不会报任何错,运算符协议的隐藏bug会悄悄潜伏下来。

这类问题最容易踩坑的是写运算符重载的库作者——__eq____add__这些方法依赖NotImplemented作为“本类型不支持该操作”的哨兵值,一旦这个名字在某个模块里被意外覆盖,Python不会拦你,只会在运行时给出一个让人摸不着头脑的行为。真正该做的,不是继续对着REPL惊呼“怪不怪”,而是先去PEP和issue里查一眼——语言设计史往往比直觉更可信。