Python 的预声明常量有点奇怪
摘要
深入探讨 Python 预声明常量的不一致行为,包括关键字状态、赋值限制和遮蔽(shadowing)怪癖。
<p><a href="https://lobste.rs/s/62kxco/python_s_pre_declared_constants_are_kinda">评论</a></p>
查看缓存全文
缓存时间: 2026/08/13 15:24
# python 的预声明常量有点奇怪
来源:https://sebsite.pw/w/20260801-pythonconstants.html
python 有 6 个预声明的“常量”:`True`、`False`、`None`、`__debug__`、`Ellipsis`(或等价的 `...`)和 `NotImplemented`。但出于某种原因,它们的行为都略有不同。
## `True`、`False` 和 `None`
`True`、`False` 和 `None` 是关键字。它们不是标识符,而是直接作为独立的词法标记。这真的很奇怪;python 中没有任何其他东西是这样。通常事物是在常规名称解析过程中解析的,而不是在词法分析器本身中。
这样做的一个有趣副作用是,像 `x.True` 这样的表达式会抛出 `SyntaxError`。我很好奇这个决定背后的理由是什么(如果有的话)。
这些常量还有一些更有趣的地方,但我稍后再谈,因为它与其他常量有关。
## `__debug__`
`__debug__` 是一个布尔常量:它通常为 `True`,但在使用 `-O` 运行时为 `False`。这个思路类似于 `assert` 在非调试构建中被禁用:如果检查在“优化”构建中代价太高,你可以将代码包裹在 `if __debug__` 中,之类的。
不过 `__debug__` 真的很有趣,因为尽管它是一个普通标识符(不像 `True`、`False` 和 `None`),它是这门语言中唯一不能被赋值的标识符:
``
>>> __debug__ = 67
File "", line 1
SyntaxError: cannot assign to __debug__
``
你甚至不能把它作为属性来赋值:
``
>>> x.__debug__ = 67
File "", line 1
SyntaxError: cannot assign to __debug__
``
同样,没有其他标识符有这样的行为。这确实是一个特殊的特例。
但因为它不是关键字,它的行为与 `True`、`False` 和 `None` 略有不同:`x.__debug__` 会抛出 `AttributeError`(而不是 `SyntaxError`),因为它在语法上是有效的;它只是在查找一个不存在的属性。
有趣的是,试图删除 `__debug__` 时也会有一条特殊的错误消息(尽管如果没有这个特例,这本来会抛出 `NameError`),但这*不*适用于删除名为 `__debug__` 的属性:
``
>>> del __debug__
File "", line 1
SyntaxError: cannot delete __debug__
>>> del x.__debug__
Traceback (most recent call last):
File "", line 1, in
NameError: name 'x' is not defined
``
如果 `x` 已定义,则会抛出 `AttributeError`。不管怎样,出于某种原因,这都不是 `SyntaxError`(不像赋值那样)。
### 题外话:`SyntaxError` 是个谎言
说到错误:赋值给 `__debug__` 是我所知的少数几个情况之一,尽管某件事实际上并非无效语法,却会抛出 `SyntaxError`。你可以自己验证:
``
>>> assert (__debug__ := 67)
``
在调试构建中运行该断言会抛出 `SyntaxError`,但在使用 `-O` 时,断言永远不会被编译,因此不会抛出异常。
另外两个这样的例子是在函数外使用 `yield` 或 `await`:
``
>>> assert (yield)
>>> assert (await 67)
``
## `Ellipsis` 和 `NotImplemented`
`Ellipsis` 和 `NotImplemented` 在参考文档的“常量”部分有记载,但与另外 4 个常量不同,它们并不是“真正的”常量。它们只是普通的內建名称,所以可以被全局变量遮蔽:
``
>>> NotImplemented = 67
>>> NotImplemented
67
``
同样,我很好奇这里的设计理由。为什么它们不是特殊的,而其他常量却是特殊的?
## 覆写常量
这里有些有趣的事:尽管 `True`、`False` 和 `None` 是词法标记,它们也作为普通的內建对象存在:
``
>>> import builtins
>>> getattr(builtins, 'True')
True
>>> getattr(builtins, 'False')
False
>>> getattr(builtins, 'None') is None
True
``
如果不使用 `getattr`,就没有办法直接访问这些。
但真正有趣的是:`setattr` 也能用!
``
>>> setattr(builtins, 'True', 67)
>>> getattr(builtins, 'True')
67
``
然而,当使用词法标记访问时,这并不会改变其值:
``
>>> True
True
``
但 `__debug__` 也有同样的行为!
``
>>> setattr(builtins, '__debug__', 67)
>>> builtins.__debug__
67
>>> __debug__
True
``
所以 `__debug__` 有点*可以被*赋值,但尽管它不是词法标记,它和 `True`、`False`、`None` 一样被特殊处理:它的值不受 builtins 模块变化的影响。所以它确实是一个常量!
`Ellipsis` 和 `NotImplemented` 再次证明了它们实际上并不是常量:
``
>>> setattr(builtins, 'Ellipsis', 67)
>>> Ellipsis
67
``
不过这并不会改变 `...` 的值:
``
>>> ...
Ellipsis
``
所以从某种意义上说,`...` 是一个真正的常量,但 `Ellipsis` 不是。很奇怪,对吧?
相似文章
Python 字符串字面量有点搞笑
一篇博客文章,探讨 Python 原始字符串字面量和 f-string 语法中的古怪行为,包括原始字符串不能以反斜杠结尾以及 f-string 表达式可以包含注释和多行的示例。
@charliermarsh: 对于这个126字节的代码片段:- ty 0.0.65 栈溢出 - mypy 2.3.0 段错误 - Pyright 1.1.411 超时 - Pyrefly 1.1.…
一个126字节的代码片段导致多个Python类型检查器(ty、mypy、Pyright、Pyrefly、Pycroscope)崩溃、挂起或恐慌,凸显了这些工具中有趣的边界情况。
PHP 的古怪特性
一位开发者在使用了五年后反思 PHP 的古怪之处,重点介绍了其数组实现和类型系统的奇特之处。
现在需要运行五个 Python 类型检查器吗?
一篇博文主张 Python 库维护者应优先在测试套件中运行多个类型检查器,以确保公共 API 的兼容性,并强调了兼容性和代码污染方面的挑战。
关于NaN的两个案例分析
本文探讨了NaN在Python和Lua中的两种意外行为,其中对相等性和比较的隐含假设导致了令人惊讶的结果,例如Python列表相等性忽略了NaN的自不等性,以及Lua的for循环将NaN作为步长或限制时处理不当。