提问人:poke 提问时间:12/5/2015 最后编辑:Peter O.poke 更新时间:5/2/2018 访问量:2453
标识符规范化:为什么将微符号转换为希腊字母 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,即键盘上的一个,另一个是希腊文小写字母 MU。
为了理解这是怎么回事,我们应该看看 Python 是如何在语言参考中定义标识符的:
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 和希腊语小写字母 MU 都是 unicode 组(小写字母)的一部分,因此它们都可以在标识符的任何位置使用。现在请注意,实际的定义是指 和 ,它们被定义为相应非 x 定义中的所有字符,其 NFKC 规范化导致标识符的有效字符序列。Ll
identifier
xid_start
xid_continue
Python 显然只关心标识符的规范化形式。下面证实了这一点:
解析时,所有标识符都转换为正常形式的 NFKC;标识符的比较基于 NFKC。
NFKC 是一种 Unicode 规范化,可将字符分解为单独的部分。MICRO SIGN分解成希腊小写字母MU,这正是那里发生的事情。
还有很多其他字符也受到这种规范化的影响。另一个例子是 OHM SIGN,它分解为希腊语大写字母 OMEGA。将其用作标识符会给出类似的结果,此处使用局部变量显示:
>>> Ω = '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
和 locals
,都是在字典上操作的。 通过对象的 访问对象的属性,并返回字典。在字典中,键可以是任何字符串,所以在里面有一个 MICRO SIGN 或 OHM SIGN 是完全可以的。getattr()
__dict__
locals()
在这些情况下,您需要记住自己执行规范化。我们可以利用 unicodedata.normalize
来实现这一点,这也允许我们从内部(或使用)正确地获取我们的值:locals()
getattr
>>> normalized_ohm = unicodedata.normalize('NFKC', 'Ω')
>>> locals()[normalized_ohm]
'bar'
评论
class Test: mu = 'foo'
Python 在这里所做的是基于 Unicode 标准附件 #31:
考虑规范化和大小写的实现有两种选择:将变体视为等效变体,或不允许变体。
本节的其余部分提供了更多细节,但基本上,这意味着如果一种语言允许您使用命名的标识符,它应该将两个字符 MICRO SIGN 和 GREEK SMALL LETTER MU 视为相同的字符,并且应该将它们都视为 GREEK SMALL LETTER MU。µ
µ
大多数允许非 ASCII 标识符的其他语言都遵循相同的标准;1 只有少数几种语言发明了自己的语言。2 因此,此规则的优点是跨多种语言都相同(并且可能得到 IDE 和其他工具的支持)。
可以说,在像 Python 这样反射量很大的语言中,它确实不能很好地工作,在 Python 中,字符串可以像编写一样轻松地用作标识符。但是,如果您能阅读 python-3000 邮件列表讨论,围绕 PEP 3131;唯一认真考虑的选择是坚持使用 ASCII、UAX-31 或 Java 对 UAX-31 的微小变体;没有人愿意为Python发明一个新标准。getattr(Test, 'µ')
解决此问题的另一种方法是添加一个记录在案的类型,以应用与编译器应用于源代码中的标识符完全相同的查找规则,并在旨在用作命名空间的映射中使用该类型(例如,对象、模块、局部变量、类定义)。我依稀记得有人建议过,但没有任何好的激励例子。如果有人认为这是一个足够好的例子来恢复这个想法,他们可以将其发布在 bugs.python.org 或 python-ideas 列表中。collections.identifierdict
1. 一些语言,如 ECMAScript 和 C#,改用“Java 标准”,它基于早期形式的 UAX-31,并添加了一些小的扩展,比如忽略 RTL 控制代码——但这已经足够接近了。
2. 例如,Julia 允许 Unicode 货币和数学符号,并且还具有 LaTeX 和 Unicode 标识符之间的映射规则——但他们明确添加了规则来规范化 ɛ
和 μ
到希腊语后者......
评论