Saltar al contenido

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

Solución:

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

Al intentar imprimir una cadena Unicode, u ‘ xe9’, Python implícitamente intenta codificar esa cadena utilizando el esquema de codificación almacenado actualmente en sys.stdout.encoding. Python realmente toma 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 defecto, ASCII.

Por ejemplo, uso un shell bash cuya codificación predeterminada es UTF-8. Si inicio Python desde él, toma 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 una codificación falsa:

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

Luego, inicie el shell de Python nuevamente 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 mensaje de error agradable

>>> 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 shell bash.

Ahora observaremos lo que sucede después de que Python genera cadenas. Para esto, primero iniciaremos un shell bash dentro de una terminal gráfica (yo uso Gnome Terminal) y configuraremos la terminal para decodificar la salida con ISO-8859-1 también conocido como latin-1 (las terminales gráficas generalmente tienen una opción para Establecer codificación de caracteres en uno de sus menús desplegables). Tenga en cuenta que esto no cambia la entorno de shell codificación, solo cambia la forma en que Terminal en sí mismo decodificará 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, iniciemos Python desde el shell y verifiquemos que sys.stdout.encoding esté configurado en 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 como está, 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 la terminal.

(2) Python intenta implícitamente codifique la cadena Unicode con cualquier esquema que esté configurado actualmente 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 más adelante). La 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 los flujos de 1 byte a la vez. 0xc3a9 tiene 2 bytes de longitud, 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 de 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 codifiquen en latin-1. Entonces, u ‘ xe9’ (233) codificado en latin-1 también producirá la cadena binaria ‘ xe9’. La terminal recibe ese valor e intenta hacer coincidirlo en el mapa de caracteres latin-1. Al igual que en 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 si cambiara la configuración de codificación de su navegador web). No es necesario detener Python o reiniciar el shell. La codificación de la 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 binario cadena como está. La terminal intenta decodificar ese flujo con UTF-8. Pero UTF-8 no comprende el valor 0xe9 (consulte la 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 implícitamente codifique la cadena Unicode con lo que sea que esté en sys.stdout.encoding. Sigue siendo “UTF-8”. La cadena binaria resultante es ‘ xc3 xa9’. La terminal recibe el flujo e intenta decodificar 0xc3a9 también usando UTF-8. Devuelve el valor de código 0xe9 (233), que en el mapa de caracteres Unicode apunta al símbolo “é”. La terminal muestra “é”.

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

Conclusiones: – Python genera cadenas que no son 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 usando 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 tecla 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 apunta a ‘A’ en ASCII, latin-1 y Unicode, 0xc8 apunta 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 ser representados 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 texto, un mapeo 1 a 1 sería poco práctico, 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 a los requisitos 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 terminan también son 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 los del 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 ha 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 enlaza 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 una bandera 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 codificado en UTF-8 también es 65). Teniendo en cuenta que Unicode y ASCII también son compatibles en el mismo rango, por cierto 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 la bandera ’10’ internos se utilizan para señalar el comienzo de un byte interno.
  • de nuevo, 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

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

Cuando se imprimen caracteres Unicode en stdout, sys.stdout.encoding se utiliza. Se supone que un carácter no Unicode está en sys.stdout.encoding y simplemente se envía a la 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 usa las API Unicode para escribir Unicode en la terminal. No hay advertencias de UnicodeEncodeError y se muestra el carácter correcto si la fuente lo admite. Incluso si la fuente no Admítelo, los caracteres aún se pueden cortar y pegar desde el terminal a una aplicación con una fuente de soporte y será correcto. ¡Potenciar!

Python REPL intenta elegir qué codificación usar de su entorno. Si encuentra algo cuerdo, todo funciona. Es cuando no puede averiguar qué está pasando cuando se equivoca.

>>> print sys.stdout.encoding
UTF-8
¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *