Python 的预声明常量有点奇怪
Hacker News 摘要原标题:Python's pre-declared constants are kinda weird
Python 拥有 6 个预声明的常量:True、False、None、__debug__、Ellipsis(也可以写成 ...)以及 NotImplemented。尽管它们都被统称为常量,但由于某些原因,它们的行为表现各不相同。
True、False 和 None
True、False 和 None 是 Python 的关键字。它们不是普通的标识符,而是直接作为词法标记存在的。这在 Python 中非常少见,因为通常这类内容是在常规名称解析过程中处理的,而不是在词法分析器中处理。
这种设计产生了一个有趣的副作用:像 x.True 这样的表达式会直接触发 SyntaxError 语法错误。目前尚不清楚这种设计背后的具体原因。
debug
__debug__ 是一个布尔常量。在正常运行时它的值为 True,但在使用 -O 优化标志运行 Python 时,它的值为 False。它的用途类似于 assert 断言:如果你有一些在优化构建中过于耗时的检查代码,可以将其包装在 if __debug__ 块中。
__debug__ 的特殊之处在于,虽然它是一个普通的标识符(不像 True 那样是关键字),但它是语言中唯一一个无法被赋值的标识符。尝试执行 __debug__ = 67 甚至 x.__debug__ = 67 都会触发 SyntaxError。
不过,因为它不是关键字,所以它的行为与 True 等常量仍有区别。访问 x.__debug__ 会抛出 AttributeError 而不是语法错误,因为这在语法上是合法的,只是查找了一个不存在的属性。
此外,删除 __debug__ 也有专门的错误提示。执行 del __debug__ 会触发 SyntaxError。但如果尝试删除属性 del x.__debug__,则会抛出正常的 NameError 或 AttributeError,而不会触发语法错误。
关于 SyntaxError 的特殊情况
对 __debug__ 赋值是少数几个明明语法正确却触发 SyntaxError 的案例。例如,执行 assert (__debug__ := 67) 时,在调试模式下会报语法错误。但在使用 -O 运行程序时,由于断言语句根本不会被编译,所以不会抛出任何异常。
类似的案例还包括在函数体之外使用 yield 或 await,它们也会在断言未被禁用的情况下触发类似的报错。
Ellipsis 和 NotImplemented
Ellipsis 和 NotImplemented 虽然在参考文档的常量章节中被提及,但它们并不是真正的常量。它们只是普通的内置对象,因此可以被全局变量覆盖。例如,你可以成功执行 NotImplemented = 67。目前并不清楚为什么这两个对象没有像其他常量那样获得特殊保护。
修改常量的实验
尽管 True、False 和 None 是词法标记,但它们在 builtins 模块中也作为普通内置对象存在。你可以通过 getattr(builtins, 'True') 获取它们。
有趣的是,你甚至可以使用 setattr 来修改它们在 builtins 模块中的值。例如,执行 setattr(builtins, 'True', 67) 之后,通过 getattr 获取的值会变成 67。但是,这并不会改变直接使用关键字 True 时的值,此时 True 依然等于 1。
__debug__ 表现出类似的行为。虽然你可以修改 builtins.__debug__ 的值,但直接访问 __debug__ 得到的值不会受到影响。这说明 __debug__ 确实经过了特殊的硬编码处理。
相比之下,Ellipsis 和 NotImplemented 就显得很随意。如果你修改了 builtins.Ellipsis 的值,那么后续代码中所有的 Ellipsis 都会变成新值。不过有趣的是,这并不会改变 ... 符号的值。在某种意义上,... 是一个真正的常量,而 Ellipsis 单词本身却不是。