¿Por qué Python imprime caracteres Unicode cuando la codificación predeterminada es ASCII?

139

Desde el shell Python 2.6:

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

Esperaba tener algo de galimatías o un error después de la declaración de impresión, ya que el carácter "é" no es parte de ASCII y no he especificado una codificación. Supongo que no entiendo qué significa ASCII ser la codificación predeterminada.

EDITAR

Moví la edición a la sección Respuestas y la acepté como se sugiere.

Michael Ekoka
fuente
66
Sería bastante bueno si pudieras convertir esa edición en una respuesta y aceptarla.
mercator
2
La impresión '\xe9'en un terminal configurado para UTF-8 no se imprimirá é. Imprimirá un carácter de reemplazo (generalmente un signo de interrogación) ya \xe9que no es una secuencia UTF-8 válida (le faltan dos bytes que deberían haber seguido ese byte inicial). Sin duda, no se interpretará como Latin-1 en su lugar.
Martijn Pieters
2
@MartijnPieters Sospecho que podría haber pasado por alto la parte donde especifiqué que el terminal está configurado para decodificar en ISO-8859-1 (latin1) cuando salgo \xe9a imprimir é.
Michael Ekoka
2
Ah sí, me perdí esa parte; El terminal tiene una configuración que difiere del shell. Cheque.
Martijn Pieters
hojeé la respuesta pero en realidad, tengo la cadena sin el prefijo u para python 2.7. ¿por qué ese todavía se maneja como unicode? (my sys.getdefaultencoding () es ascii)
dtc

Respuestas:

104

Gracias a fragmentos de varias respuestas, creo que podemos unir una explicación.

Al intentar imprimir una cadena Unicode, u '\ xe9', Python intenta implícitamente codificar esa cadena usando el esquema de codificación actualmente almacenado en sys.stdout.encoding. Python en realidad recoge esta configuración del entorno desde el que se inició. Si no puede encontrar una codificación adecuada del entorno, solo entonces vuelve a su valor predeterminado , ASCII.

Por ejemplo, yo uso un shell bash cuya codificación por defecto es UTF-8. Si inicio Python desde allí, recoge y usa esa configuración:

$ python

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

Salgamos por un momento del shell de Python y configuremos el entorno de bash con alguna codificación falsa:

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

Luego, vuelva a iniciar el shell de Python y verifique que efectivamente vuelva a su codificación ASCII predeterminada.

$ python

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

¡Bingo!

Si ahora intenta generar algún carácter unicode fuera de ASCII, debería recibir un buen mensaje de error

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

Salgamos de Python y descartemos el bash shell.

Ahora observaremos lo que sucede después de que Python genera cadenas. Para esto, primero comenzaremos un shell bash dentro de un terminal gráfico (uso el terminal Gnome) y configuraremos el terminal para decodificar la salida con ISO-8859-1 aka latin-1 (los terminales gráficos generalmente tienen una opción para establecer el carácter Codificación en uno de sus menús desplegables). Tenga en cuenta que esto no cambia la codificación del entorno de shell real , solo cambia la forma en que el terminal descodificará la salida que se le da, un poco como lo hace un navegador web. Por lo tanto, puede cambiar la codificación del terminal, independientemente del entorno del shell. Entonces, comencemos Python desde el shell y verifiquemos que sys.stdout.encoding esté configurado para la codificación del entorno del shell (UTF-8 para mí):

$ 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 genera una cadena binaria tal cual, la terminal la recibe e intenta hacer coincidir su valor con el mapa de caracteres latin-1. En latin-1, 0xe9 o 233 produce el carácter "é" y eso es lo que muestra el terminal.

