标识符规范化:为什么将微符号转换为希腊字母 mu?

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

提问人:poke 提问时间:12/5/2015 最后编辑:Peter O.poke 更新时间:5/2/2018 访问量:2453

问:

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

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

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

python-3.x unicode 标识符 python-internals

评论


答:

33赞 poke 12/5/2015 #1

这里涉及两个不同的角色。一个是 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 规范化导致标识符的有效字符序列。Llidentifierxid_startxid_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 做的事情。不幸的是,实际上没有一种很好的方法来检测这种行为,从而导致如图所示的错误。通常,当标识符仅被称为标识符时,即它像实际变量或属性一样使用,那么一切都会好起来的:每次都运行规范化,并找到标识符。

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

在这些情况下,您需要记住自己执行规范化。我们可以利用 unicodedata.normalize 来实现这一点,这也允许我们从内部(或使用)正确地获取我们的值:locals()getattr

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

评论

1赞 Galax 12/5/2015
这是非常清楚和彻底的。即使在字符串文字中,我仍然尽量避免使用非 ASCII 字符,更不用说变量名称了。规范化只是一个问题,事情也可能被一些编辑器破坏,复制和粘贴更改编码等。class Test: mu = 'foo'
1赞 poke 12/5/2015
只要你对源文件使用 UTF-8(你真的应该这样做),你就可以在大多数情况下使用 Python 3,尤其是在字符串文字中。如果你有一个编辑器可以搞砸它,你应该得到一个更好的编辑器;)至于标识符,您也可以在那里发挥创意,除了显示的问题可能会给某些人带来问题,或者对其他人来说完全没有注意到:)
3赞 abarnert 5/2/2018 #2

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.orgpython-ideas 列表中collections.identifierdict


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

2. 例如,Julia 允许 Unicode 货币和数学符号,并且还具有 LaTeX 和 Unicode 标识符之间的映射规则——但他们明确添加了规则来规范化 ɛμ 到希腊语后者......