Pourquoi Python imprime-t-il des caractères Unicode lorsque l'encodage par défaut est ASCII?

139

Depuis le shell Python 2.6:

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

Je m'attendais à avoir du charabia ou une erreur après l'instruction d'impression, car le caractère "é" ne fait pas partie de l'ASCII et je n'ai pas spécifié d'encodage. Je suppose que je ne comprends pas ce que signifie le codage par défaut ASCII.

ÉDITER

J'ai déplacé la modification dans la section Réponses et l' ai acceptée comme suggéré.

Michael Ekoka
la source
6
Ce serait plutôt bien si vous pouviez transformer cette modification en réponse à la place et l'accepter.
mercator
2
L'impression '\xe9'dans un terminal configuré pour UTF-8 n'imprimera pasé . Il imprimera un caractère de remplacement (généralement un point d'interrogation) car ce \xe9n'est pas une séquence UTF-8 valide (il manque deux octets qui auraient dû suivre cet octet de début). Il ne sera certainement pas interprété comme Latin-1 à la place.
Martijn Pieters
2
@MartijnPieters Je suppose que vous pourriez avoir écrémé sur la partie où je précisé que le terminal est réglé sur decode dans la norme ISO-8859-1 (latin1) quand je sortie \xe9pour imprimer é.
Michael Ekoka
2
Ah oui, j'ai raté cette partie; le terminal a une configuration différente de celle du shell. Vérifier.
Martijn Pieters
J'ai parcouru la réponse mais en fait, j'ai la chaîne sans le préfixe u pour python 2.7. pourquoi celui-ci est-il toujours traité comme unicode? (mon sys.getdefaultencoding () est ascii)
dtc

Réponses:

104

Grâce à des bribes de diverses réponses, je pense que nous pouvons recoudre une explication.

En essayant d'imprimer une chaîne Unicode, u '\ xe9', Python essaie implicitement d'encoder cette chaîne en utilisant le schéma d'encodage actuellement stocké dans sys.stdout.encoding. Python récupère en fait ce paramètre à partir de l'environnement à partir duquel il a été lancé. S'il ne trouve pas de codage approprié dans l'environnement, ce n'est qu'alors qu'il revient à sa valeur par défaut , ASCII.

Par exemple, j'utilise un shell bash dont le codage par défaut est UTF-8. Si je démarre Python à partir de celui-ci, il récupère et utilise ce paramètre:

$ python

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

Quittons un instant le shell Python et définissons l'environnement de bash avec un faux encodage:

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

Ensuite, redémarrez le shell python et vérifiez qu'il revient bien à son encodage ascii par défaut.

$ python

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

Bingo!

Si vous essayez maintenant de sortir un caractère unicode en dehors de ascii, vous devriez obtenir un joli message d'erreur

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

Permet de quitter Python et de supprimer le shell bash.

Nous allons maintenant observer ce qui se passe après que Python a produit des chaînes. Pour cela, nous allons d'abord démarrer un shell bash dans un terminal graphique (j'utilise Gnome Terminal) et nous allons configurer le terminal pour décoder la sortie avec ISO-8859-1 aka latin-1 (les terminaux graphiques ont généralement une option pour Set Character Encodage dans l'un de leurs menus déroulants). Notez que cela ne change pas le codage de l'environnement shell réel , cela ne change que la façon dont le terminal lui-même décodera la sortie qui lui est donnée, un peu comme le fait un navigateur Web. Vous pouvez donc changer l'encodage du terminal, indépendamment de l'environnement du shell. Commençons ensuite Python à partir du shell et vérifions que sys.stdout.encoding est défini sur l'encodage de l'environnement du shell (UTF-8 pour moi):

$ 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 renvoie la chaîne binaire telle quelle, le terminal la reçoit et essaie de faire correspondre sa valeur avec la table de caractères latin-1. En latin-1, 0xe9 ou 233 donne le caractère "é" et c'est donc ce que le terminal affiche.

(2) python tente de coder implicitement la chaîne Unicode avec le schéma actuellement défini dans sys.stdout.encoding, dans ce cas, c'est "UTF-8". Après le codage UTF-8, la chaîne binaire résultante est '\ xc3 \ xa9' (voir l'explication ultérieure). Le terminal reçoit le flux en tant que tel et essaie de décoder 0xc3a9 en utilisant latin-1, mais latin-1 passe de 0 à 255 et donc, ne décode que les flux 1 octet à la fois. 0xc3a9 fait 2 octets de long, le décodeur latin-1 l'interprète donc comme 0xc3 (195) et 0xa9 (169) et cela donne 2 caractères: Ã et ©.