(2) python intenta codificar implícitamente la cadena Unicode con cualquier esquema actualmente configurado en sys.stdout.encoding, en este caso es "UTF-8". Después de la codificación UTF-8, la cadena binaria resultante es '\ xc3 \ xa9' (ver explicación posterior). El terminal recibe el flujo como tal e intenta decodificar 0xc3a9 usando latin-1, pero latin-1 va de 0 a 255 y, por lo tanto, solo decodifica flujos de 1 byte a la vez. 0xc3a9 tiene 2 bytes de longitud, por lo tanto, el decodificador latin-1 lo interpreta como 0xc3 (195) y 0xa9 (169) y eso produce 2 caracteres: Ã y ©.

(3) python codifica el punto de código unicode u '\ xe9' (233) con el esquema latin-1. Resulta que el rango de puntos del código latin-1 es 0-255 y apunta exactamente al mismo carácter que Unicode dentro de ese rango. Por lo tanto, los puntos de código Unicode en ese rango producirán el mismo valor cuando se codifican en latin-1. Entonces u '\ xe9' (233) codificado en latin-1 también producirá la cadena binaria '\ xe9'. Terminal recibe ese valor e intenta hacer coincidirlo en el mapa de caracteres latin-1. Al igual que el caso (1), produce "é" y eso es lo que se muestra.

Cambiemos ahora la configuración de codificación del terminal a UTF-8 desde el menú desplegable (como cambiaría la configuración de codificación de su navegador web). No es necesario detener Python o reiniciar el shell. La codificación del terminal ahora coincide con la de Python. Intentemos imprimir de nuevo:

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

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

>>>

(4) python genera una cadena binaria tal cual. El terminal intenta decodificar ese flujo con UTF-8. Pero UTF-8 no entiende el valor 0xe9 (ver explicación posterior) y, por lo tanto, no puede convertirlo en un punto de código Unicode. No se encontró ningún punto de código, no se imprimió ningún carácter.

(5) python intenta codificar implícitamente la cadena Unicode con lo que esté en sys.stdout.encoding. Todavía "UTF-8". La cadena binaria resultante es '\ xc3 \ xa9'. El terminal recibe el flujo e intenta decodificar 0xc3a9 también usando UTF-8. Devuelve el valor del código 0xe9 (233), que en el mapa de caracteres Unicode apunta al símbolo "é". El terminal muestra "é".

(6) python codifica una cadena unicode con latin-1, produce una cadena binaria con el mismo valor '\ xe9'. Nuevamente, para el terminal esto es más o menos lo mismo que el caso (4).

Conclusiones: - Python genera cadenas no unicode como datos sin procesar, sin considerar su codificación predeterminada. El terminal simplemente los muestra si su codificación actual coincide con los datos. - Python genera cadenas Unicode después de codificarlas utilizando el esquema especificado en sys.stdout.encoding. - Python obtiene esa configuración del entorno del shell. - el terminal muestra la salida de acuerdo con su propia configuración de codificación. - la codificación del terminal es independiente de la del shell.


Más detalles sobre unicode, UTF-8 y latin-1:

Unicode es básicamente una tabla de caracteres donde algunas teclas (puntos de código) se han asignado convencionalmente para señalar algunos símbolos. por ejemplo, por convención, se ha decidido que la clave 0xe9 (233) es el valor que apunta al símbolo 'é'. ASCII y Unicode usan los mismos puntos de código de 0 a 127, al igual que latin-1 y Unicode de 0 a 255. Es decir, 0x41 puntos a 'A' en ASCII, latin-1 y Unicode, 0xc8 puntos a 'Ü' en latin-1 y Unicode, 0xe9 apunta a 'é' en latin-1 y Unicode.

Cuando se trabaja con dispositivos electrónicos, los puntos de código Unicode necesitan una forma eficiente de representarse electrónicamente. De eso se tratan los esquemas de codificación. Existen varios esquemas de codificación Unicode (utf7, UTF-8, UTF-16, UTF-32). El enfoque de codificación más intuitivo y directo sería simplemente usar el valor de un punto de código en el mapa Unicode como su valor para su forma electrónica, pero Unicode actualmente tiene más de un millón de puntos de código, lo que significa que algunos de ellos requieren 3 bytes para ser expresado Para trabajar de manera eficiente con el texto, una asignación 1 a 1 sería poco práctica, ya que requeriría que todos los puntos de código se almacenen exactamente en la misma cantidad de espacio, con un mínimo de 3 bytes por carácter, independientemente de su necesidad real.

