Катастрофа Unicode В Python3

От переводчика: Армин Ронахер — довольно известный разработчик в сообществе Python (Flask, jinja2, werkzeug).

Своеобразный крестовый поход против Python3 он начал довольно давно, но обвинить его в истеричности и ретроградности не так-то просто: его возражения продиктованы серьезным опытом разработки, свою точку зрения он аргументирует довольно подробно.

Немного о терминологии: Я перевел приведение как принудительное преобразование кодировок, а байтовую строку как байтовую строку, поскольку термин «необработанные» строки по-прежнему означает нечто несколько иное.

"Историческая" справка: в 2012 году Армин предложил PEP 414, который содержал ряд мер по устранению проблем с Unicode, PEP был подтвержден довольно быстро, но воз и ныне там, так как текст ниже написан 5 января 2014 года.

Становится все труднее вести информированное обсуждение различий между Python 2 и 3, поскольку один из языков уже мертв.

а второй активно развивается.

Когда кто-то начинает обсуждать поддержку Unicode в двух ветках Python, это очень сложная тема.

Вместо того, чтобы рассматривать поддержку Unicode в двух версиях языка, я рассмотрю базовую модель обработки текстовых и байтовых строк.

В этом посте я покажу на примере решений от разработчиков языка и стандартной библиотеки, что Python 2 лучше работает с текстовыми и байтовыми строками.

Поскольку мне приходилось поддерживать много кода, который непосредственно занимался преобразованием между байтовыми строками и Unicode, ухудшение, произошедшее в Python3, вызвало у меня немало печали.

Меня они особенно раздражают материалы основная команда разработчиков Python, которая вдохновляет меня поверить в то, что Python 3 лучше, чем 2.7.



Модель текстового представления

Основное различие между Python 2 и Python 3 — это базовые типы, существующие для работы со строками и байтовыми строками.

В Python 3 у нас есть один тип строки: ул.

, который хранит данные в Юникоде, и два типа байтов: байты И байтаррей .

С другой стороны, в Python 2 есть два типа строк: ул.

, чего достаточно для любых целей, ограниченных строками в кодировке ASCII + некоторыми неопределенными данными, размер которых превышает 7-бит. Наряду с типом str Python2 имеет тип данных Юникод , эквивалентный типу данных ул.

Python 3. Для работы с байтами в Python 2 есть один тип: bytearray, взятый из Python 3. Присмотревшись к ситуации, можно увидеть, что из Python 3 убрали кое-что: поддержку строковых данных, не поддерживающих Юникод. Компенсацией за жертву стал хэшируемый байтовый тип данных( байты ).

Тип данных bytarray изменчив и поэтому не может быть хэширован.

Я очень редко использую двоичные данные в качестве ключей словаря, и поэтому возможность или невозможность хеширования двоичных данных мне не кажутся очень серьезными.

Особенно в Python 2, поскольку байты можно помещать в переменную, например ул.

.



Потерянный тип

В Python 3 удалена поддержка байтовых строк, которые были типом в ветке 2.x. ул.

.

На бумаге в этом решении нет ничего плохого.

С академической точки зрения строки, всегда представленные в Юникоде, хороши.

И это верно, если весь мир является вашим переводчиком.

К сожалению, в реальном мире всё работает иначе: вы вынуждены регулярно работать с разными кодировками, и в этом случае подход Python 3 к работе со строками трещит по швам.

Я буду с вами честен: способ обработки Unicode в Python 2 содержит ошибки, и я полностью одобряю улучшения в обработке Unicode. Моя позиция такова, что то, как это сделано в Python 3, является обратным и более ошибочным, и я совершенно ненавижу работать с Python 3.

Ошибки при работе с Юникодом

Прежде чем углубиться в детали, нам нужно понять разницу между поддержкой Unicode в Python 2 и 3. а также почему разработчики решили изменить механизм поддержки Unicode. Изначально Python 2, как и многие другие языки до него, создавался без поддержки обработки разных кодировок.

Строка — это просто строка, она содержит байты.

Это требовало от разработчиков корректной работы с различными ручные кодировки.

Это было вполне приемлемо для многих ситуаций.

На протяжении многих лет веб-фреймворк Django не работал с Юникодом, а использовал исключительно байтовые строки.

Тем временем Python 2 потратил годы на улучшение внутренней поддержки Unicode. Улучшенная поддержка Юникода.

позволило использовать его для единообразного представления данных в различных кодировках.

Подход к обработке строк с использованием определенной кодировки в Python 2 довольно прост: вы берете строку (байт), которую можно получить откуда угодно, а затем преобразуете это из кодировки, характерной для источника строки (метаданные, заголовки и т.д.) в строку Юникода.

Став строкой Unicode, она поддерживает все те же операции то же, что и байт, но теперь он может хранить более широкий диапазон символов.

Когда вам нужно передать строку для обработки куда-то еще, вы снова преобразовать его в кодировку, используемую принимающей стороной, и перед нами снова строка байтов Какие особенности связаны с этим подходом? Чтобы это работало на уровне ядра языка, Python 2 должен предоставить возможность перейти из мира без Unicode в прекрасный мир с Unicode. Это возможно путем принудительного преобразования байтовых и небайтовых строк.

Когда это произойдет и как работает этот механизм? Суть в том, что когда байтовая строка участвует в той же операции со строкой Юникода, затем строка байтов преобразуется в строку Юникода с использованием процесса неявного декодирования строки, в котором используется кодировка по умолчанию.

По умолчанию эта кодировка считается ASCII. В Python предусмотрена возможность изменения кодировки по умолчанию с помощью одного модуля, но теперь функции изменения кодировки по умолчанию удалены из модуля site.py, он установлен на ASCII. Если вы запустите интерпретатор с флагом , то функция sys.setdefaultencoding будет доступен вам, и вы сможете поэкспериментировать, чтобы узнать, что произойдет, если вы установите кодировку по умолчанию на UTF-8. В некоторых ситуациях могут возникнуть проблемы при работе с кодировкой по умолчанию: 1. неявная установка и преобразование кодировки при конкатенации:

  
  
  
  
  
  
  
  
  
  
  
   

>>> "Hello " + u"World" u'Hello World'

Здесь левая строка преобразуется с использованием кодировки по умолчанию в строку Unicode. Если строка содержит символы, отличные от ASCII, то при обычном выполнении программы преобразование останавливается с выдачей исключения UnicodeDecodeError, поскольку по умолчанию используется кодировка ASCII. 2. Неявная установка и преобразование кодировки при сравнении строк

>>> "Foo" == u"Foo" True

Это звучит опаснее, чем есть на самом деле.

Левая часть преобразуется в Unicode, а затем происходит сравнение.

В случае, если левая часть не может быть преобразована, интерпретатор выдает предупреждение и строки считаются неравными (в результате сравнения возвращается False).

Это совершенно здоровое поведение, даже если при первом знакомстве с ним так не кажется.

3. Явная установка и преобразование кодировки, как часть механизма с использованием кодеков.

Это одна из самых зловещих вещей и наиболее распространенный источник всех сбоев и недоразумений Unicode в Python 2. Чтобы преодолеть проблемы в этой области, Python 3 пошел на сумасшедший шаг, удалив метод .

decode() из строк Unicode и файла .

encode() из байтовых строк, это вызвало у меня наибольшее непонимание и раздражение.

С моей точки зрения, это очень глупое решение, но мне много раз говорили, что я ничего не понимаю, пути назад не будет. Явное преобразование кодировки при работе с кодеками выглядит так:

>>> "foo".

encode('utf-8') 'foo'

Эта строка, очевидно, является байтовой строкой.

Нам необходимо преобразовать его в UTF-8. Это само по себе бессмысленно, поскольку кодек UTF-8 преобразует строку Unicode в строку байтов в кодировке UTF-8. Как это работает? Кодек UTF-8 видит, что строка не является строкой Юникода, поэтому сначала выполняет принудительное преобразование в Юникод. Пока «foo» представляет собой только данные ASCII и кодировкой по умолчанию является ASCII, принудительное преобразование проходит успешно, и после этого строка Unicode u «foo» преобразуется в UTF-8.

Механизм кодека

Теперь вы знаете, что в Python 2 есть два подхода к представлению строк: байтовый и Unicode. Преобразование между этими представлениями осуществляется с помощью механизма кодека.

Этот механизм не навязывает Unicode-> byte или аналогичную схему преобразования.

Кодек может выполнять преобразование байт-> байт или Юникод-> Юникод. Фактически, система кодеков может реализовать преобразование между любыми типами Python. У вас может быть кодек JSON, который преобразует строку в сложный объект Python на ее основе, если вы считаете, что такое преобразование вам нужно.

