为什么默认编码是ASCII,Python还能打印Unicode字符?

143

来自Python 2.6 shell:

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

在打印语句后面,我预期要么输出一些无意义的字符,要么出现错误,因为"é"字符不属于ASCII码表,而且我没有指定编码方式。我猜我不理解ASCII作为默认编码的含义。

编辑

我将编辑内容移到了答案部分,并按建议进行了接受。


6
如果您能将那个"编辑"变成答案并接受它,那会非常好。 - mercator
2
在配置为UTF-8的终端中打印'\xe9'不会打印出é。它将打印一个替换字符(通常是问号),因为\xe9不是有效的UTF-8序列(缺少应该跟随该前导字节的两个字节)。它肯定不会被解释为Latin-1。 - Martijn Pieters
2
@MartijnPieters 我猜你可能忽略了我指定终端解码为ISO-8859-1(latin1)的部分,当我输出\xe9以打印é时。 - Michael Ekoka
2
嗯,我确实错过了那一部分;终端的配置与 shell 不同。请检查一下。 - Martijn Pieters
我浏览了一下答案,但实际上,我在Python 2.7中的字符串没有u前缀。为什么那个字符串仍然被处理为Unicode?(我的sys.getdefaultencoding()是ASCII) - dtc
显示剩余2条评论
6个回答

108
感谢各位回复中提供的信息,我认为我们可以总结一个解释。当尝试打印Unicode字符串u'\xe9'时,Python会隐式地尝试使用当前存储在sys.stdout.encoding中的编码方案对该字符串进行编码。Python实际上是从它被初始化的环境中获取这个设置的。如果它无法从环境中找到适当的编码方式,那么它才会退回到其默认编码ASCII。
例如,我使用的是默认编码为UTF-8的bash shell。如果我从它启动Python,Python会自动选择并使用这个设置。
$ python

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

让我们暂时退出Python shell,并使用一些虚假编码来设置bash环境:

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

然后重新启动Python shell,验证它是否确实恢复到默认的ASCII编码。
$ python

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

太好了!

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

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

让我们退出Python并关闭bash shell。

现在,我们将观察Python输出字符串后会发生什么。为此,我们首先要在图形终端中启动一个bash shell(我使用Gnome Terminal),并将终端设置为使用ISO-8859-1(也称为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)
é
>>>

(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,因此仅逐字节解码流。 0xc3a9长达2个字节,因此latin-1解码器将其解释为0xc3(195)和0xa9(169),并产生2个字符:Ã和©。

(3) Python使用latin-1方案对Unicode代码点u'\ xe9'(233)进行编码。结果发现,latin-1代码点范围为0-255,并且在该范围内指向与Unicode相同的确切字符。因此,在该范围内的Unicode代码点将在latin-1中编码时产生相同的值。因此,在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)

>>>