La mayoría de los esquemas de codificación tienen deficiencias con respecto al requisito de espacio, los más económicos no cubren todos los puntos de código Unicode, por ejemplo, ascii solo cubre los primeros 128, mientras que latin-1 cubre los primeros 256. Otros que intentan ser más completos también terminan siendo un desperdicio, ya que requieren más bytes de los necesarios, incluso para los caracteres "baratos" comunes. UTF-16, por ejemplo, utiliza un mínimo de 2 bytes por carácter, incluidos aquellos en el rango ASCII ('B', que es 65, todavía requiere 2 bytes de almacenamiento en UTF-16). UTF-32 es aún más derrochador ya que almacena todos los caracteres en 4 bytes.

UTF-8 resulta haber resuelto inteligentemente el dilema, con un esquema capaz de almacenar puntos de código con una cantidad variable de espacios de bytes. Como parte de su estrategia de codificación, UTF-8 ata puntos de código con bits de bandera que indican (presumiblemente a los decodificadores) sus requisitos de espacio y sus límites.

Codificación UTF-8 de puntos de código Unicode en el rango ASCII (0-127):

0xxx xxxx  (in binary)
  • las x muestran el espacio real reservado para "almacenar" el punto de código durante la codificación
  • El 0 inicial es un indicador que indica al decodificador UTF-8 que este punto de código solo requerirá 1 byte.
  • al codificar, UTF-8 no cambia el valor de los puntos de código en ese rango específico (es decir, 65 codificados en UTF-8 también es 65). Teniendo en cuenta que Unicode y ASCII también son compatibles en el mismo rango, incidentalmente hace que UTF-8 y ASCII también sean compatibles en ese rango.

Por ejemplo, el punto de código Unicode para 'B' es '0x42' o 0100 0010 en binario (como dijimos, es lo mismo en ASCII). Después de codificar en UTF-8 se convierte en:

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)

Codificación UTF-8 de puntos de código Unicode por encima de 127 (no ASCII):

110x xxxx 10xx xxxx            <-- (from 128 to 2047)
1110 xxxx 10xx xxxx 10xx xxxx  <-- (from 2048 to 65535)
  • los bits iniciales '110' indican al decodificador UTF-8 el comienzo de un punto de código codificado en 2 bytes, mientras que '1110' indica 3 bytes, 11110 indicaría 4 bytes y así sucesivamente.
  • Los bits de bandera '10' internos se utilizan para señalar el comienzo de un byte interno.
  • nuevamente, las x marcan el espacio donde se almacena el valor del punto de código Unicode después de la codificación.

por ejemplo, el punto de código 'é' Unicode es 0xe9 (233).

1110 1001    <-- 0xe9

Cuando UTF-8 codifica este valor, determina que el valor es mayor que 127 y menor que 2048, por lo tanto, debe codificarse en 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

Los puntos de código 0xe9 Unicode después de la codificación UTF-8 se convierten en 0xc3a9. Que es exactamente como lo recibe el terminal. Si su terminal está configurado para decodificar cadenas usando latin-1 (una de las codificaciones heredadas no unicode), verá à ©, porque sucede que 0xc3 en latin-1 apunta a à y 0xa9 a ©.

Michael Ekoka
fuente
66
Excelente explicación ¡Ahora entiendo UTF-8!
Doctor Coder
2
Bien, leí toda tu publicación en unos 10 segundos. Decía: "Python apesta cuando se trata de codificar".
Andrew
Gran explicación ¿Podría abordar esta pregunta?
Maggyero
26