Такое положение дел может вызвать проблемы в понимании механизма, начиная с его основ.

Примером этого может быть кодек под названием «undefined», который можно установить в качестве кодировки по умолчанию.

В этом случае любые принудительные преобразования кодировок строк будут отключены:

>>> import sys >>> sys.setdefaultencoding('undefined') >>> "foo" + u"bar" Traceback (most recent call last): raise UnicodeError("undefined encoding") UnicodeError: undefined encoding

А как решают проблему с кодеками в Python 3? Python 3 удаляет все кодеки, которые не выполняют преобразования формы: Unicode.<-> byte, а также теперь ненужные методы байтовой строки .

encode() и строковый метод .

decode().

Это очень плохое решение, так как оно было очень много полезных кодеков.

Например, в Python 2 очень часто используется преобразование шестнадцатеричного кодека:

>>> "\x00\x01".

encode('hex') '0001'

Хотя можно сказать, что в данном конкретном случае проблему можно решить с помощью модуля типа binascii, но проблема глубже, модули с кодеками доступны отдельно.

Например, библиотеки, реализующие чтение сокетов, используют кодеки для частичного преобразования данных из потоков данных библиотеки zlib:

>>> import codecs >>> decoder = codecs.getincrementaldecoder('zlib')('strict') >>> decoder.decode('x\x9c\xf3H\xcd\xc9\xc9Wp') 'Hello ' >>> decoder.decode('\xcdK\xceO\xc9\xccK/\x06\x00+\xad\x05\xaf') 'Encodings'

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

По этой причине Python теперь может генерировать следующие исключения:

>>> "Hello World".

encode('zlib_codec') Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'str' does not support the buffer interface

(Обратите внимание, что кодек теперь называется zlib_codec вместо zlib, поскольку Python 3.3 не сохранил старые обозначения кодека) Что произойдет, если мы вернем метод .

encode(), например, для байтовых строк? Это легко проверить даже без хаков интерпретатора Python. Напишем функцию с похожим поведением:

import codecs def encode(s, name, *args, **kwargs): codec = codecs.lookup(name) rv, length = codec.encode(s, *args, **kwargs) if not isinstance(rv, (str, bytes, bytearray)): raise TypeError('Not a string or byte codec') return rv

Теперь мы можем использовать эту функцию в качестве замены метода .

encode() для байтовых строк:

>>> b'Hello World'.

encode('latin1') Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'bytes' object has no attribute 'encode' >>> encode(b'Hello World', 'latin1') Traceback (most recent call last): File "<stdin>", line 4, in encode TypeError: Can't convert 'bytes' object to str implicitly

Ага! Python 3 уже знает, как справиться с этой ситуацией.

Мы получаем приятное уведомление об ошибке.

Я думаю, что даже «Невозможно неявно преобразовать объект 'bytes' в str» намного лучше и понятнее, чем «Объект 'bytes' не имеет атрибута 'encode'».

Почему бы не вернуть эти методы преобразования кодировки (кодирование и декодирование) обратно? Я правда не знаю и больше об этом не думаю.

Мне уже много раз объясняли, что я ничего не понимаю и не понимаю новичков, или что «текстовая модель» изменилась и мои требования к ней бессмысленны.



Потеряны байтовые строки

Теперь, после регресса системы кодеков, изменились и строковые операции: они определены только для строк Unicode. На первый взгляд это кажется вполне разумным, но на самом деле это не так.

Раньше интерпретатор имел реализации для операций с байтовыми строками и строками Юникода.

Этот подход был совершенно очевиден для программистов; если объект нужно было представить в виде байтовой строки или строки Юникода, были определены два метода: __str__ и __unicode__. Да, конечно, использовалась принудительная смена кодировки, что смущало новичков, но у нас был выбор.

Почему это полезно? Потому что, например, если вы работаете с низкоуровневыми протоколами, вам часто приходится иметь дело с числами определенного формата внутри байтовой строки.

Встроенная система контроля версий, используемая разработчиками Python, не работает на Python 3, поскольку команда разработчиков Python потратила годы не хочет возвращать возможность форматирования для байтовых строк .

Все вышесказанное показывает, что модель обработки строк Python 3 не работает в реальном мире.

Например, Python 3 «обновил» некоторые API для работы только с Unicode, и поэтому они совершенно непригодны для использования в реальных рабочих ситуациях.

Например, вы больше не можете парсить байты с помощью стандартной библиотеки, а только URL-адреса.

