namedtuple со строкой юникода в качестве имени

У меня возникли проблемы с назначением строк Unicode в качестве имен для namedtuple. Это работает:

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

и это не:

b = collections.namedtuple("βαδιζόντων", "value")

я получаю ошибку

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?


person Thomas    schedule 28.05.2015    source источник
comment
Кое-что, что я заметил: он отлично работает, если я пропущу ó. Мне кажется ошибка.   -  person smheidrich    schedule 28.05.2015
comment
Интересно - я должен был проверить это на себе. ó — единственный символ из блока расширенного греческого языка Юникода, так что это может иметь значение. Но это все равно не согласуется с тем, что говорится в документации.   -  person Thomas    schedule 28.05.2015
comment
При ближайшем рассмотрении оказывается, что по какой-то причине 'ó' является '\xe1\xbd\xb9' в исходном файле с кодировкой UTF-8, но превращается в '\xcf\x8c' в коде, сгенерированном namedtuple для создания своего класса. Это определенно похоже на ошибку.   -  person smheidrich    schedule 28.05.2015
comment
Не могли бы вы воспользоваться моим предложением и посмотреть, сработает ли оно для вас?   -  person knitti    schedule 28.05.2015


Ответы (3)


Проблема именно с буквой (U+1F79 греческая строчная буква омикрон с оксиа). Это «символ совместимости»: вместо этого Unicode предпочитает использовать ό (U+03CC строчная греческая буква омикрон с тоносом). U + 1F79 существует только в Unicode для того, чтобы вернуться к старым наборам символов, в которых различались oxia и tonos, различие, которое позже оказалось неверным.

Когда вы используете символы совместимости в идентификаторе, синтаксический анализатор исходного кода Python автоматически нормализует их для формирования NFKC, поэтому имя вашего класса заканчивается на U+03CC.

К сожалению, collections.namedtuple об этом не знает. Он создает новый экземпляр класса, вставляя заданное имя в кучу кода Python в строке, затем execидентифицируя его (гадость, верно?) и извлекая класс из результирующего локального словаря, используя его имя... исходное имя, а не нормализованную версию, которую Python фактически скомпилировал, поэтому она не работает.

Это ошибка в collections, которую, возможно, стоит зарегистрировать, но сейчас вы должны использовать канонический символ U+03CC ό.

person bobince    schedule 28.05.2015
comment
Арг, теперь я понял! Меня несколько раз укусили эти символы совместимости для греческих букв с акцентом. По крайней мере, это позволяет мне обойти проблему. Спасибо за ваше объяснение! - person Thomas; 28.05.2015
comment
Будет полезна ссылка на исходный код hg.python. орг/cpython/файл/661cdbd617b8/Lib/коллекции/ - person kasravnd; 28.05.2015

Этот ó — это U+1F79 ɢʀᴇᴇᴋ sᴍᴀʟʟ ʟᴇᴛᴛᴇʀ ᴏᴍɪᴄʀᴏɴ ᴡɪᴛʜ ᴏxɪᴀ. Идентификаторы Python нормализуются как NFKC, а U+1F79 в NFKC становится U+03CC ɢʀᴇᴇᴋ sᴍᴀʟʟ ʟᴇᴛᴛᴇʀ ᴏᴍɪᴄʀᴏɴ ᴡɪᴛʜ ᴛᴏɴᴏs.

Интересно, что если вы используете ту же строку с заменой U+1F79 на U+03CC, она работает.

>>> b = collections.namedtuple("βαδιζ\u03CCντων", "value")
>>>

В документации для namedtuple утверждается, что «любой действительный идентификатор Python может использоваться для имени поля». Обе строки являются допустимыми идентификаторами Python, что легко проверить в интерпретаторе.

>>> βαδιζόντων = 0
>>> βαδιζόντων = 0
>>>

Это определенно ошибка в реализации. Я проследил это до этого бита в реализации namedtuple:

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

Я предполагаю, что имя типа, оставленное в словаре namespace при выполнении шаблона class_definition, будучи идентификатором Python, будет в форме NFKC и, таким образом, больше не будет соответствовать фактическому значению переменной typename, используемой для его извлечения. Я считаю, что простая предварительная нормализация typename должна исправить это, но я не проверял это.

person R. Martinho Fernandes    schedule 28.05.2015
comment
Спасибо, я продезинфицирую свой вклад и надеюсь на лучшее! - person Thomas; 28.05.2015

Хотя уже есть принятый ответ, позвольте мне предложить

Устранение проблемы

# 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. Идея Мартиньо Фернандеса о предварительной номинализации имен.

person knitti    schedule 28.05.2015
comment
Спасибо, это очень полезно! Я подозреваю, что это не решит мой конкретный вариант использования (который включает в себя извлечение списка слов из текстового файла и сравнение его со списком известных слов), но это очень хорошо! - person Thomas; 28.05.2015
comment
это может помочь, зависит от того, как/почему вы его сравниваете... вы можете удалить объединение символов из формы NFKC с помощью регулярного выражения и завершить всю нормализацию с помощью функции lower() - person knitti; 28.05.2015