第 1 章 数字、字符串和元组

第 1 章 数字、字符串和元组

本章将通过以下实例介绍Python的基本数据类型。

  • 创建有意义的名称和使用变量
  • 使用大整数和小整数
  • 在浮点数、小数和分数之间选择
  • 在真除法和floor除法1之间选择
  • 重写不可变的字符串
  • 使用正则表达式解析字符串
  • 使用"template".format()创建复杂的字符串
  • 通过字符列表创建复杂的字符串
  • 使用键盘上没有的Unicode字符
  • 编码字符串——创建ASCII和UTF-8字节
  • 解码字节——如何根据字节获得正确的字符
  • 使用元组

1floor除法就是向下取整除法。向上取整除法是ceiling。——译者注

1.1 引言

本章将介绍Python对象的一些核心类型。我们将讨论不同类型的数字以及字符串和元组的使用方法。它们是最简单的Python数据类型,因此会首先介绍。后面的章节将探讨数据集合。

本章大部分实例假定你对Python 3有基本的了解,主要介绍如何使用Python提供的数字、字符串和元组等基本内置类型。Python拥有丰富的数字类型和两个除法运算符,因此需要仔细研究它们之间的区别。

使用字符串时,有几个常见的操作非常重要。本章将探讨操作系统文件所使用的字节和Python所使用的字符串之间的区别,以及如何充分利用Unicode字符集的强大功能。

本章将在Python的交互式解释器中演示实例,这种模式有时也称为REPL(read-eval-print loop,“读取-求值-输出”循环)。后面的章节将仔细研究脚本文件的编写。这样做的目的是鼓励交互式探索,因为它是学习语言的极佳方式。

1.2 创建有意义的名称和使用变量

如何确保程序易于理解呢?要编写富有表现力的代码,一个核心要素就是使用有意义的名称。但什么是有意义的呢?在本实例中,我们将回顾一些创建有意义的Python名称的通用规则。

我们还将介绍Python的一些不同形式的赋值语句,如用同一条语句为多个变量赋值。

1.2.1 准备工作

创建名称的核心问题是:被命名的是什么?对于软件,我们需要一个描述被命名对象的名称。显然,像x这样的名称不是很有描述性,它似乎并不指向实际的事物。

模糊的非描述性名称在一些程序设计中很常见,令人十分痛苦。当使用它们时,无助于其他人理解我们的程序。描述性名称则一目了然。

在命名时,区分解决方案域和问题域(真正想要解决的问题)也很重要。解决方案域包括Python、操作系统和互联网的技术细节。不需要深入的解释,任何人在阅读代码时都可以看到解决方案。然而,问题域可能因技术细节而变得模糊。我们的任务是使问题清晰可见,而精心挑选的名称将对此有所帮助。

1.2.2 实战演练

首先看看如何命名,然后再学习如何为对象分配名称。

  1. 明智地选择名称

    在纯技术层面上,Python名称必须以字母开头。它们可以包括任意数量的字母、数字和下划线。因为Python 3基于Unicode,所以字母不限于拉丁字母。虽然通常使用拉丁字母A~Z,但这不是必须遵循的规定。

    当创建一个描述性变量时,我们需要创建既具体又能表达程序中事物之间关系的名称。一种广泛使用的命名技巧就是创建“从特殊到一般”这种风格的长名称。

    选择名称的步骤如下。

    (1) 名称的最后一部分是事物的广义概要。有时候,仅此一部分就能满足命名的需要,靠上下文提供其余的信息。稍后将介绍一些典型的广义概要的类别。

    (2) 在应用程序或问题域周围使用前缀限定名称。

    (3) 如有必要,使用更精确和专用的前缀,以阐明它与其他类、模块、包、函数和其他对象的区别。对前缀有疑问时,回想一下域名的工作原理。例如,mail.google.com这个名称表明了从特殊到一般的三个级别。三个级别的命名并不罕见,我们经常采用这种命名方法。

    (4) 根据在Python中的使用方法来命名。需要命名的事物有三大类,如下所示。

    • :类的名称能够概述类中的所有对象。这些名称通常使用大驼峰命名法CapitalizedCamelCase)。类名的第一个字母大写,强调它是一个类,而不是类的实例。类通常是一个通用的概念,很少用于描述有形的事物。
    • 对象:对象的名称通常使用蛇底命名法snake_case)。名称全部小写,单词之间使用多个下划线连接。在Python中,一切皆是对象,包括变量、函数、模块、包、参数、对象的属性、类的方法等。
    • 脚本和模块文件:这些文件是Python看到的真正的操作系统资源。因此,文件名应遵循Python对象的约定,使用字母、下划线并以 .py扩展名结尾。单从技术上说,你可天马行空地设置文件名。但是,不遵循Python规则的文件名可能难以用作模块或包的名称。

    如何选择名称中广义类别的那一部分呢?通用类别取决于讨论的是事物还是事物的属性。虽然世界上有很多事物,但我们仍然可以创建一些有用的广义分类,例如文档、企业、地点、程序、产品、过程、人、资产、规则、条件、植物、动物、矿物等。

    然后可以用修饰语来限定这些名称:

    FinalStatusDocument
    ReceivedInventoryItemName

    第一个示例是Document类,我们通过添加一个前缀对其进行了略微的限定,即StatusDocument,又通过将其命名为FinalStatusDocument来进一步限定。第二个示例是Name类,我们通过详细说明它是一个ReceivedInventoryItemName来对其进行限定。该示例需要一个4个级别的名称来阐明。

    对象通常具有特性(property)或者属性(attribute)。它们应当是完整名称的一部分,可以根据其表示的信息类型进行分解,如数量、代码、标识符、名称、文本、日期、时间、日期时间、图片、视频、声音、图形、值、速率、百分比、尺寸等。

    命名的思路就是把狭义、详细的描述放在最前面,把宽泛的信息放在最后:

    measured_height_value
    estimated_weight_value
    scheduled_delivery_date
    location_code

    在第一个示例中,height限定了更一般的表示术语value,而measured_height_value做了进一步限定。通过这个名称,可以思考一下其他与hight相关的变体。类似的思想也适用于weight_valuedelivery_datelocation_code。这些名称都有一个或者两个限定前缀。

     需要避免的情况

    切勿使用经过编码的前缀或后缀去描述详细的技术信息。不要使用f_measured_height_value这样的名称,其中f可能指的是浮点数。这种命名方法通常被称为匈牙利命名法(Hungarian Notation)。像measured_height_value这样的变量可以是任意数字类型,Python会做所有必要的转换。技术性修饰对于代码阅读者并没有多大帮助,因为类型说明可能造成误导甚至错误。

    不要浪费太多的精力使名称看起来属于哪个类别。不要使用SpadesCardSuitClubsCardSuit这样的名称。Python有许多种命名空间,包括包、模块和类,命名空间对象会把相关的名称收集起来。如果将这些名称添加到CardSuit类中,就可以使用CardSuit.Spades,以类作为命名空间来区分其他相似的名称。

  2. 为对象分配名称

    Python没有使用静态变量定义。当把名称分配给对象时,就会创建变量。把对象看作处理过程的核心非常重要,变量有点像标识对象的标签。使用基本赋值语句的方法如下。

    (1) 创建对象。在许多示例中,对象以字面量的形式创建。我们使用355113作为Python中整数对象的字面量表示,也可以使用FireBrick表示字符串,或使用(178,34,34)表示元组。

    (2) 编写如下类型的语句:变量 = 对象。例如:

    >>> circumference_diameter_ratio = 355/113
    >>> target_color_name = 'FireBrick'
    >>> target_color_rgb = (178, 34, 34)

    我们创建了一些对象并把它们赋值给了变量。第一个对象是数值计算的结果,接下来的两个对象是简单的字面量。对象通常由包含函数或类的表达式创建。

    上面的基本赋值语句并不是唯一的赋值方式,还可以使用链式赋值的方式,将一个对象分配给多个变量,例如:

    >>> target_color_name = first_color_name = 'FireBrick'

    上例为同一个字符串对象创建了两个名称。可以通过检查Python使用的内部ID值来确认这两个对象是否为同一个对象:

    >>> id(target_color_name) == id(first_color_name)
    True

    结果表明,这两个对象的内部ID值是相同的。

     相等测试使用==,简单赋值使用=

    随后介绍数字和集合时将会说明结合运算符进行赋值的方法。例如:

    >>> total_count = 0
    >>> total_count += 5
    >>> total_count += 6
    >>> total_count
    11

    我们通过运算符进行了增量赋值。total_count + = 5total_count = total_count + 5是等价的。增量赋值的优点在于简化了表达式。

1.2.3 工作原理

本实例创建名称的方法遵循如下模式:狭义的、更具体的限定符放在前面,更宽泛的、不太特定的类别放在最后。这种方法遵守用于域名和电子邮件地址的通用约定。

