Почему Python печатает символы юникода, если кодировка по умолчанию - ASCII?

140

Из оболочки Python 2.6:

>>> import sys
>>> print sys.getdefaultencoding()
ascii
>>> print u'\xe9'
é
>>> 

Я ожидал, что после оператора печати будет какая-то тарабарщина или ошибка, поскольку символ «é» не является частью ASCII, и я не указал кодировку. Думаю, я не понимаю, что означает кодировка ASCII по умолчанию.

РЕДАКТИРОВАТЬ

Я переместил правку в раздел " Ответы" и принял его, как было предложено.

Майкл Экока
источник
6
Было бы неплохо, если бы вы могли вместо этого превратить это изменение в ответ и принять его.
mercator
2
Печать '\xe9'в терминале, настроенном для UTF-8, не будет печататься é. Он напечатает заменяющий символ (обычно вопросительный знак), поскольку \xe9он не является допустимой последовательностью UTF-8 (отсутствуют два байта, которые должны были следовать за этим ведущим байтом). Это, безусловно, не будет интерпретироваться как Latin-1.
Мартейн Питерс
2
@MartijnPieters Я подозреваю, что вы могли пролистать часть, где я указал, что терминал настроен на декодирование в ISO-8859-1 (latin1) при выводе \xe9на печать é.
Майкл Экока
2
Ах да, я пропустил эту часть; терминал имеет конфигурацию, отличную от оболочки. Проверьте.
Martijn Pieters
Я бегло просмотрел ответ, но на самом деле у меня есть строка без префикса u для python 2.7. почему этот до сих пор обрабатывается как юникод? (мой sys.getdefaultencoding () - ascii)
dtc

Ответы:

106

Я думаю, что благодаря фрагментам из различных ответов мы сможем составить объяснение.

Пытаясь напечатать строку юникода u '\ xe9', Python неявно пытается закодировать эту строку, используя схему кодирования, которая в настоящее время хранится в sys.stdout.encoding. Python фактически берет этот параметр из среды, из которой он был инициирован. Если он не может найти правильную кодировку в среде, только тогда он возвращается к своему умолчанию , ASCII.

Например, я использую оболочку bash, в которой по умолчанию используется кодировка UTF-8. Если я запускаю Python с него, он подбирает и использует этот параметр:

$ python

>>> import sys
>>> print sys.stdout.encoding
UTF-8

Давайте на мгновение выйдем из оболочки Python и установим среду bash с какой-то фиктивной кодировкой:

$ export LC_CTYPE=klingon
# we should get some error message here, just ignore it.

Затем снова запустите оболочку python и убедитесь, что она действительно возвращается к кодировке ascii по умолчанию.

$ python

>>> import sys
>>> print sys.stdout.encoding
ANSI_X3.4-1968

Бинго!

Если теперь вы попытаетесь вывести какой-либо символ Unicode вне ascii, вы должны получить приятное сообщение об ошибке

>>> print u'\xe9'
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9' 
in position 0: ordinal not in range(128)

Выйдем из Python и откажемся от оболочки bash.

Теперь посмотрим, что происходит после вывода строк Python. Для этого мы сначала запустим оболочку bash в графическом терминале (я использую Gnome Terminal), и мы настроим терминал на декодирование вывода с помощью ISO-8859-1, также известного как latin-1 (графические терминалы обычно имеют опцию Set Character Кодировка в одном из раскрывающихся меню). Обратите внимание, что это не меняет фактическую кодировку среды оболочки , а только изменяет способ, которым сам терминал будет декодировать выданный им вывод, как это делает веб-браузер. Таким образом, вы можете изменить кодировку терминала независимо от среды оболочки. Затем запустим Python из оболочки и убедимся, что для sys.stdout.encoding задана кодировка среды оболочки (для меня UTF-8):

$ python

>>> import sys

>>> print sys.stdout.encoding
UTF-8

>>> print '\xe9' # (1)
é
>>> print u'\xe9' # (2)
é
>>> print u'\xe9'.encode('latin-1') # (3)
é
>>>

(1) python выводит двоичную строку как есть, терминал получает ее и пытается сопоставить ее значение с картой символов latin-1. В latin-1, 0xe9 или 233 дает символ «é», и это то, что отображает терминал.

