>>> from env_helper import info; info()
页面更新时间: 2023-04-15 21:15:15
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-7-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

1.4. 原始字符串和Unicode

1.4.1. 原始字符串

计算机模拟现实的世界,但总有不尽如人意之处;使用一种方式来表达或解释另外一种东西, 总会有些事情无法来表达。 在计算机的一些基本表达方面,也有这样的问题。比如打印 Windows 操作系统中的一个目录(或者说路径),如下,但得到的结果并不是期望的。

>>> print('C:\nowhere')
C:
owhere

产生这个问题的原因很复杂。在这里只需要记住,计算机的表达能力是有限的。为了更好地表达,就不得不引入一些额外的表达方式。

为了尽可能自然是表达上面的路径,Python引入了原始字符串的表达方式。 原始字符串以 r 开头,可以在原始字符串中放入任何字符,但不能以反斜线“ \ ”结尾,用来防止反斜线转义。例如:

>>> print(r'C:\nowhere')
C:nowhere

看起来可在原始字符串中包含任何字符,这大致是正确的。 一个例外是,引号需要像通常那样进行转义,但这意味着用于执行转义的反斜杠也将包含在最终的字符串中。

>>> print(r'Let\'s go!')
Let's go!

注意 如果要指定以反斜杠结尾的原始字符串(如以反斜杠结尾的DOS路径),以下两种方式都不对。

