Por que o Python imprime caracteres unicode quando a codificação padrão é ASCII?

139

No shell do Python 2.6:

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

Eu esperava ter alguma bobagem ou um erro após a declaração de impressão, pois o caractere "é" não faz parte do ASCII e não especifiquei uma codificação. Acho que não entendo o que significa ASCII como codificação padrão.

EDITAR

Mudei a edição para a seção Respostas e a aceitei como sugerido.

Michael Ekoka
fonte
6
Seria muito bom se você pudesse transformar essa edição em uma resposta e aceitá-la.
mercator
2
Imprimir '\xe9'em um terminal configurado para UTF-8 não será impresso é. Ele imprimirá um caractere de substituição (geralmente um ponto de interrogação), pois \xe9não é uma sequência UTF-8 válida (faltam dois bytes que deveriam ter seguido o byte inicial). Certamente não será interpretado como latino-1.
Martijn Pieters
2
@MartijnPieters Eu suspeito que você possa ter deslizado sobre a parte em que especifiquei que o terminal está configurado para decodificar na ISO-8859-1 (latin1) quando eu \xe9imprimir para imprimir é.
Michael Ekoka
2
Ah sim, eu senti falta dessa parte; o terminal possui uma configuração que difere do shell. Verifica.
Martijn Pieters
Eu percorri a resposta, mas, na verdade, eu tenho a string sem o prefixo u para python 2.7. por que esse ainda é tratado como unicode? (meu sys.getdefaultencoding () é ascii)
dtc

Respostas:

104

Graças aos fragmentos de várias respostas, acho que podemos explicar uma explicação.

Ao tentar imprimir uma cadeia de caracteres unicode, u '\ xe9', o Python tenta codificar implicitamente essa cadeia usando o esquema de codificação atualmente armazenado em sys.stdout.encoding. Na verdade, o Python pega essa configuração no ambiente em que foi iniciada. Se não conseguir encontrar uma codificação adequada do ambiente, só então reverterá para o padrão ASCII.

Por exemplo, eu uso um shell bash que codifica como padrão UTF-8. Se eu iniciar o Python, ele pega e usa essa configuração:

$ python

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

Vamos sair por um momento do shell Python e definir o ambiente do bash com alguma codificação falsa:

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

Em seguida, inicie o shell python novamente e verifique se ele realmente reverte para a codificação ASCII padrão.

$ python

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

Bingo!

Se você agora tentar gerar algum caractere unicode fora do ascii, deverá receber uma boa mensagem de erro

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

Vamos sair do Python e descartar o shell bash.

Vamos agora observar o que acontece depois que o Python produz strings. Para isso, primeiro iniciaremos um shell bash dentro de um terminal gráfico (eu uso o Gnome Terminal) e definiremos o terminal para decodificar a saída com a ISO-8859-1, também conhecida como latin-1 (os terminais gráficos geralmente têm uma opção para definir caracteres Codificação em um de seus menus suspensos). Observe que isso não altera a codificação do ambiente de shell real , apenas altera a maneira como o próprio terminal decodifica a saída fornecida, um pouco como um navegador da Web. Portanto, você pode alterar a codificação do terminal, independentemente do ambiente do shell. Vamos então iniciar o Python a partir do shell e verificar se sys.stdout.encoding está definido como a codificação do ambiente do shell (UTF-8 para mim):

$ 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 gera uma string binária como está, o terminal a recebe e tenta combinar seu valor com o mapa de caracteres latin-1. Em latin-1, 0xe9 ou 233 produz o caractere "é" e é isso que o terminal exibe.

(2) o python tenta codificar implicitamente a cadeia Unicode com qualquer esquema atualmente definido em sys.stdout.encoding; nesse caso, é "UTF-8". Após a codificação UTF-8, a sequência binária resultante é '\ xc3 \ xa9' (consulte a explicação posterior). O terminal recebe o fluxo como tal e tenta decodificar 0xc3a9 usando latin-1, mas latin-1 vai de 0 a 255 e, portanto, apenas decodifica os fluxos de 1 byte por vez. 0xc3a9 tem 2 bytes, o decodificador latin-1, portanto, o interpreta como 0xc3 (195) e 0xa9 (169) e que gera 2 caracteres: Ã e ©.

(3) python codifica o ponto de código unicode u '\ xe9' (233) com o esquema latin-1. Acontece que o intervalo de pontos de código latin-1 é de 0 a 255 e aponta exatamente para o mesmo caractere que Unicode dentro desse intervalo. Portanto, os pontos de código Unicode nesse intervalo produzirão o mesmo valor quando codificados em latin-1. Portanto, u '\ xe9' (233) codificado em latin-1 também produzirá a cadeia binária '\ xe9'. O terminal recebe esse valor e tenta correspondê-lo no mapa de caracteres latin-1. Assim como no caso (1), produz "é" e é isso que é exibido.

Vamos agora alterar as configurações de codificação do terminal para UTF-8 no menu suspenso (como você alteraria as configurações de codificação do navegador da web). Não há necessidade de parar o Python ou reiniciar o shell. A codificação do terminal agora corresponde à do Python. Vamos tentar imprimir novamente:

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

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