(2) python пытается неявно закодировать строку Unicode любой схемой, установленной в настоящее время в sys.stdout.encoding, в данном случае это «UTF-8». После кодировки UTF-8 результирующая двоичная строка будет '\ xc3 \ xa9' (см. Объяснение ниже). Терминал принимает поток как таковой и пытается декодировать 0xc3a9, используя latin-1, но latin-1 принимает значения от 0 до 255, поэтому за раз декодирует потоки только по 1 байту. 0xc3a9 имеет длину 2 байта, поэтому декодер latin-1 интерпретирует его как 0xc3 (195) и 0xa9 (169), что дает 2 символа: Ã и ©.

(3) python кодирует кодовую точку unicode u '\ xe9' (233) по схеме latin-1. Оказывается, диапазон кодовых точек latin-1 составляет от 0 до 255 и указывает на тот же символ, что и Unicode в этом диапазоне. Следовательно, кодовые точки Unicode в этом диапазоне будут давать то же значение при кодировании в latin-1. Таким образом, u '\ xe9' (233), закодированный в latin-1, также даст двоичную строку '\ xe9'. Терминал получает это значение и пытается сопоставить его на карте символов latin-1. Как и в случае (1), он дает «é», и это то, что отображается.

Теперь давайте изменим настройки кодировки терминала на UTF-8 из раскрывающегося меню (как если бы вы изменили настройки кодировки своего веб-браузера). Нет необходимости останавливать Python или перезапускать оболочку. Кодировка терминала теперь соответствует кодировке Python. Попробуем снова распечатать:

>>> print '\xe9' # (4)

>>> print u'\xe9' # (5)
é
>>> print u'\xe9'.encode('latin-1') # (6)

>>>

(4) python выводит двоичную строку как есть. Терминал пытается декодировать этот поток с помощью UTF-8. Но UTF-8 не понимает значение 0xe9 (см. Объяснение ниже) и поэтому не может преобразовать его в кодовую точку Unicode. Кодовая точка не найдена, символ не напечатан.

(5) python пытается неявно закодировать строку Unicode, используя все, что указано в sys.stdout.encoding. Еще "UTF-8". В результате получается двоичная строка '\ xc3 \ xa9'. Терминал принимает поток и пытается декодировать 0xc3a9, также используя UTF-8. Он возвращает значение кода 0xe9 (233), которое на карте символов Unicode указывает на символ «é». Терминал отображает «é».

(6) python кодирует строку Unicode с помощью latin-1, это дает двоичную строку с тем же значением '\ xe9'. Опять же, для терминала это почти то же самое, что и в случае (4).

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


Подробнее о Unicode, UTF-8 и latin-1:

Юникод - это в основном таблица символов, в которой некоторые клавиши (кодовые точки) были условно назначены для указания на некоторые символы. например, по соглашению было решено, что ключ 0xe9 (233) - это значение, указывающее на символ «é». ASCII и Unicode используют те же кодовые точки от 0 до 127, что и latin-1 и Unicode от 0 до 255. То есть 0x41 указывает на 'A' в ASCII, latin-1 и Unicode, 0xc8 указывает на 'Ü' в latin-1 и Unicode, 0xe9 указывает на 'é' в latin-1 и Unicode.

При работе с электронными устройствами кодовые точки Unicode нуждаются в эффективном способе электронного представления. Вот в чем суть схем кодирования. Существуют различные схемы кодирования Unicode (utf7, UTF-8, UTF-16, UTF-32). Самый интуитивно понятный и простой подход к кодированию - это просто использовать значение кодовой точки в карте Unicode в качестве значения для ее электронной формы, но Unicode в настоящее время имеет более миллиона кодовых точек, что означает, что для некоторых из них требуется 3 байта для выражено. Для эффективной работы с текстом сопоставление 1 к 1 было бы довольно непрактичным, поскольку для этого потребовалось бы, чтобы все кодовые точки хранились в точно таком же объеме пространства, как минимум 3 байта на символ, независимо от их фактической потребности.