Причиной этого является неявное предположение о том, что все URL-адреса представлены только в Unicode (при таком положении дел вы больше не сможете работать с сообщениями электронной почты в кодировке, отличной от Unicode, если только вы полностью не игнорируете существование двоичных вложений в кодировке Unicode).

письмо).

Раньше это было довольно легко исправить, но поскольку байтовые строки теперь потеряны для разработчиков, библиотека обработки URL теперь имеет две реализации.

Один для Юникода, а другой для байтовых объектов.

Две реализации одной и той же функции приводят к тому, что результат обработки данных может сильно отличаться:

>>> from urllib.parse import urlparse >>> urlparse(' http://www.google.com/ ') ParseResult(scheme='http', netloc=' www.google.com ', path='/', params='', query='', fragment='') >>> urlparse(b' http://www.google.com/ ') ParseResultBytes(scheme=b'http', netloc=b' www.google.com ', path=b'/', params=b'', query=b'', fragment=b'')

Выглядит достаточно похоже? Вовсе нет, потому что в результате мы имеем совершенно разные типы данных для результата операции.

Один из них представляет собой кортеж строк, второй больше похож на массив целых чисел.

я уже писал об этом ранее и такое состояние причиняет мне страдания.

Теперь написание кода Python доставляет мне серьезное неудобство или становится крайне неэффективным, поскольку теперь вам приходится выполнять множество преобразований кодировок данных.

Это очень затрудняет написание кода, реализующего все необходимые функции.

Идея о том, что все в Unicode, очень хороша в теории, но совершенно непригодна на практике.

Python 3 пронизан кучей костылей для обработки ситуаций, когда невозможно обработать Unicode, и у таких людей, как я, которые много работают с такими ситуациями, все это вызывает жуткое раздражение.



Наши костыли не работают

Поддержка Unicode в ветке 2.x несовершенна и далека от идеала.

Не хватает API, проблемы идут с разных сторон, но мы, программисты, заставили все это работать.

Многие из методов, которые мы использовали для этого, больше нельзя применять в Python 3, а некоторые API будут изменены, чтобы хорошо работать с Python 3. Моим любимым примером была обработка файловых потоков, которые могли быть как байтовыми, так и текстовыми, но не было надежного способа определить, какой тип потока мы рассматриваем.

Уловка, которую я помог популяризировать, заключается в чтении нулевых байтов из потока для определения его типа.

Теперь этот трюк не работает .

Например, передача объекта запроса urllib в функцию Flask, обрабатывающую JSON, не работает в Python 3, но работает в Python 2:

>>> from urllib.request import urlopen >>> r = urlopen(' https://pypi.python.org/pypi/Flask/json ') >>> from flask import json >>> json.load(r) Traceback (most recent call last): File "decoder.py", line 368, in raw_decode StopIteration

При обработке выданного исключения выдается еще одно:

Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: No JSON object could be decoded



И что?

Помимо проблем, которые я описал выше, у Python 3 с поддержкой Unicode есть масса других проблем.

Я начал отписываться от разработчиков Python в Твиттере, потому что мне надоело читать, насколько хорош Python 3, поскольку это противоречит моему опыту.

Да, в Python 3 есть много вкусностей, но то, что сделали с обработкой байтовых строк и Unicode, не входит в их число.

(Хуже всего то, что многие из действительно интересных функций Python 3 обычно работают так же хорошо и в Python 2. Например, выход из, нелокальный режим, поддержка SNI SSL и т. д.) В свете того факта что только 3% разработчиков Python активно используют Python 3 , а разработчики Python в Твиттере громко заявляют, что переход на Python 3 идет по плану, я разочарован, так как подробно описал свой опыт работы с Python 3 и то, как хочу избавиться от последнего.

Я не хочу делать это прямо сейчас, но мне бы хотелось, чтобы команда разработчиков Python 3 немного больше прислушивалась к сообществу.

Для 97% из нас Python 2 — это уютный маленький мирок, в котором мы работаем годами, и поэтому достаточно болезненна ситуация, когда к нам приходят и говорят: Python 3 — это замечательно и это не обсуждается.

Это просто неправда в свете многочисленных регрессий.

Вместе с теми людьми, которые начинают обсуждать Python 2.8 и Stackless Python 2.8, я не знаю, что такое провал, если это не он.

Теги: #python #python3 #Unicode #python

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.