例如,域名mail.google.com包括特定的服务、更通用的企业和最后的非常通用的域,这遵循了从窄到宽的原则。

又如,service@packtpub.com以具体的用户名开始,然后是更通用的企业,最后是非常通用的域。甚至用户名(PacktPub)也是一个具有两部分的名称,包括限定的企业名称(Packt),以及更广泛的行业[Pub,“Publishing”(出版)的简写,而不是“Public House”(酒吧)的简写]。

赋值语句是为对象命名的唯一途径。通过前面的实例,我们注意到,同一个底层对象可以有两个名称,但现在还不清楚这种特性有什么用处。第4章将介绍为一个对象分配多个名称的一些有趣后果。

1.2.4 补充内容

我们将在所有实例中使用描述性名称。

 没有遵循这种模式的现有软件应当保持现状。一般而言,最好与遗留软件保持一致,而不是强加新规则,即使新规则更好。

几乎每个示例都涉及变量赋值。变量赋值是有状态的面向对象编程的核心。

第6章将讨论类和类名,第13章将讨论模块。

1.2.5 延伸阅读

描述性命名是一个正在研讨的主题,涉及两个方面——语法和语义。Python语法的设想起始于著名的PEP-8(Python Enhancement Proposal number 8)。PEP-8建议使用CamelCasesnake_case命名风格。

此外,务必进行以下操作:

>>> import this

