Python 的预定义常量有点奇怪
Python's pre-declared constants are kinda weird

原始链接: https://sebsite.pw/w/20260801-pythonconstants.html

Python 具有六个内置“常量”,它们表现出令人惊讶的不一致行为。这些常量可分为三大类: 1. **词法关键字 (`True`, `False`, `None`):** 这些被硬编码在词法分析器中。由于它们不是标准标识符,尝试将它们用作属性(例如 `x.True`)会触发 `SyntaxError`。 2. **特殊标识符 (`__debug__`):** 虽然它在技术上是一个标识符,但它被硬编码以防止通过 `SyntaxError` 进行赋值或删除。它作为一个常量来跟踪 Python 的优化状态,即使内置对象被手动覆盖,它也保持不变。有趣的是,它还会产生“欺骗性”的语法错误,导致看起来语法正确的代码被编译器拒绝。 3. **伪常量 (`Ellipsis`, `NotImplemented`):** 尽管被称为常量,但它们仅仅是标准的内置变量。与前几类不同,它们可以被屏蔽或覆盖。 这种不一致性在 `...`(`Ellipsis` 的字面量)中表现得更为明显,即使修改了 `Ellipsis` 内置对象,`...` 依然保持不变。归根结底,Python 对这些值的处理在编译器级保护和运行时灵活性方面存在差异,导致本应是简单的语言常量在 Python 中呈现出一种奇怪且不统一的局面。

这次 Hacker News 的讨论围绕着一篇文章展开,该文章质疑了 Python 中预声明常量(如 `True`、`False` 和 `None`)的本质。参与者讨论了它们是否是在解析过程中确定的词法记号,并指出省略号(`...`)的运作方式与之类似。 除了技术细节外,对话还涉及了 Python 生态系统的更广泛现状。一位用户表达了对该语言的失望,认为它曾经被视为对 PHP 等语言的现代改进,如今却演变成了一种“脆弱”且“过时”的工具,并特别指出了松散类型、不一致的标准库以及碎片化的包管理系统所带来的挑战。另一位评论者则认为,考虑到 Python 30 年的历史,以及作为时代产物所积累的“技术包袱”,这些问题或许是不可避免的。
相关文章

原文

python has 6 pre-declared "constants": True, False, None, __debug__, Ellipsis (or equivalently ...), and NotImplemented. but they all behave slightly differently, for some reason.

True, False, and None

True, False, and None are keywords. they aren't identifiers, they're just straight up their own lexical tokens. which is really weird; nothing else is like this in python. usually stuff is resolved during regular name resolution, not in the lexer itself.

an interesting side effect of this is that expressions like x.True raise a SyntaxError. i'm curious as to what the rationale was for this decision (if there was one).

there's some more interesting stuff with these constants, but i'll get to it later, since it ties in with the other constants.

__debug__

__debug__ is a boolean constant: it's normally True, but when running with -O, it's False. the idea is similar to how assert is disabled in non-debug builds: you can wrap code in if __debug__ if the check would be too expensive in an "optimized" build, or something.

__debug__ is really interesting though, because although it's a normal identifier (unlike True, False, and None), it's the only identifier in the language which can't be assigned to:

>>> __debug__ = 67
  File "", line 1
SyntaxError: cannot assign to __debug__

you can't even assign to it as an attribute:

>>> x.__debug__ = 67
  File "", line 1
SyntaxError: cannot assign to __debug__

again, no other identifier behaves like this. this is a true special case.

but because it's not a keyword, it behaves slightly differently to True, False, and None:
x.__debug__ raises AttributeError (rather than SyntaxError), since it's syntactically valid; it's just looking up an attribute which doesn't exist.

interestingly, there's also a special error message for attempting to delete __debug__ (despite the fact that this would raise a NameError anyway if not for the special case), but this doesn't apply for deleting an attribute named __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

if x were defined, an AttributeError would be raised instead. in either case, it's not a SyntaxError (unlike assignment), for some reason.

tangent: SyntaxError is a lie

speaking of errors: assigning to __debug__ is one of only a few cases i'm aware of where a SyntaxError is raised despite something not actually being invalid syntax. here, you can confirm it yourself:

>>> assert (__debug__ := 67)

running that assert in a debug build raises a SyntaxError, but with -O, the assertion is never compiled, and so no exception is raised.

two other instances of this are using yield or await outside of a function:

>>> assert (yield)
>>> assert (await 67)

Ellipsis and NotImplemented

Ellipsis and NotImplemented are documented in the "constants" section of the reference, but unlike the other 4 constants, they aren't "real" constants. they're just normal builtins, so they can be shadowed by globals:

>>> NotImplemented = 67
>>> NotImplemented
67

again, i'm curious about the rationale here. why is it that these aren't special, but the other constants are?

overwriting constants

here's something interesting: despite being lexical tokens, True, False, and None also exist as normal builtins:

>>> import builtins
>>> getattr(builtins, 'True')
True
>>> getattr(builtins, 'False')
False
>>> getattr(builtins, 'None') is None
True

there's no way to directly access these without using getattr.

but here's where things get really interesting: setattr also works!

>>> setattr(builtins, 'True', 67)
>>> getattr(builtins, 'True')
67

however, this doesn't change the value when accessed with the lexical token:

>>> True
True

but __debug__ has the same behavior!

>>> setattr(builtins, '__debug__', 67)
>>> builtins.__debug__
67
>>> __debug__
True

so __debug__ can sorta be assigned to, but despite not being a lexical token, it's special cased just like True, False, and None: its value is unaffected by changes to the builtins module. so it really is a constant!

Ellipsis and NotImplemented are, once again, not actually constants:

>>> setattr(builtins, 'Ellipsis', 67)
>>> Ellipsis
67

this doesn't change the value of ... though:

>>> ...
Ellipsis

so in some sense, ... is a real constant, but Ellipsis isn't. weird, right?

联系我们 contact @ memedata.com