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

31

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

>>> 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')

我输入的字符键盘上始终是µ符号,但出现转换成其他符号的情况。这是为什么呢?

2个回答

33
这里涉及到两个不同的字符。一个是键盘上的微符号,另一个是希腊小写字母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和GREEK SMALL LETTER MU,都是Ll Unicode组(小写字母)的一部分,因此它们都可以在标识符的任何位置使用。现在请注意,标识符的定义实际上是指xid_startxid_continue,这些被定义为各自非-x定义中所有字符的NFKC归一化结果,其结果为标识符的有效字符序列。
显然,Python只关心标识符的规范化形式。这在下面得到了确认:

在解析时,所有标识符都被转换为正常形式NFKC;标识符的比较基于NFKC。

NFKC是Unicode normalization,将字符分解为单独的部分。MICRO SIGN分解为GREEK SMALL LETTER MU,这正是正在发生的事情。

还有很多其他字符也受到这种规范化的影响。另一个例子是欧姆符号,它分解成希腊大写字母欧米伽。使用它作为标识符会得到类似的结果,这里使用本地语言显示:

>>> Ω = '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的一种行为。不幸的是,没有真正好的方法来检测这种行为,导致了像所示的错误。通常,在标识符仅被称为标识符时,即它被用作实际变量或属性时,一切都会很好:规范化每次运行,并找到标识符。
唯一的问题在于基于字符串的访问。字符串只是字符串,当然没有规范化发生(那将是一个坏主意)。而这里展示的两种方式,getattrlocals,都操作字典。 getattr()通过对象的__dict__访问对象的属性,而locals()返回一个字典。在字典中,键可以是任何字符串,因此在其中具有微符号或欧姆符号是完全可以的。

在这些情况下,你需要记得自己进行规范化。我们可以利用unicodedata.normalize来实现这一点,这样我们也可以正确地从locals()(或使用getattr)中获取我们的值:

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

1
那非常清晰和详尽。我仍然尝试避免在字符串字面值中使用非ASCII字符,更不用说变量名了。规范化只是一个问题,有些编辑器可能会弄乱事情,复制和粘贴也可能会改变编码等等。class Test: mu = 'foo' - Galax
1
只要你在源文件中使用UTF-8编码(你真的应该这样做),在大多数情况下,Python 3中的字符串字面量都可以正常工作。如果你的编辑器可能会搞砸它,那么你应该换一个更好的编辑器 ;) 至于标识符,你也可以在那里发挥创意,除了显示问题可能会对某些人造成问题或完全被忽略之外 :) - poke

3

Python在这里做的基于Unicode标准附录#31

考虑规范化和大小写的实现有两种选择:将变体视为等效或禁止变体。

本节的其余部分提供了更多细节,但基本上,这意味着如果语言允许您使用名为µ的标识符,则应将两个µ字符MICRO SIGN和GREEK SMALL LETTER MU视为相同,并且应通过将它们都视为GREEK SMALL LETTER MU来处理它们。


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

可以说,在像Python这样反射密集型的语言中,它的效果并不像其他语言那么好,因为字符串可以像编写getattr(Test, 'µ')一样轻松地用作标识符。但是如果您可以阅读python-3000邮件列表讨论,关于PEP 3131; 只考虑了保持ASCII、UAX-31或Java对UAX-31的微小变化;没有人想为Python发明一个新的标准。

另一种解决这个问题的方法是添加一个collections.identifierdict类型,文档说明应用于查找的确切规则与编译器在源中应用于标识符的规则相同,并在映射中使用该类型,以便作为命名空间(例如,对象、模块、本地变量、类定义)。我依稀记得有人建议过这样做,但没有任何好的动机例子。如果有人认为这是足够好的例子来恢复这个想法,他们可以在bugs.python.orgthe python-ideas list上发布它。
1. 一些语言,例如ECMAScript和C#,使用基于早期UAX-31的“Java标准”,并添加了一些小扩展,例如忽略RTL控制代码,但这已经足够接近了。
2. 例如,Julia 允许使用Unicode货币和数学符号,并且还有一些规则用于LaTeX和Unicode标识符之间的映射 - 但他们明确添加了规则以将 ɛµ 规范化为希腊字母...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接