这有助于领悟Python的设计哲学。

 有关语义的信息,请参阅遗留的UDEF和NIEM命名和设计规则标准(http://www.opengroup.org/udefinfo/AboutTheUDEF.pdf)。有关元数据和命名的详细信息,请参阅ISO11179(https://en.wikipedia.org/wiki/ISO/IEC_11179)。

1.3 使用大整数和小整数

许多编程语言区分整数、字节和长整数,有些编程语言还存在有符号整数无符号整数的区别。如何将这些概念与Python联系起来呢?

答案是“不需要”。Python以统一的方式处理所有类型的整数。对于Python,从几个字节到数百位的巨大数字,都是整数。

1.3.1 准备工作

假设我们需要计算一些非常大的数字,例如,计算一副52张的扑克牌的排列数。52! = 52 × 51 × 50 × … × 2 × 1,这是一个非常大的数字。可以在Python中实现这个运算吗?

1.3.2 实战演练

别担心!Python表现得好像有一个通用的整数类型,涵盖了所有整数,从几个字节到填满所有内存的整数。正确使用整数的步骤如下。

(1) 写下你需要的数字,比如一些小数字:355,113。实际上,数字的大小没有上限。

(2) 创建一个非常小的值——单个字节,如下所示:

>>> 2
2

或者使用十六进制:

>>> 0xff
255

后面的实例中将讨论只含有一个值的字节序列:

>>> b'\xfe'
b'\xfe'

严格说来,这不是一个整数。它有一个前缀b',这表明它是一个一字节序列(1-byte sequence)。

(3) 通过计算创建一个很大的数字。例如:

>>> 2 ** 2048
323...656

该数字有617个数位,这里并没有完全显示。

1.3.3 工作原理

Python内部使用两种数字,两者之间的转换是无缝且自动的。

对于较小的数字,Python通常使用4字节或8字节的整数值。细节隐藏在CPython的内核中,并依赖于构建Python的C编译器。

对于超出sys.maxsize的较大数字,Python将其切换到大整数——数字(digit)序列。在这种情况下,一位数字通常意味着30位(bit)的值。

一副52张的扑克牌有多少种排列方法?答案是52!\approx8\times10^{67} 。我们将使用math模块的factorial函数计算这个大整数,如下所示:

>>> import math
>>> math.factorial(52)
80658175170943878571660636856403766975289505440883277824000000000000

这些巨大的数字工作得非常完美!

计算52! 的第一部分(从52 × 51 × 50 × …一直到约42)可以完全使用较小的整数来执行。在此之后,其余的计算必须切换到大整数。我们看不到切换过程,只能看到结果。

通过下面的示例可以了解整数内部的一些细节。

>>> import sys
>>> import math
>>> math.log(sys.maxsize, 2)
63.0
>>> sys.int_info
sys.int_info(bits_per_digit = 30, sizeof_digit = 4)

sys.maxsize的值是小整数中的最大值。我们通过计算以2为底的对数来说明这个数字需要多少位。

通过计算可知,Python使用63位值来表示小整数。小整数的范围是从-2^{64} 2^{63}-1。在此范围之外,使用大整数。

通过sys.int_info的值可知,大整数是使用30位的数字序列,每个数字占用4字节。

像52! 这样比较大的值,由8个上述30位的数字组成。一个数字需要30位来表示可能有些令人困惑。以用10个符号表示十进制(base 10)的数字为例,我们需要2**30个不同的符号来表示这些大数字的每位数。

涉及多个大整数值的计算可能会消耗相当大的内存空间。小数字怎么办呢? Python如何跟踪大量的小数字,如1和0?

对于常用的数字(-5到256),Python实际上创建了一个私有的对象池来优化内存管理。你可以在检查整数对象的id()值时得到验证。

>>> id(1)
4297537952
>>> id(2)
4297537984
>>> a = 1 + 1
>>> id(a)
4297537984

我们显示了整数1和整数2的内部id。当计算a的值时,结果对象与对象池中的整数2对象是同一个对象。

当你练习这个示例时,id()值可能跟示例不同。但是,在每次使用值2时,将使用相同的对象。在我的笔记本电脑上,对象2id等于4297537984。这种机制避免了在内存里大量存放对象2的副本。

这里有个小技巧,可以看出一个数字到底有多大。

>>> len(str(2 ** 2048))
617

通过一个计算得到的数字创建一个字符串,然后查询字符串的长度。结果表明,这个数字有617个数位。

1.3.4 补充知识

Python提供了一组丰富的算术运算符:+-*///%**///用于除法,1.5节将讨论这些运算符。**将执行幂运算。

对于位处理,还有其他一些运算符,比如&^|<<>>。这些运算符在整数的内部二进制表示上逐位操作。它们分别计算二进制与二进制异或二进制或左移右移

虽然这些运算符也同样适用于大整数,但是逐位操作对于大整数来说并没有实际意义。一些二进制文件和网络协议会要查看数据的单个字节中的位。

可以通过bin()函数查看应用这些运算符的运行结果。示例如下:

>>> xor = 0b0011 ^ 0b0101
>>> bin(xor)
'0b110'

先使用0b00110b0101作为两个位串。这有助于准确说明数字的二进制表示。然后将异或(^)运算符应用于这两个位序列。最后使用bin()函数查看位串形式的结果。可以通过结果仔细观察各个位,了解操作符的实际功能。

可以把一个字节分解为若干部分。假设我们要将最左边的2个位与其他6个位分开,其中一种方法是使用位操作(bit-fiddling)表达式,例如:

>>> composite_byte = 0b01101100
>>> bottom_6_mask = 0b00111111
>>> bin(composite_byte >> 6)
'0b1'
>>> bin(composite_byte & bottom_6_mask)
'0b101100'

这里先定义了一个composite_byte,其中最高有效的2位为01,最低有效的6位为101100。再使用>>移位运算符将composite_byte的值右移6个位置,去除最低有效位并保留2个最高有效位。然后使用&运算符和掩码来进行操作。掩码中值为1的位,在结果中保留对应位置的值;掩码中值为0的位,结果中对应位置的值被设置为0。

1.3.5 延伸阅读

1.4 在浮点数、小数和分数之间选择

Python提供了多种处理有理数和无理数近似值的方法。3种基本选择如下:

  • 浮点数
  • 小数
  • 分数

有这么多种选择,那么怎样选择才合适呢?

1.4.1 准备工作

确定我们的核心数学期望值很重要。如果不确定已拥有的数据类型或者想要得到的结果,真的不应该开始编码。我们需要退一步,用铅笔和纸来演算一下。

除了整数,在数学中涉及的数字还有3种。

(1) 货币:如美元、美分或欧元。货币通常具有固定的小数位数。另外还有很多舍入规则,例如,可以用这些规则确定 $2.95的7.25% 是多少美分。2

2货币的最小单位一般为0.01元。——译者注

(2) 有理数或分数:使用美制单位的英尺和英寸,或在烹饪中使用杯和盎司进行测量时,经常需要使用分数。把一个8人量的食谱缩减为5人量时,要用5/8作为缩放因子进行分数运算。如何将这种方法应用到2/3杯米,并得到适用于厨房量具的测量值呢?

(3) 无理数:包括所有其他类型的计算。必须注意,数字计算机只能逼近这些无理数,而我们偶尔会看到近似值的一些奇怪现象。浮点近似值运算非常快,但有时会出现截断问题。

当计算涉及前两种数字时,应当避免使用浮点数。

1.4.2 实战演练

本实例将分别讨论这3种数字。首先讨论货币值计算。然后讨论有理数计算,以及无理数或浮点数计算。最后讨论这些类型之间的显式转换。

  1. 货币值计算

    在处理货币值时,应当坚持使用decimal模块。如果使用Python内置的浮点数,将会遇到数字的舍入和截断问题。

    (1) 为了处理货币值,首先从decimal模块导入Decimal类。

    >>> from decimal import Decimal

    (2) 从字符串或整数创建Decimal对象。

    >>> from decimal import Decimal
    >>> tax_rate = Decimal('7.25')/ Decimal(100)
    >>> purchase_amount = Decimal('2.95')
    >>> tax_rate * purchase_amount
    Decimal('0.213875')

    tax_rate由两个Decimal对象构建,其中一个基于字符串,另一个基于整数。我们可以直接使用Decimal('0.0725'),而不显式地执行除法。

    结果稍微大于$0.21,因为我们计算出了小数位的全部数字。

    (3) 如果通过浮点数创建Decimal对象,那么将得到异常的浮点近似值。应当避免混用 Decimalfloat。为了舍入到最近的便士(penny),创建一个penny对象。

    >>> penny = Decimal('0.01')

    (4) 使用penny对象量化数据。

    >>> total_amount = purchase_amount + tax_rate * purchase_amount
    >>> total_amount.quantize(penny)
    Decimal('3.16')

    上述示例演示了如何使用默认的ROUND_HALF_EVEN舍入规则。

    舍入规则有很多种,Decimal模块提供了所有舍入规则。例如:

    >>> import decimal
    >>> total_amount.quantize(penny, decimal.ROUND_UP)
    Decimal('3.17')

    本示例显示了使用另一种不同的舍入规则的结果。

  2. 分数计算

    当计算中含有精确分数值时,可以使用fractions模块。该模块提供了便于使用的有理数。处理分数的流程如下。

    (1) 从fractions模块导入Fraction类。

    >>> from fractions import Fraction

    (2) 由字符串、整数或整数对创建Fraction对象。如果由浮点数创建Fraction对象,可能会遇到浮点近似值的异常现象。当分母是2的幂时,一切正常。

    >>> from fractions import Fraction
    >>> sugar_cups = Fraction('2.5')
    >>> scale_factor = Fraction(5/8)
    >>> sugar_cups * scale_factor
    Fraction(25, 16)

    我们从字符串2.5创建了第一个分数,从浮点计算5/8创建了第二个分数。因为分母是2的幂,所以计算结果非常准确。

    25/16——结果是一个看起来很复杂的分数,那么它的最简分数是多少呢?

    >>> Fraction(24, 16)
    Fraction(3, 2)

    结果表明,我们使用大约一杯半的米就可以完成5人量的食谱。

  3. 浮点近似值

    Python的内置浮点(float)类型能够表示各种各样的值。对于是否使用浮点值,选择的关键在于浮点值通常涉及近似值。在某些情况下,特别是在做涉及2的幂的除法时,结果是一个精确的分数。在其他情况下,浮点值和分数值之间可能存在细小的差异,这反映了浮点数的实现与无理数的数学理想之间的差异。

    (1) 要使用浮点数,经常需要舍入值来使它们看起来合理。所有浮点计算的结果都是近似值。

    >>>(19/155) * (155/19)
    0.9999999999999999

    (2) 上面的值在数学上应该为1。由于float使用的是近似值,所以结果并不精确。虽然这个结果与1相差不多,但仍然错了。当进行适当的舍入时,这个值会更有意义。

    >>> answer =(19/155) * (155/19)
    >>> round(answer, 3)
    1.0

    (3) 认识误差项。在本例中,我们知道确切的答案,所以可以将计算结果与已知的正确答案进行比较。下面的示例给出的通用误差项适用于所有浮点数。

    >>> 1-answer
    1.1102230246251565e-16

    对于大多数浮点误差,典型值约为10^{-16}。Python有一些聪明的规则,有时通过自动舍入隐藏这个错误。但是,对于本示例,错误并没有隐藏。

    这是一个非常重要的推论。

     不要比较浮点值是否完全相等。

    在浮点数之间使用精确的==测试时,如果近似值相差一个位,代码就会出现问题。

  4. 数字的类型转换

    可以使用float()函数从其他类型的值创建一个float值。例如:

    >>> float(total_amount)
    3.163875
    >>> float(sugar_cups * scale_factor)
    1.5625

    在第一个示例中,我们将Decimal值转换为float值。在第二个示例中,我们将Fraction值转换为float值。

    正如刚刚看到的,我们永远不想将float转换为DecimalFraction

    >>> Fraction(19/155)
    Fraction(8832866365939553, 72057594037927936)
    >>> Decimal(19/155)
    Decimal('0.12258064516129031640279123394066118635237216949462890625')

    在第一个示例中,我们在整数之间进行计算,创建了一个具有已知截断问题的float值。当我们从截断的float值创建一个Fraction时,得到的是一些暴露了截断问题的数字。

    类似地,第二个示例从float创建了 Decimal值。

1.4.3 工作原理

对于数字类型,Python提供了多种运算符:+-*///%**。这些运算符用于加法、减法、乘法、真除法、截断除法、取模和幂运算。1.5节将讨论其中的两个除法运算符。

Python擅长各种数字类型之间的转换。我们可以混合使用整数(int)和浮点数(float),整数将被转换为浮点数,以提供最准确的答案。类似地,还可以混合使用整数(int)和分数(Fraction),结果将是分数(Fractions)。我们也可以混合使用整数(int)和小数(Decimal)。但是,不能随便混合使用小数(Decimal)与浮点数(float),或小数(Decimal)与分数(Fraction),在这样操作之前,需要进行显式转换。

 必须注意的是,float值是真正的近似值。虽然Python语法允许将数字写为小数值,但它们在Python内部并不是按小数处理的。

我们可以使用普通的十进制数值在Python中编写如下值:

>>> 8.066e + 67
8.066e + 67

在内部使用的实际值将包含上述十进制值的一个二进制近似值。

该示例(8.066e + 67)中的内部值为:

>>> 6737037547376141/2 ** 53 * 2 ** 226
8.066e + 67

分子是一个大数字,6737037547376141;分母总是2^{53} 。由于分母是固定的,因而所得到的分数只能有53位有意义的数据。由于53位之外的位不可用,因此值可能会被截断。这导致了我们的理想化抽象和实际数字之间的微小差异。指数(2^{226} )需要将分数缩放到适当的范围。

在数学上,即6737037547376141*2^{226}/2^{53}

可以使用math.frexp()查看数字的内部细节:

>>> import math
>>> math.frexp(8.066E + 67)
(0.7479614202861186, 226)

结果的两个部分分别称为尾数(mantissa)和指数(exponent)。如果将尾数乘以2^{53} ,那么将得到一个整数,这个整数是二进制分数的分子。

 前面提到的误差项与该值非常地匹配:10^{16}\approx2^{-53}

与内置的float不同,Fraction是两个整数值的精确比率。正如1.3节所示,Python中的整数可能非常大。我们可以创建包含具有大量数位的整数的比率,并且不受固定分母的限制。

类似地,Decimal值基于非常大的整数值和用于确定小数点位置的缩放因子。这些数字可以是巨大的,不会有特殊的表示问题。

 为什么要使用浮点数?原因有两个

并不是所有可计算的数字都可以表示为分数。这就是数学家引入(或者可能是发现)无理数的原因。内置的float类型与无理数的数学抽象非常接近。例如,像\sqrt{2} 这样的值就不能表示为分数。

此外,浮点值运算非常快。

1.4.4 补充知识

Python的math模块包含许多用于处理浮点值的专用函数。该模块包括了常用的函数,如平方根、对数和各种三角函数,还包括其他一些函数,如伽玛函数、阶乘函数和高斯误差函数。

math模块也包含了一些可以精确计算浮点数的函数。例如,math.fsum()函数将比内置sum()函数更加周密地计算浮点和。math.fsum()函数很少出现近似值问题。

还可以使用math.isclose()函数比较两个浮点值是否接近相等:

>>> (19/155)*(155/19) == 1.0
False
>>> math.isclose((19/155)*(155/19), 1)
True

该函数提供了一种正确比较浮点数的方法。

Python还提供了复数数据类型。复数由实部和虚部组成。在Python中,3.14 + 2.78j代表复数3.14+2.78\sqrt{-1}。Python可以在浮点数和复数之间进行轻松的转换。Python提供了一组常用的复数运算符。

为了更好地支持复数,Python内置了cmath包。例如,cmath.sqrt()函数将返回一个复数值,而不是在求负数的平方根时抛出异常。示例如下:

>>> math.sqrt(-2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: math domain error
>>> cmath.sqrt(-2)
1.4142135623730951j

在操作复数时,离不开cmath包。

1.4.5 延伸阅读

1.5 在真除法和floor除法之间选择

Python提供了两种除法运算符。本实例将介绍这两种运算符以及它们适用的情景,还将介绍Python除法规则以及如何对整数值进行除法运算。

1.5.1 准备工作

除法运算有几种常见的使用场景。

  • div-mod对:我们需要两部分值——商和余数。当对数值进行数制转换时,或者把秒数转换为小时、分钟和秒时,会执行div-mod除法。我们不想要确切的小时数,只是想要一个截断的小时数,余数转换为分钟和秒。
  • 真实(true)值:典型的浮点值——商的良好近似值。例如,如果计算一些测量数据的平均值,那么我们通常希望结果是浮点值,即使输入值都是整数。
  • 合理的分数值:当我们使用英尺、英寸和杯等美国单位时,常常需要这种值。为此,应当使用Fraction类。当使用Fraction对象时,总是得到确切的答案。

我们首先需要确定适用哪种情况,然后就知道该使用哪种除法运算符了。

1.5.2 实战演练

我们将分别讨论这三种情况。首先讨论截断的floor除法,然后讨论真正的浮点除法,最后讨论分数的除法。

  1. floor除法

    在做div-mod类计算时,可以使用floor除法运算符(//)和取模运算符(%)。或者也可以使用divmod()函数。

    (1) 将秒数除以3600得到小时数,模数或余数可以分别转换为分钟数和秒数。

    >>> total_seconds = 7385
    >>> hours = total_seconds//3600
    >>> remaining_seconds = total_seconds % 3600

    (2) 将步骤(1)剩余的秒数除以60得到分钟数,余数是小于60的秒数。

    >>> minutes = remaining_seconds//60
    >>> seconds = remaining_seconds % 60
    >>> hours, minutes, seconds
    (2, 3, 5)

    使用divmod()函数的示例如下。

    (1) 同时计算商和余数。

    >>> total_seconds = 7385
    >>> hours, remaining_seconds = divmod(total_seconds, 3600)

    (2) 再次计算商和余数。

    >>> minutes, seconds = divmod(remaining_seconds, 60)
    >>> hours, minutes, seconds
    (2, 3, 5)
  2. 真除法

    真值除法计算的结果是浮点近似值。例如,7386秒是多少小时?使用真除法运算符进行除法运算:

    >>> total_seconds = 7385
    >>> hours = total_seconds / 3600
    >>> round(hours, 4)
    2.0514

     我们提供了两个整数值,但得到了一个精确的浮点数结果。与以前使用浮点值的实例相一致,我们取整了结果,以避免出现微小的误差值。

    这种真除法是Python 3的特性,本实例随后的部分将介绍Python 2的除法运算符。

  3. 有理分数计算

    可以用Fraction对象和整数做除法。这将使结果是一个数学上精确的有理数。

    (1) 创建至少一个Fraction值。

    >>> from fractions import Fraction
    >>> total_seconds = Fraction(7385)

    (2) 在计算中使用Fraction值,任何整数都将被提升到分数。

    >>> hours = total_seconds / 3600
    >>> hours
    Fraction(1477, 720)

    (3) 如有必要,将确切分数转换为浮点近似值。

    >>> round(float(hours),4)
    2.0514

    我们首先为总秒数创建了一个Fraction对象。在对分数做算术运算时,Python会把所有整数都转换为分数,这种转换意味着数学运算是尽可能精确地完成的。

1.5.3 工作原理

Python 3有两个除法运算符。

  • 真除法运算符/总是试图产生一个浮点数结果,即使这两个操作数是整数。从这个角度来看,真除法运算符是一个不寻常的运算符。其他所有运算符都试图保留数据的类型。当应用于整数时,真除法运算会产生浮点数结果。
  • 截断除法运算符//总是试图产生截断的结果。对于两个整数操作数,结果是截断商。对于两个浮点数操作数,结果是一个截断的浮点数结果。

    >>> 7358.0 // 3600.0
    2.0

默认情况下,Python 2只有一个除法运算符。对于仍在使用Python 2的程序员来说,可以通过以下方法使用这些新运算符:

>>> from __future__ import division

这个导入将会引入Python 3的除法规则。

1.5.4 延伸阅读

1.6 重写不可变的字符串

如何重写不可变的字符串?字符串中的字符不可更改,例如:

>>> title = "Recipe 5: Rewriting, and the Immutable String"
>>> title[8]= ''
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

上面的方法并不能修改字符串,那么怎么才能更改字符串呢?

1.6.1 准备工作

假设有一个字符串:

>>> title ="Recipe 5:Rewriting,and the Immutable String"

我们想做以下两个转换:

  • 移除:之前的那部分字符串;
  • 把标点符号替换为_,将所有字符转换为小写。

因为不能直接替换字符串对象中的字符,所以必须找到一些替代方法。常见的替代方法如下所示。

  • 通过切片字符串和连接字符串来创建新的字符串。
  • 使用partition()方法缩短字符串。
  • 使用replace()方法替换某个字符或子字符串。
  • 将字符串扩展为字符列表,然后再连接为一个字符串。这种方法是1.9节的主题。

1.6.2 实战演练

因为不能在原位更新一个字符串,所以必须用每个修改后的结果替换字符串变量中对应的对象。语句如下所示:

some_string = some_string.method()

甚至可以使用:

some_string = some_string[:chop_here]

我们将讨论关于这个常见主题的一些具体变化。比如,切片字符串,替换字符串中的单个字符,或者应用整体转换(blanket transformation),例如将字符串转换为小写。我们还将研究如何删除字符串中多余的下划线。

  1. 切片字符串

    通过切片缩短字符串的方法如下。

    (1) 查找边界。

    >>> colon_position = title.index(':')

    index函数定位了特定的子字符串并返回该子字符串的位置。如果该子字符串不存在,那么将会抛出异常。title[colon_position] == ':'的结果始终为true

    (2) 选择子字符串。

    >>> discard_text, post_colon_text = title[:colon_position], title[colon_position+1:]
    >>> discard_text
    'Recipe 5'
    >>> post_colon_text
    ' Rewriting, and the Immutable String'

    使用切片符号来显示需要选择的字符串的start:end。利用多重赋值方法,将两个表达式分别赋值给变量discard_textpost_colon_text

    我们也可以使用partition()和手动切片来切片字符串。查找边界并分割字符串。

    >>> pre_colon_text, _, post_colon_text = title.partition(':')
    >>> pre_colon_text
    'Recipe 5'
    >>> post_colon_text
    ' Rewriting, and the Immutable String'

    partition函数返回三项内容:指定字符串左边的子字符串、指定字符串和指定字符串右边的子字符串。我们使用多重赋值将每个对象赋给不同的变量。我们把指定的字符串赋值给_变量,因为这部分结果将被忽略。对于那些必须提供变量的地方,这是一个常见的习惯用法,但我们并不关心如何使用这个对象。

  2. 使用替换更新字符串

    可以使用replace()删除标点符号。使用replace()替换标点符号时,把结果保存回原始变量。在这个示例中原始变量为post_colon_text

    >>> post_colon_text = post_colon_text.replace(' ', '_')
    >>> post_colon_text = post_colon_text.replace(',', '_')
    >>> post_colon_text
    '_Rewriting__and_the_Immutable_String'

    上述代码使用_替换了两种标点符号。利用将在第2章中介绍的for语句,可以把这种方法应用到所有标点符号。

    我们可以迭代所有标点符号:

    >>> from string import whitespace, punctuation
    >>> for character in whitespace + punctuation:
    ...     post_colon_text = post_colon_text.replace(character, '_')
    >>> post_colon_text
    '_Rewriting__and_the_Immutable_String'

    当各种标点符号都已经被替换时,再把最终的字符串赋给变量post_colon_text

  3. 使字符串全部小写

    另一个转换步骤是将字符串更改为全部小写。与前面的示例一样,我们把结果赋给原始变量。使用lower()方法,然后将结果赋给原始变量:

    >>> post_colon_text = post_colon_text.lower()
  4. 删除多余的标点符号

    在许多情况下,可能还需要一些额外的步骤。例如,可以使用strip()删除开头和结尾的_

    >>> post_colon_text = post_colon_text.strip('_')

    在某些情况下,因为有多个连在一起的标点符号,所以将会有多个连续的_。最后一步将是清理多个连续的_

    >>> while '__' in post_colon_text:
    ...     post_colon_text = post_colon_text.replace('__', '_')

    该示例是修改字符串的另一个示例,依赖于将在第2章介绍的while语句。

1.6.3 工作原理

严格来讲,我们无法原位修改字符串,因为字符串的数据结构是不可变的。但是,可以将一个新的字符串赋值给原始变量。这种技术的行为与原位修改字符串相同。

当变量的值被替换时,之前的值不再具有任何引用并且被垃圾回收。可以通过id()函数来追踪每个字符串对象:

>>> id(post_colon_text)
4346207968
>>> post_colon_text = post_colon_text.replace('_','-')
>>> id(post_colon_text)
4346205488

你在实际操作时的ID号可能与示例不同。重要的是,原来赋给post_colon_text的字符串对象有一个ID号,而赋给post_colon_tex的新字符串对象的ID号与原来的不同。这说明它是一个新的字符串对象。

当旧的字符串不再被引用时,将在内存中被自动删除。

我们利用切片符号(slice notation)分解字符串。切片有两部分:[start:end]。切片始终包含起始索引,从不包含结束索引。字符串索引总是以0作为第一个元素的索引。

 切片中元素的索引从startend-1,这有时被称为半开区间

切片也可以理解为:所有字符的索引i都在{\rm start}\leqslant i<{\rm end}范围内。

我们可以省略起始索引或结束索引,实际上可以同时省略两者。各种可用的切片符号如下。

  • title[colon_position]:对于单个元素,可以使用title.index(':')找到:
  • title[:colon_position]:省略起始索引的切片。从第一个索引开始,索引为0
  • title[colon_position+1:]:省略结束索引的切片。它结束于字符串的末端,或者说第len(title)个索引。
  • title[:]:由于同时省略了起始索引和结束索引,因此这个切片表示整个字符串。实际上,这个切片是整个字符串的一个副本(copy)。这是一种快速简单的复制字符串的方法。

1.6.4 补充知识

在Python中,可以索引像字符串这样的集合(collection)的功能还有很多种。正常的索引从字符串的左端开始索引,0为起始索引。另一种索引方法采用从字符串右端起始的负索引。

  • title[-1]title的最后一个字符g
  • title[-2]title的倒数第二个字符n
  • title[-6:]title的最后6个字符String

在Python中,还有很多方法可以从字符串中选取部分内容。

Python提供了许多种修改字符串的方法。“Python标准库”的4.7节说明了多种变换方法。字符串方法有三大类:查找字符串、解析字符串和转换字符串。例如,isnumeric()方法可以显示一个字符串是否全部为数字。示例如下:

>>> 'some word'.isnumeric()
False
>>> '1298'.isnumeric()
True

解析字符串的方法如本节介绍的partition()方法,转换字符串的方法如本节介绍的lower()方法。

1.6.5 延伸阅读

  • 1.9节将介绍按列表方式修改字符串的方法。
  • 有时数据只是一个字节流。为了使其有意义,需要将其转换为字符。1.12节将讨论这个主题。

1.7 使用正则表达式解析字符串

如何分解复杂的字符串?如果有复杂、棘手的标点符号怎么办?或者更糟,没有标点符号,但必须依靠数字的模式来找到有意义的信息,该怎么办?

1.7.1 准备工作

分解复杂字符串最简单的方法是将字符串归纳为模式(pattern),然后编写描述该模式的正则表达式。

正则表达式能够描述的模式是有限的。当遇到HTML、XML或JSON等深层嵌套的文档时,可能就不能够正常使用正则表达式了。

re模块包含了创建和使用正则表达式所需的各种类和函数。

假设我们想分离某个食谱网站中的文本。每行内容如下所示:

>>> ingredient = "Kumquat: 2 cups"

我们想分离原料和度量。

1.7.2 实战演练

编写和使用正则表达式的步骤如下。

(1) 概括示例。示例信息可以概括为:

(ingredient words): (amount digits) (unit words)

我们用摘要替换了文本,摘要分为两个部分:信息的含义和信息的表示形式。例如,原料用文字表示,数量用数字表示。

(2) 导入re模块。

>>> import re

(3) 把模式改写为正则表达式(regular expression,RE)标记。

>>> pattern_text =r'(?P<ingredient>\w+):\s+(?P<amount>\d+)\s+(?P<unit>\w+)'

我们替换了模式中的表示形式。例如,用\w+替换words,用\d+替换digits;用\s+替换单个空格,这样可以将一个或多个空格用作标点符号。我们没有处理冒号,因为在正则表达式中,冒号匹配其本身。

对于模式中的每个字段,使用?P<name>作为名称来标识需要提取的数据。我们没有在冒号或空格周围做类似操作,因为不想要这些字符。

正则表达式使用了大量\字符。为了能够在Python中正常工作,我们总是使用原始字符串。r'前缀使Python忽略\,而不是把它们替换为键盘上没有的特殊字符。

(4) 编译模式。

>>> pattern = re.compile(pattern_text)

(5) 使用模式匹配输入文本。如果输入文本匹配模式,将获得一个匹配对象,匹配对象显示了匹配的详细信息。

>>> match = pattern.match(ingredient)
>>> match is None
False
>>> match.groups()
('Kumquat', '2', 'cups')

我们从字符串中提取了一个包含不同字段的元组。1.13节将讨论元组的使用方法。

(6) 从匹配对象中提取已命名的分组。

>>> match.group('ingredient')
'Kumquat'
>>> match.group('amount')
'2'
>>> match.group('unit')
'cups'

正则表达式中的(?P<name>...)标识了每个分组。

1.7.3 工作原理

正则表达式可以描述多种字符串模式。

前面已经介绍了一些字符类:

  • \w匹配任意字母或数字(a到z,A到Z,0到9);
  • \d匹配任意十进制数字;
  • \s匹配任意空格或制表符。

这些类还有相反的类:

  • \W匹配任意不是字母或数字的字符;
  • \D匹配任意不是数字的字符;
  • \S匹配任意不是某种空格或制表符的字符。

许多字符匹配自己。然而,某些字符具有特殊意义,因此我们使用\来区分那些特殊意义。

  • +作为后缀表示匹配一个或多个前面的模式。\d+表示匹配一个或多个数字。要匹配一个普通的+字符,需要使用\+
  • *作为后缀表示匹配零个或多个前面的模式。\w*表示匹配零个或多个字符。要匹配一个*字符,需要使用\*
  • ?作为后缀表示匹配零个或一个前面的表达式。这个字符还在其他地方使用,并且具有略微不同的含义。在(?P<name>...)中,它在()里面,用于定义分组的特殊属性。
  • .表示匹配任意单个字符。要匹配具体的.需要使用\.

可以使用[]将集合中的元素括起来,创建我们自己独特的字符集:

(?P<name>\w+)\s*[=:]\s*(?P<value>.*)

这个表达式使用\w+来匹配任意数量的字母或数字字符。这些字符将被收集到一个名为name的组中。

这个表达式使用\s*来匹配一个可选的空格序列。

这个表达式匹配[=:]集中的任何字符。这个集中的两个字符之一必须存在。

这个表达式再次使用\s*来匹配一个可选的空格序列。

最后,它使用.*匹配字符串中的其他内容。这些内容将被收集到一个名为value的组中。

可以用这个表达式来解析字符串:

size = 12
weight: 14

通过灵活地使用标点符号,可以使程序更易用。我们可以处理任意数量的空格,将=:作为分隔符。

1.7.4 补充知识

复杂的正则表达式可读性较差。可以使用一种Python风格的技巧来提高正则表达式的可读性:

>>> ingredient_pattern = re.compile(
... r'(?P<ingredient>\w+):\s+' # 原料名称直到“:”为止
... r'(?P<amount>\d+)\s+'      # 数量,所有数字直到空格为止
... r'(?P<unit>\w+)'           # 单位,字母数字字符
... )

该技巧利用了3个语法规则:

  • 直到()字符匹配,语句才结束;
  • 相邻的字符串字面量自动连接为一个长字符串;
  • #与该行结尾之间的内容为注释,代码运行时将忽略注释。

我们在正则表达式的重要子句后面编写了Python注释。这样有助于了解代码的功能,也有助于随后诊断问题。

1.7.5 延伸阅读

  • 1.12节。
  • 关于正则表达式和Python正则表达式的图书非常多,比如Mastering Python Regular Expressions

1.8 使用"template".format()创建复杂的字符串

创建复杂字符串与解析复杂字符串在许多方面是截然相反的。通常使用模板以及替换规则将数据转换为更复杂的格式。

1.8.1 准备工作

假设我们有一些需要转换为正确格式的消息数据。原始数据如下所示:

>>> id = "IAD"
>>> location = "Dulles Intl Airport"
>>> max_temp = 32
>>> min_temp = 13
>>> precipitation = 0.4

目标数据格式如下所示:

IAD : Dulles Intl Airport : 32 / 13 / 0.40

1.8.2 实战演练

(1) 根据预期结果创建模板字符串,将所有数据项替换为{}占位符。在每个占位符内,放置数据项的名称。

'{id} : {location} : {max_temp} / {min_temp} / {precipitation}'

(2) 在模板字符串每个数据项的占位符中添加 :data type信息。基本数据类型代码如下:

  • s为字符串
  • d为十进制数
  • f为浮点数

添加类型代码后的模板字符串如下所示:

'{id:s}  : {location:s} : {max_temp:d} / {min_temp:d} / {precipitation:f}'

(3) 添加必要的长度信息。长度信息不是必须的,在某些情况下,甚至不需要长度信息。然而,在本例中,长度信息确保每个消息具有一致的格式。长度信息添加在类型代码之前。对于字符串和十进制数字,长度信息的格式如19s3d。对于浮点数,长度信息的格式如5.2f,该长度信息指定了两部分内容:总长度为5个字符,其中2个在小数点的右边。整个模板字符串如下所示:

>'{id:3d}  : {location:19s} : {max_temp:3d} / {min_temp:3d} / {precipitation:5.2f}'

(4) 使用字符串的format()方法创建最终的字符串。

>>> '{id:3s}  : {location:19s} :  {max_temp:3d} / {min_temp:3d} / {precipitation:5.2f}'.format(
... id=id, location=location, max_temp=max_temp,
... min_temp=min_temp, precipitation=precipitation
... )
'IAD  : Dulles Intl Airport :   32 /  13 /  0.40'

我们通过模板字符串的formate()方法按照名称提供所有变量。在某些情况下,可能需要使用变量构建一个字典对象。这时,可以使用format_map()方法:

>>> data = dict(
... id=id, location=location, max_temp=max_temp,
... min_temp=min_temp, precipitation=precipitation
... )
>>> '{id:3s}  : {location:19s} :  {max_temp:3d} / {min_temp:3d} / {precipitation:5.2f}'.format_map(data)
'IAD  : Dulles Intl Airport :   32 /  13 /  0.40'

第4章将讨论字典。

内置的vars()函数可以构建一个所有局部变量的字典:

>>> '{id:3s}  : {location:19s} :  {max_temp:3d} / {min_temp:3d} / {precipitation:5.2f}'.format_map(
...    vars()
... )
'IAD  : Dulles Intl Airport :   32 /  13 /  0.40'

vars()函数可以便捷地自动构建字典。

1.8.3 工作原理

字符串的format()方法和format_map()方法都可以组装相对复杂的字符串。

这些方法的基本功能是根据关键字参数的名称或字典中的键将数据插入到字符串中。当然,变量也可以按位置插入字符串,可以使用位置编号替代名称,例如,使用类似{0:3s}的格式规范作为format()方法的第一个位置参数。

前面已经介绍了sdf等三种格式规范,除此之外,还有很多其他格式规范,详细信息请参阅“Python标准库”的6.1.3节。常用的格式规范如下。

  • b用于二进制数,基数为2。
  • c用于Unicode字符。值必须是已经转换为字符的数字。通常用十六进制数来表示,例如0x26610x26663
  • d用于十进制数。
  • Ee用于科学记数法。使用6.626E-34还是6.626e-34取决于使用的是E字符还是e字符。
  • Ff用于浮点数。对于非数字,f格式显示为小写nanF格式显示为大写NAN
  • Gg为通用格式。该格式自动在EF(或ef)之间切换,以保持输出在给定的大小范围内。格式20.5G表示最多20位数字使用F格式显示。较大的数字将使用E格式。
  • n用于特定语言环境的十进制数。根据当前的区域设置插入,.。默认语言环境可能没有定义千位分隔符。更多信息请参阅locale模块。
  • o用于八进制数,基数为8。
  • s用于字符串。
  • Xx用于十六进制数,基数为16。数字包括大写A-F和小写a-f,具体取决于使用的是X格式符还是x格式符。
  • %用于百分比。数字乘以100,并包含%

3扑克牌的花色图案。——译者注

另外,还有许多可以用于这些格式规范的前缀。最常见的前缀是长度,例如,使用{name:5d}放入一个5位的数字。其他前缀如下。

  • 填充和对齐:可以指定一个特定的填充字符(默认值为空格)和对齐方式。数字通常右边齐,字符串通常左对齐。还可以使用<>^来改变对齐方式,这些符号将强制左对齐、右对齐或居中对齐。特殊的=对齐方式在一个前导符号后进行填充。
  • 符号:默认规则是在需要的位置前置一个负号。可以用+在所有数字前添加一个符号,用-仅在负数前添加一个符号,用空格在正数前添加一个空格而不是加号。在科学记数法格式的输出中,必须使用{value: 5.3f}。空格确保为符号留下空间,保证所有小数点整齐排列。
  • 替换形式:可以使用#获得替换形式。我们可能将前缀{0:#x}{0:#o}{0:#b}用于十六进制、八进制或二进制值。带前缀的数字的形式如0xnnn0onnn0bnnn。默认省略两个字符前缀。
  • 前导零:可以用0在数字前填充前导零。例如,{code:08x}将产生一个十六进制值,并通过前导零将其填补为8个字符。
  • 宽度和精度:对于整数值和字符串,只提供了宽度。对于浮点值,通常提供width.precision

有时候,我们将不使用{name:format}规范。有时候,我们需要使用{name!conversion}规范。可用的转换只有三种。

  • {name!r}显示由repr(name)生成的表示(representation)。
  • {name!s}显示由str(name)生成的字符串值。
  • {name!a}显示由ascii(name)生成的ASCII值。

在第6章中,我们将充分利用{name!r}格式规范来简化相关对象信息的显示。

1.8.4 补充知识

一种方便的调试工具如下所示:

>print("some_variable={some_variable!r}".format_map(vars()))

不带任何参数的vars()函数收集所有局部变量并转换为一个映射。我们把该映射作为参数提供给format_map()。格式模板使用大量的{variable_name!r}来显示局部变量的各种对象的细节。

在类定义中,可以使用类似vars(self)的技术。下面的示例使用了第6章的相关知识。

>>> class Summary:
...     def __init__(self, id, location, min_temp, max_temp, precipitation):
...         self.id= id
...         self.location= location
...         self.min_temp= min_temp
...         self.max_temp= max_temp
...         self.precipitation= precipitation
...     def __str__(self):
...         return '{id:3s}  : {location:19s} :  {max_temp:3d} / {min_temp:3d} / {precipitation:5.2f}'.format_map(
...             vars(self)
...         )
>>> s= Summary('IAD', 'Dulles Intl Airport', 13, 32, 0.4)
>>> print(s)
IAD  : Dulles Intl Airport :   32 /  13 /  0.40

类定义中包含一个 __str__()方法,该方法依靠vars(self)创建一个对象的属性字典。

1.8.5 延伸阅读

  • 字符串格式化方法的细节请参阅“Python标准库”的6.1.3节。

1.9 通过字符列表创建复杂的字符串

如何对不可变的字符串进行复杂的更改?可以用单个独立的字符组装字符串吗?

在大多数情况下,前面的实例已经为我们提供了一系列创建和修改字符串的工具。不过解决字符串操作问题的方法还有很多,本实例将介绍使用列表对象创建字符串的方法。这种方法与第4章的一些实例相吻合。

1.9.1 准备工作

需要重新排列的字符串如下:

>>> title = "Recipe 5: Rewriting an Immutable String"

字符串需要进行以下两个转换:

  • 移除:之前的那部分字符串;
  • 把标点符号替换为_,将所有字符转换为小写。

本实例需要使用string模块:

>>> from string import whitespace, punctuation

string模块有两个重要的常量。

  • string.whitespace列出了所有常用的空白字符,包括空格和制表符。
  • string.punctuation列出了常见的ASCII标点符号。Unicode拥有大量标点符号,可以根据区域设置来进行配置。

1.9.2 实战演练

字符串可以分解为列表。第4章将更深入地介绍列表。

(1) 将字符串分解为列表对象。

>>> title_list = list(title)

(2) 查找分割字符。列表的index()方法与字符串的index()方法具有相同的语义,它根据给定的值找到相应的位置。

>>> colon_position = title_list.index(':')

(3) 删除不再需要的字符。del语句可以从列表中移除元素。列表是可变的数据结构。

>>> del title_list[:colon_position+1]

我们不需要仔细处理原始字符串中的有用部分。可以从列表中删除元素。

(4) 逐个位置替换标点符号。在本示例中,使用for语句来访问字符串的各个索引。

>>> for position in range(len(title_list)):
...     if title_list[position] in whitespace+punctuation:
...         title_list[position]= '_'

(5) 表达式range(len(title_list))生成从0len(title_list)-1的所有整数。这样就可以确保position的值是列表中的每个值的索引。连接字符列表,创建新字符串。在连接字符串时,使用长度为0的字符串''作为分隔符看起来似乎有些奇怪。但是,这种方法十分有效。

>>> title = ''.join(title_list)
>>> title
'_Rewriting_an_Immutable_String'

我们把结果字符串赋值给了原始变量。我们不再需要原始变量引用的原始字符串对象,该对象将从内存中删除。新的字符串对象替换了原始变量的值。

1.9.3 工作原理

这是一种表示形式上的变化。由于字符串是不可变的,因此我们不能更新它。但是,可以将其转换为可变形式,本实例中的字符串被转换为了列表。我们可以对可变的列表对象进行任何更改。更改完成后,可以将表示形式由列表改回字符串。

字符串提供了许多列表没有的功能。例如,我们不能根据将字符串转换为小写的方法直接将列表转换为小写。

字符串与列表的区别如下。

  • 字符串是不可变的,所以操作速度非常快。字符串专注于Unicode字符。可以使用字符串作为映射中的键和集中的元素,因为字符串的值是不可变的。
  • 列表是可变的,操作较慢。列表可以容纳任何类型的元素。不能使用列表作为映射中的键或集中的元素,因为值可能会改变。

字符串和列表都是特殊类型的序列。因此,它们具有许多共同的特征,比如共享基本的元素索引和切片功能。类似于字符串,列表同样支持字符串使用的负索引值,list[-1]表示一个列表对象中的最后一个元素。

第4章将讨论可变数据结构。

1.9.4 补充知识

一旦使用字符列表代替了字符串,就不能再使用字符串处理方法。许多列表处理技术都可以使用。除了能够从列表中删除元素之外,还可以添加元素,或者使用另一个列表扩展列表,或者向列表中插入字符。

我们也可以稍微改变视角,讨论一下用字符串列表替代字符列表。''.join(list)不但支持字符列表,也支持字符串列表。例如:

>>> title_list.insert(0, 'prefix')
>>> ''.join(title_list)
'prefix_Rewriting_an_Immutable_String'

title_list对象变成了一个包含字符串prefix和30个独立字符的列表。

1.9.5 延伸阅读

  • 使用字符串的内置方法处理字符串的实例,请参阅1.6节。
  • 创建字符串并将其转换为字节的实例,请参阅1.11节。
  • 将字节转换为字符串的实例,请参阅1.12节。

1.10 使用键盘上没有的Unicode字符

一个键盘可能有将近100个独立按键,其中字母、数字和标点符号按键不到50个,还有至少12个功能键。另外,键盘还有各种修饰键,它们需要与其他按键结合使用。常见的修饰键有Shift、Ctrl、Option和Command。

大多数操作系统都支持简单的组合键,这些组合键可以创建100个左右的字符,更复杂的组合键可能会创建另外100个不常见的字符,但这离覆盖世界上的数百万字符还差得很远。而且,在计算机字体里还有图标、表情符号和装饰符号,又如何得到这些符号呢?

1.10.1 准备工作

Python默认支持Unicode。可用的独立Unicode字符有几百万个。

有关所有可用的Unicode字符,请参阅https://en.wikipedia.org/wiki/List_of_Unicode_charactershttp://www.unicode.org/charts/

我们需要Unicode字符的编码,也希望得到Unicode字符的名称。

很多计算机中的字体可能在设计时就没有考虑提供这些字符,特别是Windows计算机中的字体可能无法显示这些Unicode字符。有时使用Windows命令来修改为内码表(codepage)65001是很有必要的:

chcp 65001

Linux和Mac OS X几乎没有关于Unicode字符的问题。

1.10.2 实战演练

Python使用转义序列(escape sequence)扩展普通字符。通过转义序列我们可以输入海量的 Unicode字符。转义序列以\开始,下一个字符说明Unicode字符如何表示。找到所需的字符,获取名称或编码。编码总是以十六进制形式出现,例如U+2680。这个编码的名称可能是DIE FACE-1。可以使用 \unnnn格式将编码填充为4位数,或者使用可拼写的名称\N{name}。如果编码超过4位数,可以使用 \Unnnnnnnn格式将编码扩展为8位数。

如上例所示,我们可以在Python输出中包含各种字符。但是,在字符串中添加\时,需要使用\\。例如,在使用Windows文件名时,可能需要采用这种方式处理\

1.10.3 工作原理

Python内置使用Unicode。我们可以直接使用键盘输入的约128个字符都有便于使用的Unicode内部编码。

在输入'HELLO'时,Python将其视为下列代码的简写:

'\u0048\u0045\u004c\u004c\u004f'

如果想使用那些键盘上没有的字符,那么只能通过它们的编码来进行标识。

当字符串被Python编译时,\uxx\Uxxxxxxxx\N{name}都将被适当的Unicode字符所代替。如果其中有一些语法错误,例如 \N{name没有使用}闭合,那么Python内部语法检查会立即报错。

在1.7节中,我们注意到正则表达式使用了大量的\,我们不希望Python编译器对它们进行处理,所以在正则表达式字符串中使用r'前缀来防止\被当作转义符,进而被转化为其他字符。

如果需要在正则表达式中使用Unicode怎么办?为了解决这个问题需要在正则表达式中使用\\,例如'\\w+[\u2680\u2681\u2682\u2683\u2684\u2685]\\d+'。我们省略了字符串前面的r'前缀,在正则表达式中多用了一倍的 \。我们使用的 \uxxxx格式是Unicode字符模式的一部分。Python编译器将 \uxxxx替换为Unicode字符,将\\替换为\

 Python将以规范形式显示>>>提示符中的字符串。虽然可以使用'"作为字符串分隔符,但是Python更喜欢使用'。Python通常不显示原始字符串,而是将所有必要的转义序列转换为字符串:

>> r"\w+"
'\\w+'

我们提供了一个原始形式的字符串,但是Python将其显示为规范形式。

1.10.4 延伸阅读

  • 1.11节和1.12节将讨论如何把Unicode字符转换为字节序列以便写入文件,也将讨论如何把文件中或从网站下载的字节转换为Unicode字符,以便对它们进行处理。
  • 如果对历史感兴趣,可以了解一下ASCII、EBCDIC以及其他传统的字符编码,请参阅http://www.unicode.org/charts/

1.11 编码字符串——创建ASCII和UTF-8字节

计算机上的文件都是由字节组成的,当我们在网上上传或下载文件时,通信也是基于字节的。一个字节只有256个不同的值,而Python字符是Unicode字符,Unicode字符的数量远不止256个。

如何将Unicode字符映射到用于写入文件或传输文件的字节呢?

1.11.1 准备工作

以前,一个字符占用一个字节。Python中的字节(byte)使用ASCII编码方案,这样有时容易混淆字节和Unicode字符串。

Unicode字符通常被编码为字节序列。这些字符中既有很多标准编码,也有很多非标准编码。

另外,还有一些编码只适用于Unicode字符的某个小子集。应当尽量避免这种情况,但是在一些特殊情况下,需要使用编码子集方案。

除非有特殊的需求,否则应当一直使用UTF-8编码Unicode字符。这种方法的主要优点在于,它是英语和许多欧洲语言中的拉丁字母的简洁表现形式。

有时候,某些互联网协议需要使用ASCII字符编码。这是一个需要注意的特殊情况,因为ASCII编码只能处理一小部分Unicode字符。

1.11.2 实战演练

Python通常使用操作系统的默认编码处理文件和互联网通信,每个操作系统的处理细节各不相同。

(1) 使用PYTHONIOENCODING环境变量进行通用设置。在Python之外设置这个变量可以保证在操作系统范围内使用特定的编码。环境变量的设置方法如下:

export PYTHONIOENCODING=UTF-8

(2) 运行Python。

python3.5

(3) 有时候在脚本中打开文件需要一些特殊设置。第9章将再次讨论这个问题。以给定的编码方式打开文件、读取文件或者向文件中写入Unicode字符。

>>> with open('some_file.txt', 'w', encoding='utf-8') as output:
...     print( 'You drew \U0001F000', file=output )
>>> with open('some_file.txt', 'r', encoding='utf-8') as input:
...     text = input.read()
>>> text
'You drew �'

我们也可以手动编码字符。在极少数的情况下,需要以字节模式打开一个文件;如果使用wb模式,那么需要使用手动编码。

>>> string_bytes = 'You drew \U0001F000'.encode('utf-8')
>>> string_bytes
b'You drew \xf0\x9f\x80\x80'

字节序列(\xf0\x9f\x80\x80)被用来编码Unicode字符U+1F000,即

1.11.3 工作原理

Unicode定义了许多编码方案,其中UTF-8是最流行的,其他编码方案还有UTF-16和UTF-32。编码方案名称中的数字是该方案中每个字符的位数。一个包含1000个UTF-32编码字符的文件将有4000字节。一个包含1000个UTF-8编码字符的文件可能只有1000字节,具体的字节数取决于字符的精确组合,因为在UTF-8编码方案中,字符编码大于U+007F的字符需要使用多个字节表示。

不同的操作系统有各自的编码方案,Mac OS X文件通常使用MacRomanlatin-1编码,Windows文件可能使用CP1252编码。

这些编码方案的关键在于可以映射到Unicode字符的字节序列。另一种方法是把每个Unicode字符映射到一个或多个字节。理想情况下,所有的Unicode字符都被编码。实际上,其中一些编码方案是不完整的。编码方案最棘手的问题在于避免写入多余的字节。

古老的ASCII编码只能将大约250个Unicode字符表示为字节。创建一个不能使用ASCII方案编码的字符串非常容易。

>>> 'You drew \U0001F000'.encode('ascii')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character '\U0001f000' in position 9: ordinal not in range(128)

当我们无意中选择错误的编码方式打开文件时,就可以看到类似上面的错误。在遇到这样的错误时,需要改变处理过程,选择一个更适当的编码。理想情况下使用UTF-8。

1.11.4 延伸阅读

1.12 解码字节——如何根据字节获得正确的字符

如何处理没有正确编码的文件?如何处理使用ASCII编码的文件?

从互联网上下载的文件几乎都是以字节而不是字符为单位的。如何从字节流中解码字符呢?

另外,在使用subprocess模块时,操作系统命令的结果也是以字节为单位的。如何把字节流转换为正确的字符呢?

本实例的大部分内容与第9章相关。之所以在本章介绍这个实例,是因为它是1.11节中实例的反例。

1.12.1 准备工作

假设我们对近海海洋天气预报感兴趣,或许是因为自己拥有一艘大帆船,或许是因为好朋友有一艘大帆船,为了躲避台风正在离开加勒比海的切萨皮克湾。

弗吉尼亚韦克菲尔德的国家气象服务办公室(National Weather Services)有什么特别的警报吗?

警报信息显示在http://www.nws.noaa.gov/view/national.php?prod=SMW&sid=AKQ上。

可以通过Python的urllib模块下载这些警报信息:

>>> import urllib.request
>>> warnings_uri= 'http://www.nws.noaa.gov/view/national.php?prod=SMW&sid=AKQ'
>>> with urllib.request.urlopen(warnings_uri) as source:
...     warnings_text= source.read()

或者使用curlwget等程序获取这些信息:

curl -O http://www.nws.noaa.gov/view/national.php?prod=SMW&sid=AKQ
mv national.php\?prod\=SMW AKQ.html

但是curl生成了一个看起来很别扭的文件名,我们需要重命名这个文件。

forecast_text的值是字节流,而不是字符串。forecast_text的起始部分如下所示:

>>> warnings_text[:80]
b'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.or'

因为forecast_text起始于b'前缀,所以它是字节而不是Unicode字符。forecast_text可能使用UTF-8编码,这意味着一些字符可能显示为怪异的 \ xnn转义序列而不是字符,而我们需要的结果通常是正确的字符。

 字节和字符串

字节通常使用可打印字符显示。

b'hello'是一个五字节值的缩写。字母选择使用旧的ASCII编码方案。从0x200xfe的许多字节值将显示为字符。

这让人很困惑。b'前缀提示我们关注的是字节,而不是相应的Unicode字符。

通常,字节的行为有点类似于字符串。有时我们可以直接处理字节,但是大多数情况下,我们希望解码字节并创建正确的Unicode字符。

1.12.2 实战演练

(1) 尽可能确定编码方案。为了解码字节并创建正确的Unicode字符,需要知道字节采用的编码方案。例如,XML文档就给出了提示:

<?xml version="1.0" encoding="UTF-8"?>

浏览网页时,首部(header)通常包含如下信息:

Content-Type: text/html; charset=ISO-8859-4

有时HTML页面可能包含如下信息:

<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

在其他情况下,我们只能猜测编码方案。在美国天气数据示例中,首先猜测UTF-8编码是一种较好的选择,其他常见的编码包括ISO-8859-1。在某些情况下,编码的猜测取决于内容使用的语言。

(2)“Python标准库”的7.2.3节列出了所有可用的标准编码。解码数据:

>>> document = forecast_text.decode("UTF-8")
>>> document[:80]
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.or'

b'前缀没有了,我们从字节流中创建了一个由Unicode字符组成的正确的字符串。

(3) 如果上述步骤失败,并抛出异常,就说明猜错了编码。我们需要尝试另一种编码。最后,解析结果文档。

因为示例是一个HTML文档,所以应当使用Beautiful Soup处理。Beautiful Soup的相关信息请参阅http://www.crummy.com/software/BeautifulSoup/

但是,我们还可以在不完全解析HTML的情况下从这个文档中提取一块信息:

>>> import re
>>> title_pattern = re.compile(r"\<h3\>(.*?)\</h3\>")
>>> title_pattern.search( document )
<_sre.SRE_Match object; span=(3438, 3489), match='<h3>There are no products active at this time.</h>

这个示例说明:目前没有警报。这并不意味着一帆风顺,而是不存在会导致灾难的天气系统。

1.12.3 工作原理

关于Unicode以及将Unicode字符编码为字节流的不同方式的更多信息,请参阅1.11节。

在操作系统中,文件和网络连接是建立在字节基础上的。软件通过解码字节来发现内容。这些内容可能是字符、图像或声音。在某些情况下,默认的假设是错误的,我们需要自己解码。

1.12.4 延伸阅读

1.13 使用元组

表示简单的(x,y)值和(r,g,b)值的最好方法是什么?如何将纬度和经度之类的对象保存在一起?

1.13.1 准备工作

有一种有趣的数据结构1.7节没讲。

假设数据如下所示:

>>> ingredient = "Kumquat: 2 cups"

使用正则表达式将以上数据解析为有意义的数据,如下所示:

>>> import re
>>> ingredient_pattern = re.compile(r'(?P<ingredient>\w+):\s+(?P<amount>\d+)\s+ (?P<unit>\w+)')
>>> match = ingredient_pattern.match( ingredient )
>>> match.groups()
('Kumquat', '2', 'cups')

结果是一个含有三个数据的元组对象。在很多场合,这种分组的数据能够派上用场。

1.13.2 实战演练

本实例关注两个方面:把数据放入元组,以及从元组中取出数据。

  1. 创建元组

    Python的很多内置模块都使用了元组结构。1.7.1节展示了使用正则表达式的匹配对象从字符串创建元组的示例。

    我们也可以自己创建元组,步骤如下。

    (1) 把数据用()括起来。

    (2) 用,把元素隔开。

    >>> from fractions import Fraction
    >>> my_data = ('Rice', Fraction(1/4), 'cups')

    元组有一个重要的特例——一元组或单例元组。即使元组只有一个元素,也必须包含一个额外的,

    >>> one_tuple = ('item', )
    >>> len(one_tuple)
    1

     ()并不是必须的,某些情况下可以省略。但最好不要省略。在值后面添加一个额外的逗号时,可以看到有趣的现象:

    >> 355,
    (355,)

    355后的逗号将字面量355转换为一个单元素元组。

  2. 从元组中提取元素

    元组是一种容器,其中包含一些由问题域固定的元素,例如(red, green, blue)颜色数字,其元素的数量总是三个。

    前面的示例已经得到了由原料、数量和单位组成的数据。这是一个三元集合,可以通过两种方式来查看元素。

    • 通过索引位置。索引从最左边的位置开始编号,以0为起始值:

      >>> my_data[1]
      Fraction(1, 4)
    • 使用多重赋值:

      >>> ingredient, amount, unit = my_data
      >>> ingredient
      'Rice'
      >>> unit
      'cups'

    与字符串类似,元组是不可变的。不能改变一个元组中的元素。若想保持数据不变,可以使用元组。

1.13.3 工作原理

元组属于更通用的序列(Sequence)。我们可以对序列执行一系列操作。

待处理的示例元组如下:

>>> t = ('Kumquat', '2', 'cups')

可以对元组执行的操作如下。

  • 获得t的元素数量:

    >>> len(t)
    3
  • 获得特定值在t中的出现次数:

    >>> t.count('2')
    1
  • 获得特定值的位置:

    >>> t.index('cups')
    2
    >>> t[2]
    'cups'
  • 当访问不存在的元素时抛出异常:

    >>> t.index('Rice')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    ValueError: tuple.index(x): x not in tuple
  • 测试特定值是否存在:

    >>> 'Rice' in t
    False

1.13.4 补充知识

字符串是字符的序列,而元组是元素的序列,而且元组是多种对象的序列。因为字符串和元组都是序列,所以它们有一些共同的特性。如前所示,我们可以通过索引位置选择个别元素,也可以使用index()方法找到一个元素的位置。

以上是两者的相似之处,两者之间还有很多不同之处。字符串具有多种创建新字符串的方法,新字符串是原有字符串的转换。除此之外,字符串还具有解析字符串的方法,以及确定字符串内容的方法。元组没有这些附加特性。元组可能是Python最简单的数据结构。

1.13.5 延伸阅读

  • 1.9节讨论了另一种序列——列表。
  • 第4章将讨论多种序列。

目录

  • 版权声明
  • 前言
  • 第 1 章 数字、字符串和元组
  • 第 2 章 语句与语法
  • 第 3 章 函数定义
  • 第 4 章 内置数据结构——列表、集、字典
  • 第 5 章 用户输入和输出
  • 第 6 章 类和对象的基础知识
  • 第 7 章 高级类设计
  • 第 8 章 函数式编程和反应式编程
  • 第 9 章 输入/输出、物理格式和逻辑布局
  • 第 10 章 统计编程和线性回归
  • 第 11 章 测试
  • 第 12 章 Web服务
  • 第 13 章 应用程序集成