Большинство схем кодирования имеют недостатки, касающиеся требований к пространству, самые экономичные из них не охватывают все кодовые точки Unicode, например, ascii покрывает только первые 128, а latin-1 покрывает первые 256. Другие, которые пытаются быть более полными, также оказываются являются расточительными, поскольку требуют больше байтов, чем необходимо, даже для обычных «дешевых» символов. Например, UTF-16 использует минимум 2 байта на символ, включая символы из диапазона ascii ('B', который равен 65, по-прежнему требует 2 байта памяти в UTF-16). UTF-32 еще более расточителен, поскольку он хранит все символы в 4 байтах.

UTF-8 умело разрешил дилемму, предложив схему, способную хранить кодовые точки с переменным количеством байтовых пространств. В рамках своей стратегии кодирования UTF-8 соединяет кодовые точки с битами флагов, которые указывают (предположительно для декодеров) их требования к пространству и их границы.

Кодировка UTF-8 кодовых точек Unicode в диапазоне ascii (0-127):

0xxx xxxx  (in binary)
  • x показывают фактическое пространство, зарезервированное для "хранения" кодовой точки во время кодирования.
  • Ведущий 0 - это флаг, который указывает декодеру UTF-8, что для этой кодовой точки потребуется только 1 байт.
  • при кодировании UTF-8 не изменяет значение кодовых точек в этом конкретном диапазоне (т.е. 65, закодированные в UTF-8, также 65). Учитывая, что Unicode и ASCII также совместимы в одном и том же диапазоне, это, кстати, делает UTF-8 и ASCII также совместимыми в этом диапазоне.

например, кодовая точка Unicode для «B» - «0x42» или 0100 0010 в двоичном формате (как мы уже сказали, это то же самое в ASCII). После кодирования в UTF-8 он становится:

0xxx xxxx  <-- UTF-8 encoding for Unicode code points 0 to 127
*100 0010  <-- Unicode code point 0x42
0100 0010  <-- UTF-8 encoded (exactly the same)

Кодировка UTF-8 кодовых точек Unicode выше 127 (не-ascii):

110x xxxx 10xx xxxx            <-- (from 128 to 2047)
1110 xxxx 10xx xxxx 10xx xxxx  <-- (from 2048 to 65535)
  • ведущие биты «110» указывают декодеру UTF-8 начало кодовой точки, закодированной в 2 байта, тогда как «1110» указывает 3 байта, 11110 означает 4 байта и так далее.
  • внутренние биты флага «10» используются для обозначения начала внутреннего байта.
  • опять же, x обозначают пробел, в котором значение кодовой точки Unicode сохраняется после кодирования.

например, «é» кодовая точка Unicode - 0xe9 (233).

1110 1001    <-- 0xe9

Когда UTF-8 кодирует это значение, он определяет, что значение больше 127 и меньше 2048, поэтому должно быть закодировано в 2 байта:

110x xxxx 10xx xxxx   <-- UTF-8 encoding for Unicode 128-2047
***0 0011 **10 1001   <-- 0xe9
1100 0011 1010 1001   <-- 'é' after UTF-8 encoding
C    3    A    9

Кодовые точки Unicode 0xe9 после кодировки UTF-8 становятся 0xc3a9. Именно так его и получает терминал. Если ваш терминал настроен на декодирование строк с использованием latin-1 (одна из устаревших кодировок, отличных от Unicode), вы увидите Ã ©, потому что так уж случилось, что 0xc3 в latin-1 указывает на Ã, а 0xa9 - на ©.

Майкл Экока
источник
6
Отличное объяснение. Теперь я понимаю UTF-8!
Doctor Coder
2
Хорошо, я прочитал весь ваш пост примерно за 10 секунд. Он сказал: «Python - отстой, когда дело касается кодирования».
Эндрю
Отличное объяснение. Не могли бы вы ответить на этот вопрос?
Maggyero
27

Когда символы Unicode выводятся на стандартный вывод, sys.stdout.encodingиспользуется. Предполагается, что символ не-Unicode находится внутри sys.stdout.encodingи просто отправляется на терминал. В моей системе (Python 2):