Cuando los caracteres Unicode se imprimen en stdout, sys.stdout.encodingse utiliza. Se supone que hay un carácter que no es Unicode sys.stdout.encodingy se envía al terminal. En mi 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() solo se usa cuando Python no tiene otra opción.

Tenga en cuenta que Python 3.6 o posterior ignora las codificaciones en Windows y utiliza las API de Unicode para escribir Unicode en el terminal. No hay advertencias UnicodeEncodeError y se muestra el carácter correcto si la fuente lo admite. Incluso si la fuente no es compatible, los caracteres se pueden cortar y pegar desde el terminal a una aplicación con una fuente compatible y será correcto. ¡Potenciar!

Mark Tolonen
fuente
8

Python REPL intenta elegir qué codificación usar de su entorno. Si encuentra algo cuerdo, entonces todo funciona. Cuando no puede darse cuenta de lo que está sucediendo, se da cuenta.

>>> print sys.stdout.encoding
UTF-8
Ignacio Vazquez-Abrams
fuente
3
solo por curiosidad, ¿cómo cambiaría sys.stdout.encoding a ascii?
Michael Ekoka
2
@TankorSmash Me estoy poniendo TypeError: readonly attributeen 2.7.2
Kos
4

Usted ha especificado una codificación mediante la introducción de una cadena Unicode explícita. Compare los resultados de no usar el uprefijo.

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

>>> 

En el caso de \xe9entonces Python asume su codificación predeterminada (Ascii), imprimiendo ... algo en blanco.

Mark Rushakoff
fuente
1
Entonces, si entiendo bien, cuando imprimo cadenas Unicode (los puntos de código), Python asume que quiero una salida codificada en utf-8, en lugar de tratar de darme lo que podría haber sido en ASCII.
Michael Ekoka
1
@mike: AFAIK lo que dijiste es correcto. Si se hizo imprimir los caracteres Unicode, pero codificado como ASCII, todo iba a salir ilegible y probablemente todos los principiantes sería preguntar: "¿Cómo es que no se puede imprimir texto Unicode?"
Mark Rushakoff
2
Gracias. En realidad, soy uno de esos principiantes, pero vengo del lado de las personas que tienen cierta comprensión de Unicode, por lo que este comportamiento me está desanimando un poco.
Michael Ekoka
3
R., no es correcto, ya que '\ xe9' no está en el conjunto de caracteres ascii. Las cadenas no Unicode se imprimen utilizando sys.stdout.encoding, las cadenas Unicode se codifican en sys.stdout.encoding antes de imprimir.
Mark Tolonen
0

Esto funciona para mi:

import sys
stdin, stdout = sys.stdin, sys.stdout
reload(sys)
sys.stdin, sys.stdout = stdin, stdout
sys.setdefaultencoding('utf-8')
usuario3611630
fuente
1
Truco sucio y barato que inevitablemente romperá algo más. ¡No es difícil hacerlo de la manera correcta!
Chris Johnson
0

Según las codificaciones y conversiones de cadenas implícitas / implícitas de Python :

  • Cuando printing unicode, es encoded con <file>.encoding.
    • cuando encodingno se establece, unicodese convierte implícitamente en str(ya que el códec para eso es sys.getdefaultencoding(), es decir ascii, cualquier carácter nacional causaría a UnicodeEncodeError)
    • para flujos estándar, encodingse infiere del entorno. Por lo general, se configura para ttytransmisiones (desde la configuración regional del terminal), pero es probable que no se configure para tuberías
      • entonces print u'\xe9'es probable que a tenga éxito cuando la salida es a una terminal, y falla si se redirige. Una solución es encode()la cadena con la codificación deseada antes de printing.
  • Cuando printing str, los bytes se envían a la secuencia tal cual. Los glifos que muestre el terminal dependerán de su configuración regional.
ivan_pozdeev
fuente