使用Unicode字符串作为名称的命名元组

9

我在为一个命名元组分配Unicode字符串作为名称时遇到了问题。以下方式可行:

a = collections.namedtuple("test", "value")

而这个则不行:
b = collections.namedtuple("βαδιζόντων", "value")

I get the error

Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "/usr/lib64/python3.4/collections/__init__.py", line 370, in namedtuple
        result = namespace[typename]
KeyError: 'βαδιζόντων'

为什么会这样呢?文档上说,“Python 3也支持在标识符中使用Unicode字符”,而且关键字是有效的Unicode吗?

1
我注意到一件事:如果我省略掉“ó”,它就可以正常工作。对我来说似乎是一个错误。 - smheidrich
有趣 - 我应该自己测试一下。ó是来自Unicode“希腊扩展”块的唯一字符,因此这可能与之相关。但它仍然与文档所说的不符。 - Thomas
经过仔细检查,发现问题在于某些原因,“'ó'”在UTF-8编码的源文件中是“'\xe1\xbd\xb9'”,但在由“namedtuple”生成其类的代码中变成了“'\xcf\x8c'”。这绝对看起来像是一个错误。 - smheidrich
你能否尝试一下我的建议,并查看它是否适用于你? - knitti
3个回答

6
问题出在字符 (U+1F79 带有希腊氧音符的小写omicron字母) 上。这是一个“兼容字符”:Unicode 更希望您使用 ό (U+03CC 带有希腊重音符的小写omicron字母)。U+1F79 仅存在于Unicode中,以便回溯到区分氧音符和重音符的旧字符集,但后来发现该区分是不正确的。
当您在标识符中使用兼容字符时,Python源代码解析器会自动将它们规范化为NFKC形式,因此您的类名最终会包含 U+03CC。
不幸的是,collections.namedtuple 不知道这一点。它创建新类实例的方式是将给定名称插入到一堆Python代码字符串中,然后执行它(很糟糕,对吧?),并从结果局部字典中使用其名称提取类...原始名称,而不是Python实际编译的规范化版本,因此失败了。
这是collections中的一个错误,值得报告,但现在您应该使用规范字符 U+03CC ό

啊,现在我明白了!我已经多次被希腊重音字母的兼容性字符所困扰。至少这让我能够解决问题。谢谢你的解释! - Thomas
一个源代码的引用会很有用 https://hg.python.org/cpython/file/661cdbd617b8/Lib/collections/__init__.py#l332 - Mazdak

2

那个 ó 是 U+1F79 带氧音的希腊小写字母omicron。Python标识符被规范化为NFKC,而在NFKC中,U+1F79 变成了 U+03CC 带音调的希腊小写字母omicron。

有趣的是,如果您使用替换U+1F79为U+03CC的相同字符串,则可以正常工作。

>>> b = collections.namedtuple("βαδιζ\u03CCντων", "value")
>>>
namedtuple 的文档声称“任何有效的 Python 标识符都可以用作字段名”。这两个字符串都是有效的 Python 标识符,可以在解释器中轻松测试。
>>> βαδιζόντων = 0
>>> βαδιζόντων = 0
>>>

这绝对是实现中的一个bug。我追踪到它在namedtuple实现中的这一位:

namespace = dict(__name__='namedtuple_%s' % typename)
exec(class_definition, namespace)
result = namespace[typename] # here!

我猜想通过执行class_definition模板所留在namespace字典中的类型名称是一个Python标识符,并且已经处于NFKC格式,因此不再匹配用于检索它的typename变量的实际值。我相信只需预先规范化typename即可解决此问题,但我尚未进行测试。


谢谢,我会清理我的输入并希望一切顺利! - Thomas

1
虽然已经有一个被接受的答案,但我想提供一种解决方法。
问题的修复
# coding: utf-8
import collections
import unicodedata


def namedtuple_(typename, field_names, verbose=False, rename=False):
    ''' just like collections.namedtuple(), but does unicode nomalization
        on names
    '''

    if isinstance(field_names, str):
        field_names = field_names.replace(',', ' ').split()
    field_names = [
        unicodedata.normalize('NFKC', name) for name in field_names]
    typename = unicodedata.normalize('NFKC', typename)

    return collections.namedtuple(
        typename, field_names, verbose=False, rename=False)


βαδιζόντων = namedtuple_('βαδιζόντων', 'value')

a = βαδιζόντων(1)

print(a)
# βαδιζόντων(value=1)
print(a.value == 1)
# True

它是做什么的?

使用这个namedtuple_()实现在将名称交给collections.namedtuple()之前对名称进行了规范化,从而使名称一致。

这是对@R. Martinho Fernandes预先规范名称想法的阐述。


谢谢,这非常有帮助!我怀疑它不能解决我的特定用例(涉及从文本文件中提取单词列表并将其与已知单词列表进行比较),但是这非常好! - Thomas
这取决于您比较的方式和原因,它可能会有帮助…… 您可以使用正则表达式从NFKC形式中删除组合字符,并使用lower()完成整个规范化。 - knitti

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