>>> print(r'C:\Program Files\foo\bar\')
  Cell In [5], line 1
    print(r'C:\Program Files\foo\bar\')
          ^
SyntaxError: unterminated string literal (detected at line 1)
>>> print(r'C:\Program Files\foo\bar\\')
C:Program Filesfoobar\

该如何办呢?下面是一个简单的示例:

>>> print(r'C:\Program Files\foo\bar' '\\')
C:Program Filesfoobar

请注意,指定原始字符串时,可使用单引号或双引号将其括起,还可使用三引号将其括起。

1.4.2. Unicode编码

Python字符串使用Unicode编码来表示文本。大致而言,每个Unicode字符都用一个码点(code point)表示, 而码点是Unicode标准给每个字符指定的数字。这让你能够以任何现代软件都能识别的方式表示129个文字系统中的12万个以上的字符。 当然,鉴于计算机键盘不可能包含几十万个键,因此有一种指定Unicode字符的通用机制: 使用16或32位的十六进制字面量(分别加上前缀 \u\U )或者使用字符的Unicode名称( \N{name} )。

>>> "\u00C6"
'Æ'
>>> "\U0001F60A"
'😊'
>>> "This is a cat: \N{Cat}"
'This is a cat: 🐈'

Unicode的理念很简单,却带来了一些挑战,其中之一是编码问题。在内存和磁盘中,所有对象都是以二进制数字(0和1)表示的(这些数字每8个为一组,即1字节),字符串也不例外。在诸如C等编程语言中,这些字节完全暴露,而字符串不过是字节序列而已。为与C语言互操作 以及将文本写入文件或通过网络套接字发送出去,Python提供了两种类似的类型:不可变的bytes 和可变的bytearray。如果需要,可直接创建bytes对象(而不是字符串),方法是使用前缀b:

>>> b'Hello, world!'
b'Hello, world!'

然而,1字节只能表示256个不同的值,离Unicode标准的要求差很远。Python bytes字面量只 支持ASCII标准中的128个字符,而余下的128个值必须用转义序列表示,如 \xf0 表示十六进制值 0xf0(即 240 )。

唯一的差别好像在于可用的字母表规模,但实际上并非完全如此。乍一看,好像ASCII和 Unicode定义的都是非负整数和字符之间的映射,但存在细微的差别:Unicode码点是使用整数定 义的,而ASCII字符是使用对应的数及其二进制编码定义的。这一点好像无关紧要,原因之一是 整数0~255和8位二进制数之间的映射是固定的,几乎没有任何机动空间。问题是超过1字节后, 情况就不那么简单了:直接将每个码点表示为相应的二进制数可能不再可行。这是因为不仅存在 字节顺序的问题(即便对整数值进行编码,也会遇到这样的问题),而且还可能浪费空间:如果 对于每个码点都使用相同数量的字节进行编码,就必须考虑到文本可能包含安那托利亚象形文字 或皇家亚兰字母。有一种Unicode编码标准是基于这种考虑的,它就是UTF-32(32位统一编码转换格式,Unicode Transformation Format 32 bits),但如果你主要处理的是使用互联网上常见语言书写的文本,那么使用这种编码标准将很浪费空间。

然而,有一种非常巧妙的替代方式:不使用全部32位,而是使用变长编码,即对于不同的字 符,使用不同数量的字节进行编码。这种编码方式主要出自计算机先锋Kenneth Thompson之手。 通过使用这种编码,可节省占用的空间,就像摩尔斯码使用较少的点和短线表示常见的字母,从 而减少工作量一样。具体地说,进行单字节编码时,依然使用ASCII编码,以便与较旧的系统兼 容;但对于不在这个范围内的字符,使用多个字节(最多为6个)进行编码。下面来使用ASCII、 UTF-8和UTF-32编码将字符串转换为bytes。

>>> "Hello, world!".encode("ASCII")
b'Hello, world!'
>>> "Hello, world!".encode("UTF-8")
b'Hello, world!'
>>> "Hello, world!".encode("UTF-32")
b'xffxfex00x00Hx00x00x00ex00x00x00lx00x00x00lx00x00x00ox00x00x00,x00x00x00 x00x00x00wx00x00x00ox00x00x00rx00x00x00lx00x00x00dx00x00x00!x00x00x00'

从中可知,使用前两种编码的结果相同,但使用最后一种编码的结果长得多。再来看一个示例:

>>> len("How long is this?".encode("UTF-8"))
17
>>> len("How long is this?".encode("UTF-32"))
72

只要字符串包含较怪异的字符,ASCII和UTF-8之间的差别便显现出来了:

>>> "Hællå, wørld!".encode("ASCII")
---------------------------------------------------------------------------

UnicodeEncodeError                        Traceback (most recent call last)

Cell In [22], line 1
----> 1 "Hællå, wørld!".encode("ASCII")


UnicodeEncodeError: 'ascii' codec can't encode character '\xe6' in position 1: ordinal not in range(128)

斯堪的纳维亚字母没有对应的ASCII编码。 如果必须使用ASCII编码(这样的情况肯定会遇到),可向 encode 提供另一个实参,告诉它如何处理错误。 这个参数默认为 strict ,但可将其指 定为其他值,以忽略或替换不在 ASCII 表中的字符。

>>> "Hællå, wørld!".encode("ASCII", "ignore")
b'Hll, wrld!'
>>> "Hællå, wørld!".encode("ASCII", "replace")
b'H?ll?, w?rld!'
>>> "Hællå, wørld!".encode("ASCII", "backslashreplace")
b'H\xe6ll\xe5, w\xf8rld!'
>>> "Hællå, wørld!".encode("ASCII", "xmlcharrefreplace")
b'Hællå, wørld!'

几乎在所有情况下,都最好使用UTF-8。事实上,它也是 Python3 中默认使用的编码。

>>> "Hællå, wørld!".encode()
b'Hxc3xa6llxc3xa5, wxc3xb8rld!'

这相比于Hello, world!,编码结果要长些;但使用UTF-32编码时,结果一样长。 可将字符串编码为bytes,同样也可将bytes解码为字符串。

>>> b'H\xc3\xa6ll\xc3\xa5, w\xc3\xb8rld!'.decode()
'Hællå, wørld!'

与前面一样,默认编码也是UTF-8。你可指定其他编码,但如果指定的编码不正确,将出现 错误消息或得到一堆乱码。bytes对象本身并不知道使用的是哪种编码,因此你必须负责跟踪这 一点。

可不使用方法encode和decode,而直接创建bytes和str(即字符串)对象,如下所示:

>>> bytes("Hællå, wørld!", encoding="utf-8")
b'Hxc3xa6llxc3xa5, wxc3xb8rld!'
>>> str(b'H\xc3\xa6ll\xc3\xa5, w\xc3\xb8rld!', encoding="utf-8")
'Hællå, wørld!'

这种方法更通用一些,在你不知道类似于字符串或bytes的对象属于哪个类时,使用这种方 法也更管用。一个通用规则是,不要做过于严格的假设。

编码和解码的最重要用途之一是,将文本存储到磁盘文件中。然而,Python提供的文件读写 机制通常会替你完成这方面的工作!只要文件使用的是UTF-8编码,就无需操心编码和解码的问 题。但如果原本正常的文本变成了乱码,就说明文件使用的可能是其他编码。在这种情况下,对 导致这种问题的原因有所了解将大有裨益。

最后,Python还提供了bytearray,它是bytes的可变版。从某种意义上说,它就像是可修改 的字符串——常规字符串是不能修改的。然而,bytearray其实是为在幕后使用而设计的,因此 作为类字符串使用时对用户并不友好。例如,要替换其中的字符,必须将其指定为0~255的值。 因此,要插入字符,必须使用ord获取其序数值(ordinal value)。

>>> x = bytearray(b"Hello!")
>>> x[1] = ord(b"u")
>>> x
bytearray(b'Hullo!')