标识符归一化:微符号为什么要转换成希腊字母mu?

Identifier normalization: Why is the micro sign converted into the Greek letter mu?

我刚刚偶然发现了以下奇怪的情况:

>>> class Test:
        µ = 'foo'

>>> Test.µ
'foo'
>>> getattr(Test, 'µ')
Traceback (most recent call last):
  File "<pyshell#4>", line 1, in <module>
    getattr(Test, 'µ')
AttributeError: type object 'Test' has no attribute 'µ'
>>> 'µ'.encode(), dir(Test)[-1].encode()
(b'\xc2\xb5', b'\xce\xbc')

我输入的字符一直是键盘上的 µ 符号,但由于某种原因它被转换了。为什么会这样?

这里涉及两个不同的角色。一个是 MICRO SIGN, which is the one on the keyboard, and the other is GREEK SMALL LETTER MU.

要了解发生了什么,我们应该看看 Python 如何在 language reference 中定义标识符:

identifier   ::=  xid_start xid_continue*
id_start     ::=  <all characters in general categories Lu, Ll, Lt, Lm, Lo, Nl, the underscore, and characters with the Other_ID_Start property>
id_continue  ::=  <all characters in id_start, plus characters in the categories Mn, Mc, Nd, Pc and others with the Other_ID_Continue property>
xid_start    ::=  <all characters in id_start whose NFKC normalization is in "id_start xid_continue*">
xid_continue ::=  <all characters in id_continue whose NFKC normalization is in "id_continue*">

我们的字符 MICRO SIGN 和 GREEK SMALL LETTER MU 都是 Ll unicode 组(小写字母)的一部分,因此它们都可以在标识符中的任何位置使用。现在请注意,identifier 的定义实际上指的是 xid_startxid_continue,它们被定义为相应非 x 定义中的所有字符,其 NFKC 规范化导致有效字符序列一个标识符。

Python 显然只关心标识符的 规范化 形式。这一点在下面得到证实:

All identifiers are converted into the normal form NFKC while parsing; comparison of identifiers is based on NFKC.

NFKC 是 Unicode normalization 将字符分解为各个部分。 MICRO SIGN 分解为希腊小写字母 MU,这正是发生的事情。

还有很多其他字符也受此规范化影响。另一个例子是 OHM SIGN which decomposes into GREEK CAPITAL LETTER OMEGA。使用它作为标识符会得到类似的结果,此处使用 locals 显示:

>>> Ω = 'bar'
>>> locals()['Ω']
Traceback (most recent call last):
  File "<pyshell#1>", line 1, in <module>
    locals()['Ω']
KeyError: 'Ω'
>>> [k for k, v in locals().items() if v == 'bar'][0].encode()
b'\xce\xa9'
>>> 'Ω'.encode()
b'\xe2\x84\xa6'

所以最后,这只是 Python 所做的事情。不幸的是,并没有真正好的方法来检测这种行为,从而导致如图所示的错误。通常,当标识符仅被称为标识符时,即像真正的变量或属性一样使用时,一切都会好起来的:每次都运行归一化,并找到标识符。

唯一的问题是基于字符串的访问。字符串只是字符串,当然没有规范化发生(那只是个坏主意)。这里显示的两种方式 getattr and locals 都对字典进行操作。 getattr() 通过对象的 __dict__locals() returns 字典访问对象的属性。在字典中,键可以是任何字符串,所以里面有一个 MICRO SIGN 或一个 OHM SIGN 就完全没问题了。

在这些情况下,您需要记住自己执行规范化。为此,我们可以利用 unicodedata.normalize,这也使我们能够从 locals()(或使用 getattr)中正确获取我们的值:

>>> normalized_ohm = unicodedata.normalize('NFKC', 'Ω')
>>> locals()[normalized_ohm]
'bar'

What Python does here is based on Unicode Standard Annex #31:

Implementations that take normalization and case into account have two choices: to treat variants as equivalent, or to disallow variants.

本节的其余部分提供了更多详细信息,但基本上,这意味着如果一种语言允许您拥有一个名为 µ 的标识符,它应该将两个 µ 字符 MICRO SIGN 和 GREEK SMALL LETTER MU 相同,应该将它们都视为 GREEK SMALL LETTER MU。


大多数其他允许非 ASCII 标识符的语言都遵循相同的标准;1 只有少数语言发明了自己的标准。2 所以, 此规则的优点是在多种语言中都是相同的(并且可能受到 IDE 和其他工具的支持)。

可以证明它在像 Python 这样重反射的语言中确实不能很好地工作,在这种语言中,字符串可以像书写 getattr(Test, 'µ') 一样容易地用作标识符。但是如果你能阅读the python-3000 mailing list discussions, around PEP 3131;唯一认真考虑过的选项是坚持使用 ASCII、UAX-31 或 Java 在 UAX-31 上的微小变化;没有人愿意为 Python.

发明一个新标准

解决此问题的另一种方法是添加一个 collections.identifierdict 类型,该类型被记录为应用与编译器在源代码中应用的标识符完全相同的查找规则,并在预期的映射中使用该类型用作名称空间(例如,对象、模块、局部变量、class 定义)。我依稀记得有人这么建议,但没有任何好的激励例子。如果有人认为这是一个足够好的例子来重振这个想法,他们可以 post 在 bugs.python.org or the python-ideas list.


1.一些语言,如 ECMAScript 和 C#,使用 "Java standard" 代替,它基于 UAX-31 的早期形式并添加了一些小的扩展,比如忽略 RTL 控制代码——但这已经足够接近了。

2。例如,Julia 允许 Unicode 货币和数学符号,也有 LaTeX 和 Unicode 标识符之间的映射规则——但他们明确添加规则以将 ɛµ 规范化为希腊后者……