Python 2.7: Странное поведение Юникода

Я испытываю следующее поведение в Python 2.7:

>>> a1 = u'\U0001f04f'  #1
>>> a2 = u'\ud83c\udc4f'  #2
>>> a1 == a2  #3
False
>>> a1.encode('utf8') == a2.encode('utf8')  #4
True
>>> a1.encode('utf8').decode('utf8') == a2.encode('utf8').decode('utf8')  #5
True
>>> u'\ud83c\udc4f'.encode('utf8') #6
'\xf0\x9f\x81\x8f'
>>> u'\ud83c'.encode('utf8')  #7
'\xed\xa0\xbc'
>>> u'\udc4f'.encode('utf8')  #8
'\xed\xb1\x8f'
>>> '\xd8\x3c\xdc\x4f'.decode('utf_16_be')  #9
u'\U0001f04f'

Чем объясняется такое поведение? Более конкретно:

  1. Я ожидаю, что две строки будут равны, если утверждение № 5 верно, а № 3 доказывает обратное.
  2. Кодирование обеих кодовых точек вместе, как в утверждении № 6, дает результаты, отличающиеся от того, когда кодируются по одному в № 7 и № 8. Похоже, две кодовые точки рассматриваются как одна 4-байтовая кодовая точка. Но что, если я действительно хочу, чтобы они рассматривались как две разные точки кода?
  3. Как вы можете видеть из № 9 числа в a2 на самом деле a1 закодированы с использованием UTF-16-BE, но хотя они были определены как кодовые точки Unicode с использованием \u внутри строки Unicode (!) Python все еще может каким-то образом достичь равенства в #5. Как это могло быть возможно?

Ничто не имеет смысла здесь! В чем дело?

1 ответ

Решение

Python 2 здесь нарушает стандарт Unicode, позволяя вам кодировать кодовые точки в диапазоне от U+D800 до U+DFFF, по крайней мере, в сборке UCS4. Из Википедии:

Стандарт Unicode постоянно резервирует эти значения кодовых точек для кодирования UTF-16 суррогатов высокого и низкого уровней, и им никогда не будет назначен символ, поэтому не должно быть оснований для их кодирования. Официальный стандарт Unicode гласит, что никакие формы UTF, включая UTF-16, не могут кодировать эти кодовые точки.

Официальный стандарт UTF-8 не имеет кодировки для кодовых точек суррогатной пары UTF-16, поэтому Python 3 вызывает исключение при попытке:

>>> '\ud83c\udc4f'.encode('utf8')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'utf-8' codec can't encode characters in position 0-1: surrogates not allowed

Но поддержка Unicode в Python 2 немного более элементарна, и поведение, которое вы наблюдаете, зависит от конкретного варианта сборки UCS2 / UCS4; в сборке UCS2 ваши переменные равны:

>>> import sys
>>> sys.maxunicode
65535
>>> a1 = u'\U0001f04f'
>>> a2 = u'\ud83c\udc4f'
>>> a1 == a2
True

потому что в такой сборке все не-BMP кодовые точки кодируются как суррогатные пары UTF-16 (расширение по стандарту UCS2).

Таким образом, в сборке UCS2 нет разницы между вашими двумя значениями, и выбор кодирования в полную кодовую точку, отличную от BMP, полностью допустим, если вы предполагаете, что захотите кодировать U+1F04F и другие подобные кодовые точки. Сборка UCS4 просто соответствует этому поведению.

Другие вопросы по тегам