(3) python encode le point de code unicode u '\ xe9' (233) avec le schéma latin-1. Il s'avère que la plage de points de code latin-1 est comprise entre 0 et 255 et pointe exactement le même caractère que Unicode dans cette plage. Par conséquent, les points de code Unicode dans cette plage donneront la même valeur lorsqu'ils sont encodés en latin-1. Ainsi, u '\ xe9' (233) encodé en latin-1 donnera également la chaîne binaire '\ xe9'. Le terminal reçoit cette valeur et essaie de la faire correspondre sur la carte de caractères latin-1. Tout comme le cas (1), il donne "é" et c'est ce qui est affiché.

Modifions maintenant les paramètres d'encodage du terminal en UTF-8 dans le menu déroulant (comme si vous changiez les paramètres d'encodage de votre navigateur Web). Pas besoin d'arrêter Python ou de redémarrer le shell. Le codage du terminal correspond maintenant à celui de Python. Essayons d'imprimer à nouveau:

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

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

>>>

(4) python génère un binaire chaîne telle quelle. Le terminal tente de décoder ce flux avec UTF-8. Mais UTF-8 ne comprend pas la valeur 0xe9 (voir l'explication ultérieure) et est donc incapable de la convertir en un point de code Unicode. Aucun point de code trouvé, aucun caractère imprimé.

(5) python tente d' encoder implicitement la chaîne Unicode avec tout ce qui se trouve dans sys.stdout.encoding. Toujours "UTF-8". La chaîne binaire résultante est '\ xc3 \ xa9'. Le terminal reçoit le flux et tente de décoder 0xc3a9 en utilisant également UTF-8. Il renvoie la valeur de code 0xe9 (233), qui sur la table de caractères Unicode pointe vers le symbole «é». Le terminal affiche "é".

(6) python encode une chaîne unicode avec latin-1, il donne une chaîne binaire avec la même valeur '\ xe9'. Encore une fois, pour le terminal, c'est à peu près la même chose que le cas (4).

Conclusions: - Python génère des chaînes non-Unicode sous forme de données brutes, sans tenir compte de son encodage par défaut. Le terminal les affiche juste si son encodage actuel correspond aux données. - Python génère des chaînes Unicode après les avoir encodées à l'aide du schéma spécifié dans sys.stdout.encoding. - Python obtient ce paramètre de l'environnement du shell. - le terminal affiche la sortie en fonction de ses propres paramètres d'encodage. - l'encodage du terminal est indépendant de celui du shell.


Plus de détails sur unicode, UTF-8 et latin-1:

Unicode est essentiellement une table de caractères dans laquelle certaines touches (points de code) ont été classiquement assignées pour pointer vers certains symboles. par exemple, par convention, il a été décidé que la clé 0xe9 (233) est la valeur pointant vers le symbole «é». ASCII et Unicode utilisent les mêmes points de code de 0 à 127, comme le font latin-1 et Unicode de 0 à 255. Autrement dit, 0x41 pointe vers «A» en ASCII, latin-1 et Unicode, 0xc8 pointe vers «Ü» dans latin-1 et Unicode, 0xe9 pointe vers «é» en latin-1 et Unicode.

Lorsque vous travaillez avec des appareils électroniques, les points de code Unicode ont besoin d'un moyen efficace d'être représentés électroniquement. C'est ce que sont les schémas d'encodage. Différents schémas de codage Unicode existent (utf7, UTF-8, UTF-16, UTF-32). L'approche de codage la plus intuitive et la plus simple serait d'utiliser simplement la valeur d'un point de code dans la carte Unicode comme valeur pour sa forme électronique, mais Unicode a actuellement plus d'un million de points de code, ce qui signifie que certains d'entre eux nécessitent 3 octets pour être exprimé. Pour travailler efficacement avec du texte, un mappage 1 à 1 serait plutôt peu pratique, car il exigerait que tous les points de code soient stockés exactement dans la même quantité d'espace, avec un minimum de 3 octets par caractère, quel que soit leur besoin réel.

La plupart des schémas d'encodage présentent des lacunes en termes d'espace, les plus économiques ne couvrent pas tous les points de code unicode, par exemple ascii ne couvre que les 128 premiers, tandis que latin-1 couvre les 256 premiers. D'autres qui essaient d'être plus complets finissent également par étant un gaspillage, car ils nécessitent plus d'octets que nécessaire, même pour les caractères «bon marché» courants. UTF-16 par exemple, utilise un minimum de 2 octets par caractère, y compris ceux de la plage ascii («B» qui vaut 65, nécessite toujours 2 octets de stockage en UTF-16). UTF-32 est encore plus coûteux car il stocke tous les caractères sur 4 octets.

UTF-8 a résolu intelligemment le dilemme, avec un schéma capable de stocker des points de code avec une quantité variable d'espaces d'octets. Dans le cadre de sa stratégie de codage, UTF-8 associe des points de code à des bits d'indicateur qui indiquent (vraisemblablement aux décodeurs) leurs besoins en espace et leurs limites.

Codage UTF-8 des points de code unicode dans la plage ascii (0-127):

0xxx xxxx  (in binary)
  • les x indiquent l'espace réel réservé pour "stocker" le point de code pendant l'encodage
  • Le 0 en tête est un indicateur qui indique au décodeur UTF-8 que ce point de code ne nécessitera qu'un octet.
  • lors du codage, UTF-8 ne change pas la valeur des points de code dans cette plage spécifique (c'est-à-dire que 65 encodés en UTF-8 valent également 65). Étant donné que Unicode et ASCII sont également compatibles dans la même plage, cela rend d'ailleurs UTF-8 et ASCII également compatibles dans cette plage.

Par exemple, le point de code Unicode pour 'B' est '0x42' ou 0100 0010 en binaire (comme nous l'avons dit, c'est la même chose en ASCII). Après encodage en UTF-8, il devient:

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)

Codage UTF-8 des points de code Unicode au-dessus de 127 (non-ascii):

110x xxxx 10xx xxxx            <-- (from 128 to 2047)
1110 xxxx 10xx xxxx 10xx xxxx  <-- (from 2048 to 65535)
  • les bits de tête «110» indiquent au décodeur UTF-8 le début d'un point de code codé sur 2 octets, alors que «1110» indique 3 octets, 11110 indiquerait 4 octets et ainsi de suite.
  • les bits d'indicateur internes «10» sont utilisés pour signaler le début d'un octet interne.
  • à nouveau, les x marquent l'espace où la valeur du point de code Unicode est stockée après le codage.

Par exemple, le point de code Unicode 'é' est 0xe9 (233).

1110 1001    <-- 0xe9

Lorsque UTF-8 encode cette valeur, il détermine que la valeur est supérieure à 127 et inférieure à 2048 et doit donc être encodée sur 2 octets:

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

Le code Unicode 0xe9 pointe après le codage UTF-8 devient 0xc3a9. C'est exactement ainsi que le terminal le reçoit. Si votre terminal est configuré pour décoder des chaînes en utilisant latin-1 (l'un des encodages hérités non-unicode), vous verrez à ©, car il se trouve que 0xc3 en latin-1 pointe vers à et 0xa9 vers ©.