>>>

(4) python gera uma string binária como está. O terminal tenta decodificar esse fluxo com UTF-8. Mas o UTF-8 não entende o valor 0xe9 (veja a explicação posterior) e, portanto, não pode convertê-lo em um ponto de código unicode. Nenhum ponto de código encontrado, nenhum caractere impresso.

(5) python tenta codificar implicitamente a cadeia Unicode com o que estiver em sys.stdout.encoding. Ainda "UTF-8". A sequência binária resultante é '\ xc3 \ xa9'. O terminal recebe o fluxo e tenta decodificar 0xc3a9 também usando UTF-8. Ele retorna o valor do código 0xe9 (233), que no mapa de caracteres Unicode aponta para o símbolo "é". O terminal exibe "é".

(6) python codifica string unicode com latin-1, produz uma string binária com o mesmo valor '\ xe9'. Novamente, para o terminal, isso é praticamente o mesmo do caso (4).

Conclusões: - Python gera strings não unicode como dados brutos, sem considerar sua codificação padrão. O terminal apenas os exibe se sua codificação atual corresponder aos dados. - Python gera seqüências de caracteres Unicode após codificá-las usando o esquema especificado em sys.stdout.encoding. - Python obtém essa configuração do ambiente do shell. - o terminal exibe a saída de acordo com suas próprias configurações de codificação. - a codificação do terminal é independente da do shell.


Mais detalhes sobre unicode, UTF-8 e latin-1:

Unicode é basicamente uma tabela de caracteres em que algumas chaves (pontos de código) foram convencionalmente atribuídas para apontar para alguns símbolos. Por exemplo, por convenção, foi decidido que a chave 0xe9 (233) é o valor que aponta para o símbolo 'é'. ASCII e Unicode usam os mesmos pontos de código de 0 a 127, assim como latin-1 e Unicode de 0 a 255. Ou seja, 0x41 aponta para 'A' em ASCII, latin-1 e Unicode, 0xc8 aponta para 'Ü' em latin-1 e Unicode, 0xe9 aponta para 'é' em latin-1 e Unicode.

Ao trabalhar com dispositivos eletrônicos, os pontos de código Unicode precisam de uma maneira eficiente de serem representados eletronicamente. É disso que tratam os esquemas de codificação. Existem vários esquemas de codificação Unicode (utf7, UTF-8, UTF-16, UTF-32). A abordagem de codificação mais intuitiva e direta seria simplesmente usar o valor de um ponto de código no mapa Unicode como valor para sua forma eletrônica, mas atualmente o Unicode possui mais de um milhão de pontos de código, o que significa que alguns deles exigem que sejam necessários 3 bytes. expressa. Para trabalhar eficientemente com texto, um mapeamento 1 para 1 seria bastante impraticável, pois exigiria que todos os pontos de código fossem armazenados exatamente na mesma quantidade de espaço, com um mínimo de 3 bytes por caractere, independentemente da necessidade real.

A maioria dos esquemas de codificação tem deficiências em relação ao requisito de espaço, os mais econômicos não cobrem todos os pontos de código unicode, por exemplo, o ascii cobre apenas os primeiros 128, enquanto o latin-1 cobre os primeiros 256. Outros que tentam ser mais abrangentes também acabam ser um desperdício, uma vez que exigem mais bytes do que o necessário, mesmo para caracteres "baratos" comuns. O UTF-16, por exemplo, usa no mínimo 2 bytes por caractere, incluindo aqueles no intervalo ascii ('B', que é 65, ainda requer 2 bytes de armazenamento no UTF-16). UTF-32 é ainda mais inútil, pois armazena todos os caracteres em 4 bytes.

O UTF-8 resolveu o dilema de maneira inteligente, com um esquema capaz de armazenar pontos de código com uma quantidade variável de espaços de bytes. Como parte de sua estratégia de codificação, o UTF-8 amarra pontos de código com bits de flag que indicam (presumivelmente para decodificadores) seus requisitos de espaço e seus limites.

Codificação UTF-8 de pontos de código unicode no intervalo ascii (0-127):

0xxx xxxx  (in binary)
  • os x mostram o espaço real reservado para "armazenar" o ponto de código durante a codificação
  • O 0 inicial é um sinalizador que indica ao decodificador UTF-8 que esse ponto de código exigirá apenas 1 byte.
  • na codificação, o UTF-8 não altera o valor dos pontos de código nesse intervalo específico (ou seja, 65 codificados no UTF-8 também são 65). Considerando que Unicode e ASCII também são compatíveis no mesmo intervalo, faz com que o UTF-8 e o ASCII também sejam compatíveis nesse intervalo.

por exemplo, o ponto de código Unicode para 'B' é '0x42' ou 0100 0010 em binário (como dissemos, é o mesmo em ASCII). Após a codificação em UTF-8, torna-se:

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)

Codificação UTF-8 de pontos de código Unicode acima de 127 (não ascii):

