当默认编码为 ASCII 时,为什么 Python 会打印 Unicode 字符?

来自 Python 2.6 shell:

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

我希望在 print 语句之后出现一些胡言乱语或错误,因为“ é”字符不是 ASCII 的一部分,而且我还没有指定编码。我想我不明白 ASCII 作为默认编码意味着什么。

剪辑

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

84005 次浏览

PythonREPL 尝试从您的环境中获取要使用的编码。如果它找到了一些正常的东西,那么一切都会好起来的。当它搞不清楚发生了什么的时候,它就会发飙。

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

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

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


>>>

\xe9的情况下,Python 假设您的默认编码(Ascii) ,因此打印... 一些空白。

将 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.
Θ

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

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

多亏了各种各样的回复,我想我们可以拼凑出一个解释。

通过尝试打印 Unicode 字符串 u’xe9’,Python 隐式地尝试使用当前存储在 sys.stdout.coding 中的编码模式对该字符串进行编码。Python 实际上是从启动它的环境中获取这个设置的。如果它无法从环境中找到正确的编码,那么只有这样它才能恢复到它的 违约,ASCII。

例如,我使用一个 bash shell,它的编码缺省为 UTF-8。如果我从它开始启动 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 的环境更改终端的编码。然后,让我们从 shell 启动 Python,并验证 sys.stdout.coding 是否设置为 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 按原样输出二进制字符串,终端接收二进制字符串并尝试将其值与拉丁字符映射匹配。在拉丁文 -1中,0xe9或233产生字符“ é”,这就是终端显示的内容。

(2) python 尝试使用当前在 sys.stdout.coding 中设置的任何方案对 Unicode 字符串进行 毫无疑问编码,在本例中是“ UTF-8”。在 UTF-8编码之后,生成的二进制字符串是“ xc3xa9”(请参阅后面的说明)。终端接收流本身,并尝试使用拉丁文 -1解码0xc3a9,但拉丁文 -1从0到255,因此,一次只解码流1字节。0xc3a9有2个字节长,因此拉丁 -1解码器将其解释为0xc3(195)和0xa9(169) ,并且产生2个字符: ó。

(3) python 使用拉丁1方案对 Unicode字符 u’xe9’(233)进行编码。结果表明,拉丁 -1代码点的范围是0-255,并指向该范围内与 Unicode 完全相同的字符。因此,当用拉丁文 -1编码时,该范围内的 Unicode 代码点将产生相同的值。因此,用拉丁文 -1编码的 u’xe9’(233)也将生成二进制字符串‘ xe9’。终端接收到该值并尝试在拉丁字符映射中匹配它。就像大小写(1)一样,它产生“ é”,这就是所显示的内容。

现在让我们从下拉菜单中将终端的编码设置更改为 UTF-8(就像更改浏览器的编码设置一样)。不需要停止 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.coding 中的内容对 Unicode 字符串进行 毫无疑问编码。还是“ UTF-8”。生成的二进制字符串是“ xc3 xa9”。终端接收流并尝试使用 UTF-8对0xc3a9进行解码。它会返回代码值0xe9(233) ,在 Unicode字符地图上指向符号“ é”。终端显示“ é”。

(6) python 用 latin-1编码 unicode 字符串,它生成一个值为“ xe9”的二进制字符串。同样,对于终端,这与 case (4)几乎相同。

结论: - Python 将非 unicode 字符串作为原始数据输出,而不考虑其默认编码。如果终端的当前编码与数据匹配,那么它只是碰巧显示它们。 - Python 在使用 sys.stdout.coding 中指定的方案对 Unicode 字符串进行编码之后,输出 Unicode 字符串。 Python 从 shell 的环境中获取这个设置。 终端根据自己的编码设置显示输出。 终端的编码是独立于外壳的。


关于 unicode、 UTF-8和拉丁文 -1的更多细节:

Unicode 基本上是一个字符表,其中一些键(代码点)通常被分配给指向某些符号。例如,按照惯例,键0xe9(233)是指向符号‘ é’的值。ASCII 和 Unicode 使用从0到127的相同代码点,拉丁文 -1和 Unicode 使用从0到255的相同代码点。也就是说,在 ASCII,latin-1和 Unicode 中,0x41指向‘ A’,在 latin-1和 Unicode 中,0xc8指向‘ Ü’,在 latin-1和 Unicode 中,0xe9指向‘ é’。

在使用电子设备时,Unicode 代码点需要一种有效的电子表示方式。这就是编码模式的意义所在。有各种 Unicode 编码模式(utf7、 UTF-8、 UTF-16、 UTF-32)。最直观和直接的编码方法是简单地使用 Unicode 地图中的一个编码点的值作为其电子形式的值,但 Unicode 目前有超过100万个编码点,这意味着其中一些编码点需要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在代码点上加上了标志位,这些标志位指示(大概是指解码器)它们的空间需求和边界。

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

0xxx xxxx  (in binary)
  • X 显示了在编码期间为“存储”代码点保留的实际空间
  • 前面的0是一个标志,它向 UTF-8解码器指示这个代码点只需要1个字节。
  • 在编码时,UTF-8不会改变该特定范围内的代码点值(即用 UTF-8编码的65也是65)。考虑到 Unicode 和 ASCII 在同一范围内也是兼容的,所以顺便说一下,UTF-8和 ASCII 在这个范围内也是兼容的。

例如,“ b”的 Unicode字符是“0x42”或者二进制的0100010(正如我们说过的,在 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)

UTF-8编码 Unicode 编码点大于127(非 ascii) :

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

例如,Unicode字符是0x9(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

0xe9Unicode 代码指向 UTF-8编码之后的0xc3a9。这正是终端接收信号的方式。如果您的终端设置为使用 latin-1(非 unicode 遗留编码之一)对字符串进行解码,您将看到 ó,因为正好 latin-1中的0xc3指向 ó 和0xa9 to。

这对我很有用:

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

根据 Python 默认/隐式字符串编码和转换:

  • print0unicode,它是 encoded 与 <file>.encoding
    • 当没有设置 encoding时,unicode被隐式转换为 str(因为 sys.getdefaultencoding()的编解码器是 sys.getdefaultencoding(),也就是 ascii,任何国家字符都会导致 UnicodeEncodeError)
    • 对于文本数据流来说,encoding是从环境中推断出来的。它通常是为 tty流设置的(来自终端的区域设置) ,但可能不是为管道设置的
      • 因此,当输出到终端时,print u'\xe9'很可能会成功,而当输出被重定向时,print u'\xe9'很可能会失败。一种解决方案是在 printing 之前使用所需的编码对 encode()字符串进行编码。
  • printingstr时,字节按原样发送到流。终端显示的字形取决于它的区域设置。