Python 基础教程(3rd ed)(21):快速上手:基础知识 1.11.4

阅读数:36 2019 年 10 月 26 日 09:44

Python基础教程(3rd ed)(21):快速上手:基础知识 1.11.4

(字符串:长字符串、原始字符串和字节)

有一些独特而有用的字符串表示方式。例如,有一种独特的语法可用于表示包含换行符或反斜杠的字符串(长字符串原始字符串)。对于包含特殊符号的字符串,Python 2 还提供了一种专用的表示语法,结果为 Unicode 字符串。这种语法现在依然管用,但是多余,因为在 Python 3 中,所有的字符串都是 Unicode 字符串。Python 3 还引入了一种新语法,用于表示大致相当于老式字符串的字节对象。你将看到,在处理 Unicode编码方面,这种对象依然扮演着重要的角色。

  1. 长字符串

    要表示很长的字符串(跨越多行的字符串),可使用三引号(而不是普通引号)。

    复制代码
    print('''This is a very long string. It continues here.
    And it's not over yet. "Hello, world!"
    Still here.''')
    

    还可使用三个双引号,如"""like this"""。请注意,这让解释器能够识别表示字符串开始和结束位置的引号,因此字符串本身可包含单引号和双引号,无需使用反斜杠进行转义。

    提示 常规字符串也可横跨多行。只要在行尾加上反斜杠,反斜杠和换行符将被转义,即被忽略。例如,如果编写如下代码:

    复制代码
    print("Hello, \ world!")
    

    它将打印Hello, world!。这种处理手法也适用于表达式和语句。

    复制代码
    >> 1 + 2 + \
      4 + 5
    2
    >> print \
      ('Hello, world')
    ello, world
    
  2. 原始字符串

    原始字符串不以特殊方式处理反斜杠,因此在有些情况下很有用1。在常规字符串中,反斜杠扮演着特殊角色:它对字符进行转义,让你能够在字符串中包含原本无法包含的字符。例如,你已经看到可使用\n表示换行符,从而像下面这样在字符串中包含换行符:

    1 编写正则表达式时,原始字符串很有用,这将在第 10 章详细介绍。

    复制代码
    >>> print('Hello,\nworld!')
    Hello,
    world!
    

    这通常挺好,但在有些情况下,并非你想要的结果。如果你要在字符串中包含\n呢?例如,你可能要在字符串中包含 DOS 路径 C:\nowhere。

    复制代码
    >>> path = 'C:\nowhere'
    >>> path
    'C:\nowhere'
    

    这好像没问题,但如果将其打印出来,就会出现问题。

    复制代码
    >>> print(path)
    C:
    owhere
    

    这并非你想要的结果,不是吗?那该怎么办呢?可对反斜杠本身进行转义。

    复制代码
    >>> print('C:\\nowhere')
    C:\nowhere
    

    这很好,但对于很长的路径,将需要使用大量的反斜杠。

    复制代码
    path = 'C:\\Program Files\\fnord\\foo\\bar\\baz\\frozz\\bozz'
    

    在这样的情况下,原始字符串可派上用场,因为它们根本不会对反斜杠做特殊处理,而是让字符串包含的每个字符都保持原样。

    复制代码
    >>> print(r'C:\nowhere')
    C:\nowhere
    >>> print(r'C:\Program Files\fnord\foo\bar\baz\frozz\bozz')
    C:\Program Files\fnord\foo\bar\baz\frozz\bozz
    

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

    复制代码
    >>> print(r'Let\'s go!')
    Let\'s go!
    

    另外,原始字符串不能以单个反斜杠结尾。换而言之,原始字符串的最后一个字符不能是反斜杠,除非你对其进行转义(但进行转义时,用于转义的反斜杠也将是字符串的一部分)。根据前一个示例,这一点应该是显而易见的。如果最后一个字符(位于结束引号前面的那个字符)为反斜杠,且未对其进行转义,Python 将无法判断字符串是否到此结束。

    复制代码
    >>> print(r"This is illegal\")
    SyntaxError: EOL while scanning string literal
    

    这合乎情理,但如果要指定以反斜杠结尾的原始字符串(如以反斜杠结尾的 DOS 路径),该如何办呢?本节介绍了大量技巧,应该能够帮助你解决这个问题,但基本技巧是将反斜杠单独作为一个字符串,下面是一个简单的示例:

    复制代码
    >>> print(r'C:\Program Files\foo\bar' '\\')
    C:\Program Files\foo\bar\
    

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

  3. Unicode、bytesbytearray

    Python 字符串使用 Unicode 编码来表示文本。对大多数简单程序来说,这一点是完全透明的,因此如果你愿意,可跳过本节,等需要时再学习这个主题。然而,鉴于处理字符串和文本文件的 Python 代码很多,大致浏览一下本节至少不会有什么坏处。

    大致而言,每个 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 码点和名称,可在网上使用有关该字符的描述进行搜索,也可参阅特定的网站,如 http://unicode-table.com

    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 之手。通过使用这种编码,可节省占用的空间,就像摩尔斯码使用较少的点和短线表示常见的字母,从而减少工作量一样2。具体地说,进行单字节编码时,依然使用 ASCII 编码,以便与较旧的系统兼容;但对于不在这个范围内的字符,使用多个字节(最多为 6 个)进行编码。下面来使用 ASCII、UTF-8 和 UTF-32 编码将字符串转换为bytes

    2 这是一种重要的压缩方法,为多个现代压缩工具使用的霍夫曼编码所采用。

    >>> "Hello, world!".encode("ASCII")
    b'Hello, world!'
    >>> "Hello, world!".encode("UTF-8")
    b'Hello, world!'
    >>> "Hello, world!".encode("UTF-32")
    b'\xff\xfe\x00\x00H\x00\x00\x00e\x00\x00\x00l\x00\x00\x00l\x00\x00\x00o\x00\x00\x00,\x00\
    x00\x00 \x00\x00\x00w\x00\x00\x00o\x00\x00\x00r\x00\x00\x00l\x00\x00\x00d\x00\x00\x00!\x00\
    x00\x00'
    

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

    >>> 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")
    Traceback (most recent call last):
      ...
    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。事实上,它也是默认使用的编码。

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

    这相比于 Hello, world!,编码结果要长些;但使用 UTF-32 编码时,结果一样长。

    可将字符串编码为bytes,同样也可将bytes解码为字符串。

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

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

    可不使用方法encodedecode,而直接创建bytesstr(即字符串)对象,如下所示:

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

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

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

    3 请参见 https://docs.python.org/3/howto/unicode.html

    注意 源代码也将被编码,且默认使用的也是 UTF-8 编码。如果你想使用其他编码(例如,如果你使用的文本编辑器使用其他编码来存储源代码),可使用特殊的注释来指定。

    # -*- coding: encoding name -*-
    

    请将其中的encoding name替换为你要使用的编码(大小写都行),如utf-8latin-1

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

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

评论

发布