110x xxxx 10xx xxxx            <-- (from 128 to 2047)
1110 xxxx 10xx xxxx 10xx xxxx  <-- (from 2048 to 65535)
  • os bits iniciais '110' indicam para o decodificador UTF-8 o início de um ponto de código codificado em 2 bytes, enquanto '1110' indica 3 bytes, 11110 indica 4 bytes e assim por diante.
  • os bits de flag '10' internos são usados ​​para sinalizar o início de um byte interno.
  • novamente, os x marcam o espaço em que o valor do ponto de código Unicode é armazenado após a codificação.

por exemplo, o ponto de código Unicode 'é' é 0xe9 (233).

1110 1001    <-- 0xe9

Quando UTF-8 codifica esse valor, ele determina que o valor é maior que 127 e menor que 2048; portanto, deve ser codificado em 2 bytes:

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

O código 0xe9 Unicode aponta após a codificação UTF-8 se tornar 0xc3a9. Qual é exatamente como o terminal o recebe. Se seu terminal estiver configurado para decodificar strings usando latin-1 (uma das codificações herdadas não unicode), você verá à ©, porque acontece que 0xc3 em latin-1 aponta para à e 0xa9 para ©.

Michael Ekoka
fonte
6
Excelente explicação. Agora eu entendo UTF-8!
Doutor Coder
2
Ok, li toda a sua postagem em cerca de 10 segundos. Dizia: "Python é péssimo quando se trata de codificação".
Andrew
Ótima explicação. Você poderia resolver esta questão?
Maggyero 26/02
26

Quando caracteres Unicode são impressos em stdout, sys.stdout.encodingé usado. Presume-se que um caractere não Unicode esteja dentro sys.stdout.encodinge é enviado apenas ao terminal. No meu sistema (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() é usado apenas quando o Python não tem outra opção.

Observe que o Python 3.6 ou posterior ignora as codificações no Windows e usa APIs Unicode para gravar Unicode no terminal. Nenhum aviso UnicodeEncodeError e o caractere correto são exibidos se a fonte suportar. Mesmo que a fonte não a suporte, os caracteres ainda podem ser recortados e colados do terminal para um aplicativo com uma fonte de suporte e ela estará correta. Melhoria!

Mark Tolonen
fonte
8

O Python REPL tenta captar qual codificação usar em seu ambiente. Se encontrar algo sadio, tudo funcionará. É quando não consegue descobrir o que está acontecendo que o problema ocorre.

>>> print sys.stdout.encoding
UTF-8
Ignacio Vazquez-Abrams
fonte
3
só por curiosidade, como eu mudaria sys.stdout.encoding para ascii?
Michael Ekoka
2
@TankorSmash eu estou começando TypeError: readonly attributeno 2.7.2
Kos
4

Você ter especificado uma codificação inserindo uma seqüência de caracteres Unicode explícito. Compare os resultados de não usar o uprefixo.

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

>>> 

No caso de \xe9então, o Python assume sua codificação padrão (Ascii), imprimindo ... algo em branco.

Mark Rushakoff
fonte
1
então, se eu entendi bem, quando imprimo strings unicode (os pontos de código), o python assume que eu quero uma saída codificada em utf-8, em vez de apenas tentar me dar o que poderia ter sido em ascii?
Michael Ekoka
1
@ Mike: AFAIK o que você disse está correto. Se ele fez imprimir os caracteres Unicode, mas codificado como ASCII, tudo sairia truncado e, provavelmente, todos os novatos seria perguntar: "Como é que eu não posso imprimir texto Unicode?"
Mark Rushakoff
2
Obrigado. Na verdade, sou um desses iniciantes, mas sou do lado de pessoas que entendem o unicode, e é por isso que esse comportamento está me deixando um pouco irritado.
#
3
R., incorreto, pois '\ xe9' não está no conjunto de caracteres ascii. As seqüências não Unicode são impressas usando sys.stdout.encoding, as seqüências Unicode são codificadas para sys.stdout.encoding antes da impressão.
Mark10
0

Funciona para mim:

import sys
stdin, stdout = sys.stdin, sys.stdout
reload(sys)
sys.stdin, sys.stdout = stdin, stdout
sys.setdefaultencoding('utf-8')
user3611630
fonte
1
Hack sujo e barato que inevitavelmente quebrará outra coisa. Não é difícil fazê-lo da maneira certa!
31716 Chris Johnson
0

De acordo com as codificações e conversões de sequência padrão / implícitas do Python :

  • Quando printing unicode, é encoded com <file>.encoding.
    • quando encodingnão está definido, unicodeé implicitamente convertido em str(já que o codec para isso é sys.getdefaultencoding(), ou seja ascii, qualquer caractere nacional causaria a UnicodeEncodeError)
    • para fluxos padrão, isso encodingé inferido do ambiente. É normalmente definido para ttyfluxos (a partir das configurações de localidade do terminal), mas provavelmente não será definido para tubos
      • portanto, a print u'\xe9'provavelmente terá êxito quando a saída for para um terminal e falhará se for redirecionado. Uma solução é encode()a string com a codificação desejada antes de printing.
  • Quando printing str, os bytes são enviados para o fluxo como estão. Os glifos mostrados pelo terminal dependerão de suas configurações de localidade.
ivan_pozdeev
fonte