(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字符串进行编码后输出。 - Python从shell的环境获取该设置。 - 终端根据其自己的编码设置显示输出。 - 终端的编码与shell的编码是独立的。
更多关于Unicode、UTF-8和latin-1的细节: Unicode基本上是一个字符表,其中一些键(代码点)被传统地分配为指向某些符号的值。例如,按照惯例,已决定将键0xe9(233)指向指向符号'é'的值。 ASCII和Unicode使用相同的代码点从0到127,latin-1和Unicode使用从0到255的相同代码点。也就是说,0x41在ASCII、latin-1和Unicode中都指向'A',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在码点中添加标志位,指示(可能是给解码器)它们的空间需求和边界。
Unicode码点在ASCII范围内(0-127)的UTF-8编码方式:
0xxx xxxx  (in binary)
  • x 的位置显示了编码期间实际保留的用于“存储”代码点的空间
  • 前导 0 是一个标志,告诉 UTF-8 解码器这个代码点仅需要 1 字节。
  • 在编码过程中,UTF-8 不会改变该特定范围内(即 65)代码点的值(即 65 在 UTF-8 中编码后仍然是 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)

Unicode代码点超过127(非ASCII)的UTF-8编码:

110x xxxx 10xx xxxx            <-- (from 128 to 2047)
1110 xxxx 10xx xxxx 10xx xxxx  <-- (from 2048 to 65535)
  • UTF-8解码器中,前导位“110”表示使用2个字节对代码点进行编码,“1110”表示3个字节,而“11110”表示4个字节,以此类推。
  • 内部的“10”标志位用于标识内部字节的开始。
  • 同样,x代表编码后存储Unicode代码点值的空间。

例如,“é”的Unicode代码点为0xe9(233)。

1110 1001    <-- 0xe9

当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

0xe9 Unicode编码在UTF-8编码后变成0xc3a9,这也正是终端接收到的内容。如果你的终端使用Latin-1(一种非Unicode遗留编码)解码字符串,你会看到é,因为恰好0xc3在Latin-1中指向Ã,0xa9指向©。

6
非常清晰易懂的解释,现在我懂得了UTF-8! - Doctor Coder
3
好的,我会尽力进行翻译。这段话的意思是:好的,我在大约10秒钟内阅读了你的整篇帖子。其中说到,“涉及编码时,Python很烂。” - Andrew
很好的解释。你能回答一下这个问题吗?这里 - Géry Ogam

27

当Unicode字符被打印到标准输出时,会使用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.
Θ

sys.getdefaultencoding() 只有在Python没有其他选项时才会使用。

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


8

Python REPL会尝试从您的环境中选择要使用的编码方式。如果它发现了一些合理的内容,那么所有的操作都可以正常工作。但当它无法确定正在发生什么时,就会出现错误。

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

3
仅出于好奇,我该怎样将 sys.stdout.encoding 改为 ascii 呢? - Michael Ekoka
2
@TankorSmash 我在2.7.2上遇到了“TypeError: readonly attribute”的问题。 - Kos

4

您通过输入显式的Unicode字符串指定了编码。与不使用u前缀的结果进行比较。

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

>>> 

在使用\xe9时,Python会默认使用Ascii编码,因此打印出来的内容可能为空。

1
如果我理解正确的话,当我打印Unicode字符串(代码点)时,Python会假定我想要一个以UTF-8编码的输出,而不是仅仅尝试给我ASCII中可能存在的内容? - Michael Ekoka
1
@mike:据我所知,你说的是正确的。如果它将Unicode字符打印出来但编码为ASCII,那么一切都会变得混乱,可能所有初学者都会问:“为什么我无法打印Unicode文本?” - Mark Rushakoff
2
谢谢。实际上我是其中的一位初学者,但从那些对 Unicode 有一定了解的人的角度来看,这种行为让我有些摸不着头脑。 - Michael Ekoka
3
R. 不正确,因为 '\xe9' 不在 ASCII 字符集中。非 Unicode 字符串使用 sys.stdout.encoding 进行打印,Unicode 字符串在打印之前编码为 sys.stdout.encoding。 - Mark Tolonen

0

对我来说它有效:

import sys
stdin, stdout = sys.stdin, sys.stdout
reload(sys)
sys.stdin, sys.stdout = stdin, stdout
sys.setdefaultencoding('utf-8')

1
廉价的肮脏黑客行为,必然会破坏其他东西。用正确的方式做并不难! - Chris Johnson

0
根据Python默认/隐式字符串编码和转换
  • 当打印unicode时,它会使用<file>.encoding进行编码。
    • 当未设置encoding时,unicode会隐式转换为str(因为该编解码器为sys.getdefaultencoding(),即ascii,任何国际字符都会导致UnicodeEncodeError
    • 对于标准流,encoding是从环境中推断出来的。它通常针对tty流进行设置(从终端的区域设置中),但可能不适用于管道
      • 因此,当输出到终端时,print u'\xe9'很可能会成功,如果重定向,则会失败。解决方案是在print之前使用所需的编码对字符串进行encode()
  • 当打印str时,字节将按原样发送到流中。终端显示的图形取决于其区域设置。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接