为什么Python在默认编码为ASCII时会打印unicode字符?

Mic*_*oka 137 python unicode encoding ascii python-2.x

从Python 2.6 shell:

>>> import sys
>>> print sys.getdefaultencoding()
ascii
>>> print u'\xe9'
é
>>> 
Run Code Online (Sandbox Code Playgroud)

我希望在print语句之后有一些乱码或错误,因为"é"字符不是ASCII的一部分,我没有指定编码.我想我不明白ASCII是默认编码的意思.

编辑

我将编辑移动到了答案部分并按照建议接受了它.

Mic*_*oka 102

感谢各种回复的点点滴滴,我想我们可以解释一下.

通过尝试打印unicode字符串u'\ xe9',Python隐式尝试使用当前存储在sys.stdout.encoding中的编码方案对该字符串进行编码.Python实际上是从它所启动的环境中获取此设置.如果它无法从环境中找到正确的编码,那么它才会恢复为默认的 ASCII.

例如,我使用bash shell,其默认编码为UTF-8.如果我从它启动Python,它会选择并使用该设置:

$ python

>>> import sys
>>> print sys.stdout.encoding
UTF-8
Run Code Online (Sandbox Code Playgroud)

让我们暂时退出Python shell并使用一些伪造的编码设置bash的环境:

$ export LC_CTYPE=klingon
# we should get some error message here, just ignore it.
Run Code Online (Sandbox Code Playgroud)

然后再次启动python shell并验证它确实恢复为其默认的ascii编码.

$ python

>>> import sys
>>> print sys.stdout.encoding
ANSI_X3.4-1968
Run Code Online (Sandbox Code Playgroud)

答对了!

如果你现在尝试在ascii之外输出一些unicode字符,你应该得到一个很好的错误消息

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

让我们退出Python并丢弃bash shell.

我们现在将观察Python输出字符串后会发生什么.为此,我们首先在图形终端(我使用Gnome终端)中启动bash shell,然后我们将终端设置为使用ISO-8859-1 aka latin-1解码输出(图形终端通常有一个选项来设置字符在其中一个下拉菜单中编码).请注意,这不会改变实际shell环境的编码,它只会改变终端本身解码输出的方式,有点像Web浏览器.因此,您可以独立于shell的环境更改终端的编码.然后让我们从shell启动Python并验证sys.stdout.encoding是否设置为shell环境的编码(对我来说是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)
é
>>>
Run Code Online (Sandbox Code Playgroud)

(1)python按原样输出二进制字符串,终端接收它并尝试将其值与latin-1字符映射匹配.在latin-1中,0xe9或233产生字符"é",这就是终端显示的内容.

(2)python尝试使用sys.stdout.encoding中当前设置的任何方案隐式编码Unicode字符串,在本例中它是"UTF-8".在UTF-8编码之后,生成的二进制字符串是'\ xc3\xa9'(参见后面的解释).终端如此接收流并尝试使用latin-1解码0xc3a9,但是latin-1从0变为255,因此,一次只解码1个字节的流.0xc3a9长度为2个字节,因此latin-1解码器将其解释为0xc3(195)和0xa9(169),并产生2个字符:Ã和©.

(3)python使用latin-1方案对unicode代码点u'\ xe9'(233)进行编码.结果是latin-1代码点范围是0-255并指向与该范围内的Unicode完全相同的字符.因此,在latin-1中编码时,该范围内的Unicode代码点将产生相同的值.因此,在latin-1中编码的u'\ xe9'(233)也将产生二进制字符串'\ xe9'.终端接收该值并尝试在latin-1字符映射上匹配它.就像情况(1)一样,它产生"é",这就是显示的内容.

现在让我们从下拉菜单中将终端的编码设置更改为UTF-8(就像您将更改Web浏览器的编码设置一样).无需停止Python或重启shell.终端的编码现在与Python相匹配.我们再试一次打印:

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

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

>>>
Run Code Online (Sandbox Code Playgroud)

(4)python 按原样输出二进制字符串.终端尝试使用UTF-8解码该流.但是UTF-8不理解值0xe9(参见后面的解释),因此无法将其转换为unicode代码点.找不到代码点,没有打印字符.

(5)python尝试使用sys.stdout.encoding中的任何内容隐式编码Unicode字符串.仍然是"UTF-8".生成的二进制字符串是'\ xc3\xa9'.终端接收流并尝试使用UTF-8解码0xc3a9.它产生后代码值0xe9(233),在Unicode字符映射上指向符号"é".终端显示"é".