>>> import unicodedata as ud
>>> import sys
>>> sys.stdout.encoding
'cp437'
>>> ud.name(u'\xe9') # U+00E9 Unicode codepoint
'LATIN SMALL LETTER E WITH ACUTE'
>>> ud.name('\xe9'.decode('cp437')) 
'GREEK CAPITAL LETTER THETA'
>>> '\xe9'.decode('cp437') # byte E9 decoded using code page 437 is U+0398.
u'\u0398'
>>> ud.name(u'\u0398')
'GREEK CAPITAL LETTER THETA'
>>> print u'\xe9' # Unicode is encoded to CP437 correctly
é
>>> print '\xe9'  # Byte is just sent to terminal and assumed to be CP437.
Θ

sys.getdefaultencoding() используется только тогда, когда у Python нет другого варианта.

Обратите внимание, что Python 3.6 или более поздней версии игнорирует кодировки в Windows и использует API-интерфейсы Unicode для записи Unicode в терминал. Предупреждения об ошибке UnicodeEncodeError отсутствуют, и отображается правильный символ, если шрифт его поддерживает. Даже если шрифт не поддерживает его, символы все равно можно вырезать и вставить из терминала в приложение с поддерживающим шрифтом, и это будет правильно. Обновить!

Марк Толонен
источник
8

Python REPL пытается выбрать, какую кодировку использовать из вашей среды. Если он находит что-то разумное, тогда все просто работает. Когда он не может понять, что происходит, он выходит из строя.

>>> print sys.stdout.encoding
UTF-8
Игнасио Васкес-Абрамс
источник
3
просто из любопытства, как мне изменить sys.stdout.encoding на ascii?
Майкл Экока
2
@TankorSmash У меня TypeError: readonly attribute2.7.2
Кос
4

Вы уже указали кодировку, введя явно строку Unicode. Сравните результаты отказа от uпрефикса.

>>> import sys
>>> sys.getdefaultencoding()
'ascii'
>>> '\xe9'
'\xe9'
>>> u'\xe9'
u'\xe9'
>>> print u'\xe9'
é
>>> print '\xe9'

>>> 

В этом случае \xe9Python принимает кодировку по умолчанию (Ascii), таким образом печатая ... что-то пустое.

Марк Рушаков
источник
1
поэтому, если я хорошо понимаю, когда я распечатываю строки Unicode (кодовые точки), python предполагает, что мне нужен вывод, закодированный в utf-8, вместо того, чтобы просто пытаться дать мне то, что могло быть в ascii?
Майкл Экока
1
@mike: AFAIK то, что вы сказали, правильно. Если он сделал распечатать символы Unicode , но закодирован как ASCII, все выйдет искажен и , вероятно , все новички будут спрашивать, «Почему я не могу напечатать текст Unicode?»
Марк Рушаков 08
2
Спасибо. Я на самом деле один из тех новичков, но со стороны людей, которые хоть немного разбираются в юникоде, поэтому такое поведение меня немного сбивает.
Майкл Экока
3
R., неверно, поскольку '\ xe9' не входит в набор символов ascii. Строки в формате, отличном от Unicode, печатаются с использованием sys.stdout.encoding, строки Unicode перед печатью кодируются в sys.stdout.encoding.
Марк Толонен
0

Меня устраивает:

import sys
stdin, stdout = sys.stdin, sys.stdout
reload(sys)
sys.stdin, sys.stdout = stdin, stdout
sys.setdefaultencoding('utf-8')
user3611630
источник
1
Дешевый грязный хакер, который неминуемо сломает что-нибудь еще. Сделать это правильно - несложно!
Крис Джонсон
0

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

  • Когда printing unicode, это encoded с <file>.encoding.
    • когда encodingне установлен, unicodeнеявно преобразуется в str(поскольку кодек для этого есть sys.getdefaultencoding(), т. е. asciiлюбые национальные символы вызовут a UnicodeEncodeError)
    • для стандартных потоков encodingвыводится из среды. Обычно он устанавливает ttyпотоки fot (из настроек локали терминала), но, скорее всего, не будет установлен для каналов
      • поэтому a print u'\xe9', скорее всего, будет успешным, когда вывод направлен на терминал, и неудачным, если он перенаправлен. Решение - encode()строка с желаемой кодировкой перед printing.
  • При printвходе strбайты отправляются в поток как есть. Какие глифы показывает терминал, будет зависеть от настроек локали.
ivan_pozdeev
источник