Michael Ekoka
la source
6
Excellente explication. Maintenant, je comprends UTF-8!
Doctor Coder
2
D'accord, j'ai lu l'intégralité de votre message en 10 secondes environ. Il disait: "Python est nul en matière d'encodage."
Andrew
Excellente explication. Pouvez-vous répondre à cette question?
Maggyero
26

Lorsque les caractères Unicode sont imprimés sur stdout, sys.stdout.encodingest utilisé. Un caractère non Unicode est supposé être dans sys.stdout.encodinget est simplement envoyé au terminal. Sur mon système (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() n'est utilisé que lorsque Python n'a pas d'autre option.

Notez que Python 3.6 ou version ultérieure ignore les encodages sous Windows et utilise les API Unicode pour écrire Unicode sur le terminal. Aucun avertissement UnicodeEncodeError et le caractère correct s'affiche si la police le prend en charge. Même si la police ne la prend pas en charge, les caractères peuvent toujours être copiés-collés depuis le terminal vers une application avec une police de support et ce sera correct. Améliorer!

Mark Tolonen
la source
8

Le Python REPL essaie de sélectionner le codage à utiliser dans votre environnement. S'il trouve quelque chose de sain, alors tout fonctionne. C'est quand il ne peut pas comprendre ce qui se passe que ça dérange.

>>> print sys.stdout.encoding
UTF-8
Ignacio Vazquez-Abrams
la source
3
juste par curiosité, comment changerais-je sys.stdout.encoding en ascii?
Michael Ekoka
2
@TankorSmash J'obtiens TypeError: readonly attribute2.7.2
Kos
4

Vous avez spécifié un encodage en entrant une chaîne Unicode explicite. Comparez les résultats de la non-utilisation du upréfixe.

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

>>> 

Dans le cas de, \xe9Python suppose votre encodage par défaut (Ascii), imprimant ainsi ... quelque chose de vide.

Mark Rushakoff
la source
1
donc si je comprends bien, lorsque j'imprime des chaînes unicode (les points de code), python suppose que je veux une sortie encodée en utf-8, au lieu d'essayer simplement de me donner ce que cela aurait pu être en ascii?
Michael Ekoka
1
@mike: AFAIK ce que vous avez dit est correct. Si elle a fait imprimer les caractères Unicode , mais codé en ASCII, tout serait confus et sortir probablement tous les débutants serait demander: « Pourquoi je ne peux pas imprimer le texte Unicode? »
Mark Rushakoff
2
Je vous remercie. Je suis en fait l'un de ces débutants, mais je viens du côté de personnes qui ont une certaine compréhension de l'unicode, c'est pourquoi ce comportement me dérange un peu.
Michael Ekoka
3
R., incorrect, car '\ xe9' n'est pas dans le jeu de caractères ascii. Les chaînes non Unicode sont imprimées à l'aide de sys.stdout.encoding, les chaînes Unicode sont encodées en sys.stdout.encoding avant l'impression.
Mark Tolonen
0

Ça marche pour moi:

import sys
stdin, stdout = sys.stdin, sys.stdout
reload(sys)
sys.stdin, sys.stdout = stdin, stdout
sys.setdefaultencoding('utf-8')
user3611630
la source
1
Hack sale bon marché qui cassera inévitablement autre chose. Ce n'est pas difficile de le faire de la bonne façon!
Chris Johnson
0

Selon les encodages et conversions de chaînes par défaut / implicites de Python :

  • Quand on printest unicode, c'est encoded avec <file>.encoding.
    • lorsque le encodingn'est pas défini, le unicodeest implicitement converti en str(puisque le codec pour cela est sys.getdefaultencoding(), c'est ascii-à- dire que tout caractère national entraînerait un UnicodeEncodeError)
    • pour les flux standard, le encodingest déduit de l'environnement. Il est généralement défini pour les ttyflux (à partir des paramètres régionaux du terminal), mais il est susceptible de ne pas être défini pour les tubes
      • donc a print u'\xe9'est susceptible de réussir lorsque la sortie est vers un terminal, et d'échouer s'il est redirigé. Une solution est de encode()la chaîne avec le codage souhaité avant printing.
  • Lorsque printing str, les octets sont envoyés au flux comme il est. Les glyphes affichés par le terminal dépendront de ses paramètres régionaux.
ivan_pozdeev
la source