(6)python使用latin-1对unicode字符串进行编码,它产生一个具有相同值'\ xe9'的二进制字符串.同样,对于终端,这与情况(4)几乎相同.

结论: - Python输出非unicode字符串作为原始数据,而不考虑其默认编码.如果当前编码与数据匹配,则终端恰好显示它们. - Python使用sys.stdout.encoding中指定的方案对Unicode字符串进行编码后输出Unicode字符串. - Python从shell的环境中获取该设置. - 终端根据自己的编码设置显示输出. - 终端的编码与shell的编码无关.


关于unicode,UTF-8和latin-1的更多细节:

Unicode基本上是一个字符表,其中一些键(代码点)通常被指定为指向某些符号.例如,通过惯例,已经确定密钥0xe9(233)是指向符号'é'的值.ASCII和Unicode使用相同的代码点,从0到127,latin-1和Unicode从0到255.也就是说,0x41指向ASCII中的'A',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,在UTF-16中仍需要2个字节的存储空间).UTF-32更加浪费,因为它将所有字符存储在4个字节中.

UTF-8碰巧巧妙地解决了这个难题,一个方案能够存储具有可变数量的字节空间的代码点.作为其编码策略的一部分,UTF-8使用标志位来标记代码点,这些标志位指示(可能是解码器)它们的空间要求及其边界.

ascii范围内的unicode代码点的UTF-8编码(0-127):

0xxx xxxx  (in binary)
Run Code Online (Sandbox Code Playgroud)
  • x表示在编码期间保留用于"存储"代码点的实际空间
  • 前导0是一个标志,向UTF-8解码器指示该代码点仅需要1个字节.
  • 在编码时,UTF-8不改变该特定范围内的代码点的值(即,以UTF-8编码的65也是65).考虑到Unicode和ASCII也在相同的范围内兼容,它偶然使得UTF-8和ASCII也在该范围内兼容.

例如,'B'的Unicode代码点是'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)
Run Code Online (Sandbox Code Playgroud)

Unicode代码的UTF-8编码高于127(非ascii):

110x xxxx 10xx xxxx            <-- (from 128 to 2047)
1110 xxxx 10xx xxxx 10xx xxxx  <-- (from 2048 to 65535)
Run Code Online (Sandbox Code Playgroud)
  • 前导比特'110'向UTF-8解码器指示以2字节编码的码点的开始,而'1110'表示3字节,11110表示4字节,依此类推.
  • 内部'10'标志位用于发信号通知内部字节的开头.
  • 再次,x标记编码后存储Unicode代码点值的空间.

例如,'é'Unicode代码点是0xe9(233).

1110 1001    <-- 0xe9
Run Code Online (Sandbox Code Playgroud)

当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
Run Code Online (Sandbox Code Playgroud)

UTF-8编码后的0xe9 Unicode代码点变为0xc3a9.这正是终端接收它的方式.如果您的终端设置为使用latin-1(非unicode遗留编码之一)解码字符串,您将看到é,因为它恰好发生在latin-1中的0xc3指向Ã和0xa9指向©.

  • 优秀的解释.现在我明白了UTF-8! (6认同)
  • 好的,我在大约10秒内读完了你的整个帖子.它说,"在编码方面,Python很糟糕." (2认同)

Mar*_*nen 25

将Unicode字符打印到stdout时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.
?
Run Code Online (Sandbox Code Playgroud)

sys.getdefaultencoding() 仅在Python没有其他选项时使用.

请注意,Python 3.6或更高版本会忽略Windows上的编码,并使用Unicode API将Unicode写入终端.如果字体支持,则不显示UnicodeEncodeError警告并显示正确的字符.即使字体支持它,字符仍然可以从终端切割到具有支持字体的应用程序,并且它将是正确的.升级!


Ign*_*ams 8

Python REPL尝试从您的环境中获取要使用的编码.如果它找到了理智的东西那么它就是Just Works.它是什么时候它无法弄清楚它发生了什么,它的错误.

>>> print sys.stdout.encoding
UTF-8
Run Code Online (Sandbox Code Playgroud)

  • 出于好奇,我如何将sys.stdout.encoding更改为ascii? (3认同)
  • @TankorSmash我在2.7.2上得到`TypeError:readonly attribute` (2认同)