第二章 序列构成的数组

目录

和第一版的变化

内置序列总览

列表推导式和生成器表达式

列表推导式和可读性

Listcomps 对比 map 和 filter内置函数

笛卡尔积 

生成器表达式

元组不仅仅是不可变列表

元组作为记录

元组作为不可变列表

比较元组和列表方法

拆包序列和可迭代对象

使用 * 抓取多余的项

在函数调用和序列字面量中使用 *进行拆包

嵌套拆包

序列的模式匹配

解释器中的模式匹配序列

切片

为什么切片和range不包括最后一项

切片对象

多维切片和省略号

给切片赋值

在序列中使用 + 和 *

构建列表组成的列表

序列的增量赋值

一个关于 += 赋值的谜题

list.sort 与 内置的 sorted方法

当列表不是首选时

数组

内存视图

Numpy

双向队列和其他队列


您可能已经注意到,提到的几个操作同样适用于文本、列表和表格。文本、列表和表格一起称为序列。 […] FOR 命令也可以在所有的序列上使用

                                                --Geurts, Meertens, and Pemberton, ABC Programmer’s Handbook

在创建 Python 之前,Guido 是 ABC 语言的贡献者,这是一个为期 10 年的研究项目,旨在为初学者设计一个编程环境。ABC 引入了许多我们现在认为是“Pythonic”的想法:对不同类型序列的泛型操作、内置元组和映射类型、缩进结构、没有变量声明的强类型等等。Python 对用户如此友好并非偶然。

Python 继承了 ABC 对序列的统一处理。字符串、列表、字节序列、数组、XML 元素和数据库结果共享一组丰富的常见操作,包括迭代、切片、排序和拼接。

了解 Python 中可用的各种序列使我们不需要重新发明轮子,这些通用接口可以帮助我们把自己定义的API设计的和原生序列一样。

本章的大部分讨论都适用于一般的序列,从熟悉的list到 Python 3 中添加的 str 和 bytes 类型。此处还涵盖了有关列表、元组、数组和队列的特定主题,但 Unicode 字符串和字节序列的细节出现在第 4 章中。此外,这里的想法是涵盖准备使用的序列类型。创建自定义序列类型是第 12 章的主题。

这些是本章将涵盖的主要主题:

  • 列表推导式和生成器表达式的基础知识;
  • 使用元组作为记录,而不是使用元组作为不可变列表;
  • 序列拆包和序列模式;
  • 从切片读取和写入切片;
  • 特殊的序列类型,如数组和队列

和第一版的变化

序列类型是 Python 中非常稳定的部分,因此这里的大部分变化不是更新,而是对 Fluent Python 第一版的改进。最重要的包括以下几点:

  • 容器序列和扁平序列的对比,序列内部结构的描述和图解
  • 列表与元组的性能和存储特性的简单比较
  • 具有可变项的元组的注意事项,以及如何在需要的情况下对它们进行测试

我在第 5 章中将命名元组的覆盖范围移至“经典命名元组”,在那里将它们与 Typing.NamedTuple 和 @dataclass 进行比较。

Note:为了为新内容腾出空间并使页数保持在合理范围内,第一版中的使用 Bisect 管理有序的序列部分现在是 fluentpython.com 配套网站上的一个帖子。

内置序列总览

标准库提供了多种用 C 实现的序列类型:

  • 容器序列:list、tuple 和 collections.deque 可以保存不同类型的数据,包括嵌套容器。
  • 扁平序列:str、bytes、bytearray、memoryview 和 array.array ,这类序列只能容纳一种简单的类型

容器序列保存对其包含的对象的引用,这些对象可以是任何类型;扁平序列将序列内容的值存储在自己的内存空间中,而不是作为一个独立的对象。如下图所示:

图 2-1。tuple和array的简化内存图,同时包含3个项。灰色单元格代表每个 Python 对象的内存头——未按比例绘制。元组中包含一个对列表对象的引用。每个项都是单独的 Python 对象,也可能是对其他 Python 对象的引用,比如那个包含2个项的list。相比之下,Python 的array是一个单独的对象-拥有3个double类型的 C 语言数组。

因此,扁平序列更紧凑,但它们仅限于保存原始机器值,例如字节、整数和浮点数。

Note:

内存中的每个 Python 对象都有一个包含元数据的头。例如python中的float对象,有一个对象值的字段和两个元数据字段。在 64 位 Python 构建中,表示float对象的结构具有以下 64 位字段:

  • ob_refcnt: 对象引用计数;

  • ob_type: 对象类型指针;

  • ob_fval: 存储float值的C语言的 double对象。

这就是为什么浮点数数组比浮点数元组紧凑得多的原因:数组是一个保存浮点数原始值的单个对象,而元组由多个对象组成——元组本身和包含在其中的每个float对象

另一种对序列类型进行分组的方法是可变性:

可变序列:listbytearrayarray.arraycollections.deque, and memoryview

不可变序列:tuplestr, and bytes

图 2-2 形象化的展示了可变序列如何从不可变序列继承所有方法,并且实现额外的方法。内置的具体序列类型实际上并没有继承 Sequence 和 MutableSequence 抽象基类 (ABC),但它们是 注册为ABC 虚拟子类(abc.Sequence和abc.MutableSequence)。

作为虚拟子类,tuple和list可以通过下面的测试:

>>> from collections import abc
>>> issubclass(tuple, abc.Sequence)
True
>>> issubclass(list, abc.MutableSequence)
True

请记住这些共同特征:可变序列与不可变序列;容器序列与扁平序列。这样就可以将对一种序列类型的理论应用到其他类型。 

最基本的序列类型是list:一个可变容器。我想您对列表已经非常熟悉了,因此我们将直接进入列表推导式,这是一种构建列表的强大方法,但有时未被充分利用,因为语法最开始可能看起来有些晦涩。掌握列表推导式为生成器表达式打开了大门,生成器可以生成各种类型的元素并用他们来填充序列。两者都是下一节的主题。

列表推导式和生成器表达式

构建序列的一种快速方法是使用列表推导式(如果目标是列表)或生成器表达式(对于其他类型的序列)。如果没有日常使用这些句法形式,我敢打赌您将错过编写更具可读性且速度更快的代码的机会。

如果您怀疑我声称这些结构“更具可读性”,请继续阅读。我会努力说服你。

TIP:

为简洁起见,许多 Python 程序员将列表推导式称为 listcomps,将生成器表达式称为 genexps。我也会用这些词。

列表推导式和可读性

这是一个测试:您觉得哪个更容易阅读,示例 2-1 还是示例 2-2?

例 2-1。从字符串构建 Unicode 码点列表

>>> symbols = '$¢£¥€¤'
>>> codes = []
>>> for symbol in symbols:
...     codes.append(ord(symbol))
...
>>> codes
[36, 162, 163, 165, 8364, 164]

例 2-2。使用 listcomp 从字符串构建 Unicode 码点列表 

>>> symbols = '$¢£¥€¤'
>>> codes = [ord(symbol) for symbol in symbols]
>>> codes
[36, 162, 163, 165, 8364, 164]

任何对 Python 有一点了解的人都可以阅读示例 2-1。然而,在了解了 listcomps 之后,我发现示例 2-2 更具可读性,因为它的意图是明确的。

for 循环可以用来做很多不同的事情:扫描一个序列来计数或选则项、计算聚合(总和、平均值)或任意数量的其他任务。 示例 2-1 中的代码正在构建一个列表。相比之下,listcomp 更为明确。它的目标是构建一个新列表。

当然,滥用列表推导式会编写出难以理解的代码。我见过带有 listcomps 的 Python 代码只是用来重复一段代码以消除其副作用。如果您没有对生成的列表执行某些操作,则不应使用该语法。另外,尽量保持简短。如果列表推导式跨越两行以上,最好将其拆分或重写为普通的旧 for 循环。使用您的最佳判断:对于 Python 和英语,清晰写作没有硬性规定。

语法提示:

在 Python 代码中,在 []、{} 或 () 对中会忽略换行符。所以你可以构建多行列表、listcomps、元组、字典等。不使用 \ 进行续行转义,如果您不小心在它后面键入一个空格,则该转义将不起作用。此外,当这些分隔符对用于定义具有逗号分隔的一系列项目的字面量时,将忽略尾随逗号。因此,例如,在对多行列表字面量进行编码时,最好在最后一项之后放置一个逗号,这样下一个编码人员就可以更轻松地向该列表中再添加一项,并减少读取差异时的噪音.

推导式和生成器表达式中的局部作用域:

在 Python 3 中,列表推导式、生成器表达式以及它们的兄弟 set 和 dict 推导式都有一个局部作用域来保存在 for 子句中分配的变量。但是,在这些推导式或表达式返回后,使用“海象运算符” := 赋值的变量仍然可以访问 - 这与函数中的局部变量不同。PEP 572—Assignment Expressions将 := 的目标范围定义为闭包函数,除非该目标有global或nonlocal声明。

>>> x = 'ABC'
>>> codes = [ord(x) for x in x]
>>> x  1
'ABC'
>>> codes
[65, 66, 67]
>>> codes = [last := ord(c) for c in x]
>>> last 2
67
>>> c 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'c' is not defined
  1. x 没有被破坏:它仍然绑定到“ABC”;
  2. last变量被保留了下来
  3.  c 只存在于 listcomp 中。

列表推导式通过过滤和转换项目从序列或任何其他可迭代类型构建列表。filter和map内置函数可以组合起来做同样的事情,但可读性会受到影响,我们将在接下来看到。

Listcomps 对比 map 和 filter内置函数

Listcomps 可以完成 map 和 filter 函数所做的一切,并且不会受到Python lambda 的扭曲导致的后果。考虑示例 2-3。

例 2-3。由 listcomp 和 map/filter 组合构建的相同列表

>>> symbols = '$¢£¥€¤'
>>> beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
>>> beyond_ascii
[162, 163, 165, 8364, 164]
>>> beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))
>>> beyond_ascii
[162, 163, 165, 8364, 164]

我曾经相信 map 和 filter 比等效的 listcomps 更快,但 Alex Martelli 指出事实并非如此——至少在前面的例子中不是这样。Fluent Python 代码库中的 02-array-seq/listcomp_speed.py 脚本是一个简单的速度测试,将 listcomp 与 filter/map 进行比较。

我将在第 7 章中详细介绍 map 和 filter。现在我们转向使用 listcomps 计算笛卡尔积:一个包含由两个或多个列表中的所有项构建的元组的列表。

笛卡尔积 

Listcomps 可以从两个或多个可迭代对象的笛卡尔积构建列表。构成笛卡尔积的项是由每个输入可迭代项的项组成的元组。result列表的长度等于输入可迭代对象的长度相乘。参见图 2-3。

例如,假设您需要生成有两种颜色和三种尺寸的 T 恤列表。示例 2-4 展示了如何使用 listcomp 生成该列表。结果有六个项。

例 2-4。使用列表推导式的笛卡尔积

>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> tshirts = [(color, size) for color in colors for size in sizes]  1
>>> tshirts
[('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'),
 ('white', 'M'), ('white', 'L')]
>>> for color in colors:  2
...     for size in sizes:
...         print((color, size))
...
('black', 'S')
('black', 'M')
('black', 'L')
('white', 'S')
('white', 'M')
('white', 'L')
>>> tshirts = [(color, size) for size in sizes      3
...                          for color in colors]
>>> tshirts
[('black', 'S'), ('white', 'S'), ('black', 'M'), ('white', 'M'),
 ('black', 'L'), ('white', 'L')]
  1. 这会生成一个按color排列的元组列表,然后是size。 
  2. 请注意结果列表的排列方式,就好像 for 循环的嵌套顺序与它们在 listcomp 中出现的顺序相同。
  3. 要按大小排列项目,然后按颜色排列,只需重新排列 for 子句即可;在 listcomp 中添加换行符可以更容易地查看结果的排序方式。

在示例 1-1(第 1 章)中,我使用以下表达式初始化一副牌组,其中包含来自 4 种花色的所有 13 个牌面的 52 张牌组成的列表,按花色排序然后排名: 

        self._cards = [Card(rank, suit) for suit in self.suits
                                        for rank in self.ranks]

Listcomps 只有一个目标:构建列表。要为其他序列类型生成数据,可以使用 genexp。下一节将在构建非列表序列的上下文中简要介绍 genexp。 

生成器表达式

要初始化元组、数组和其他类型的序列,您也可以使用 listcomp ,但是 genexp 可以节省内存,因为它遵循迭代器协议逐个的产出项,而不是构建一个完整的列表来提供给某个构造函数。

Genexps 使用与 listcomps 相同的语法,但用圆括号而不是方括号括起来。

示例 2-5 展示了 genexps 构建元组和数组的基本用法。

例 2-5。从生成器表达式初始化元组和数组

>>> symbols = '$¢£¥€¤'
>>> tuple(ord(symbol) for symbol in symbols)  1
(36, 162, 163, 165, 8364, 164)
>>> import array
>>> array.array('I', (ord(symbol) for symbol in symbols))  2
array('I', [36, 162, 163, 165, 8364, 164])
  1. 如果生成器表达式是函数调用中的单个参数,则无需复制括号。

  2. array构造函数接受两个参数,因此生成器表达式周围的括号是必需的。数组构造函数的第一个参数定义了用于数组中数字的存储类型,我们将在“数组”中看到。

示例 2-6 使用带有笛卡尔乘积的 genexp 打印出三种尺寸的两种颜色的 T 恤花名册。与示例 2-4 不同的是,内存中未构建由T 恤的六个项组成的列表:生成器表达式为 for 循环的每次循环生成一个项。

如果笛卡尔积中使用的两个列表各有 1,000 个项,则使用生成器表达式将节省构建包含一百万个项的列表以供 for 循环的成本。

例 2-6。生成器表达式中的笛卡尔积

>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> for tshirt in (f'{c} {s}' for c in colors for s in sizes):  1
...     print(tshirt)
...
black S
black M
black L
white S
white M
white L
  1. 生成器表达式一一生成项;在此示例中从未生成包含所有六种 T 恤的列表的变量。 

Note:

第 17 章详细解释了生成器的工作原理。这里的想法只是展示如何使用生成器表达式来初始化列表以外的序列,或者生成不需要保存在内存中的输出。

现在我们转到 Python 中的另一个基本序列类型:元组。

元组不仅仅是不可变列表

一些关于 Python 的教程将元组表示为“不可变列表”,但这没有完全概括元素的特点。元组有双重作用:它们可以用作不可变列表,也可以用作没有字段名称的记录。这种用法有时会被忽视,所以我们将从它开始。

元组和记录

元组保存记录:元组中的每个项保存一个字段的数据,项的位置给出了它的含义。

如果您将元组视为不可变列表,则项的数量和顺序可能重要也可能不重要,具体取决于上下文。但是当使用元组作为字段的集合时,项的数量通常是固定的,并且它们的顺序总是很重要的。

示例 2-7 显示了用作记录的元组。请注意,在每个表达式中,对元组进行排序都会破坏信息,因为每个字段的含义由其在元组中的位置给出。

例 2-7。用作记录的元组

>>> lax_coordinates = (33.9425, -118.408056)  1
>>> city, year, pop, chg, area = ('Tokyo', 2003, 32_450, 0.66, 8014)  2
>>> traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'),  3
...     ('ESP', 'XDA205856')]
>>> for passport in sorted(traveler_ids):  4
...     print('%s/%s' % passport)   5
...
BRA/CE342567
ESP/XDA205856
USA/31195855
>>> for country, _ in traveler_ids:  6
...     print(country)
...
USA
BRA
ESP
  1. 洛杉矶国际机场的经纬度。
  2. 关于东京的数据:名称、年份、人口(千)、人口变化(%)、面积(km²)。
  3. 格式为 (country_code,passport_number) 的元组列表。 
  4. 当我们遍历列表时,passport 绑定到每个元组。
  5. % 格式化运算符理解元组并将每个项视为一个单独的字段。
  6. for 循环知道如何分别检索元组的项——这称为“拆包”。这里我们对第二项不感兴趣,所以我们将它分配给 _,一个虚拟变量。

TIP:

通常,使用 _ 作为虚拟变量只是一种约定。这只是一个奇怪但合法的变量名。在 match/case 语句中,_ 是一个通配符,它​​可以匹配任何值但从未被赋值。请参阅“与序列的模式匹配”。并且在 Python 控制台中,执行的前一条命令的结果被赋值给 _——除非这个结果是 None。

我们经常将记录视为具有命名字段的数据结构。第 5 章介绍了创建具有命名字段的元组的两种方法。

但通常,没有必要为了命名字段而麻烦地创建一个类,特别是如果您利用拆包并避免使用索引来访问字段。在示例 2-7 中,我们在一个语句中将 ('Tokyo', 2003, 32_450, 0.66, 8014) 分配给了 city、year、pop、chg、area。然后,% 运算符将passport元组中的每个项赋值给print参数中格式字符串中的相应位置。这是元组拆包的两个例子。


Note:

术语元组拆包被 Pythonistas 广泛使用,但可迭代对象拆包也将如此,如 PEP 3132 — Extended Iterable Unpacking的标题。“拆包序列和可迭代对象”不仅介绍了对元组的拆包,还介绍了一般的序列和可迭代对象的拆包。

现在让我们将tuple类视为list类的不可变的变体。

元组作为不可变列表

Python 解释器和标准库广泛使用元组作为不可变列表。这有两个主要好处:

  • 明确:当你在代码中看到一个元组时,你知道它的长度永远不会改变。
  • 性能:元组比相同长度的列表占用更少的内存,并且允许 Python 对项进行优化。

但是,请注意元组的不变性仅适用于其中包含的引用。元组中的引用不能被删除或替换。但是如果其中有某个引用指向一个可变对象,并且该对象发生了变化,那么元组的值就会发生变化。下面代码片段通过创建两个最初相等的元组(a 和 b)来演示这一点。当 b 中的最后一项发生变化,它们不再相等.图 2-4 表示内存中 b 元组的初始布局。

>>> a = (10, 'alpha', [1, 2])
>>> b = (10, 'alpha', [1, 2])
>>> a == b
True
>>> b[-1].append(99)
>>> a == b
False
>>> b
(10, 'alpha', [1, 2, 99])

具有可变项的元组中可能会引入缺陷。正如我们将在“什么是可散列的?”中看到的,一个对象只有在其值永远不会改变时才是可散列的。不可散列的元组不能作为字典键或者集合的项。

如果要明确确定元组(或任何对象)的值是固定的,可以使用内置的hash方法来创建判断一个对象是否是固定的fixed函数,如下所示:

>>> def fixed(o):
...     try:
...         hash(o)
...     except TypeError:
...         return False
...     return True
...
>>> tf = (10, 'alpha', (1, 2))
>>> tm = (10, 'alpha', [1, 2])
>>> fixed(tf)
True
>>> fixed(tm)
False

除了这个警告外,元组被广泛用作为不可变列表。 Python 核心开发人员 Raymond Hettinger 在 StackOverflow 上对问题(Are tuples more efficient than lists in Python? )的回答中解释:元组具有性能优势。总而言之:

  • 为了计算元组的字面量,Python 编译器通过一次操作为元组常量生成字节码;对于列表字面量,生成的字节码又将每个项作为单独的常量推送到数据栈,然后构建列表。
  • 给定一个可散列的元组 t,tuple(t) 调用直接返回t的引用。根本无需复制,因为如果 t 是可散列的,则其值是固定的。相反,给定一个列表 l,list(l) 构造函数必须创建一个全新的l副本。
  • 由于元组的长度是固定的,其实例分配到的内存空间是确定的。而list的实例分配有空闲空间,用以应对将来发生的append操作。
  • 元组结构中的数组存储了元组中指向其各项的引用,而列表则包含一个指向存储在别的地方的引用数组的指针。间接寻址是必要的,因为当列表增长到超出当前分配的空间时,Python 需要重新分配引用数组以增加空间。额外的间接寻址降低了 CPU 缓存的效率

比较元组和列表方法

当使用元组作为列表的不可变变体时,最好知道它们的 API 很相似。正如您在表 2-1 中看到的那样,元组支持所有不涉及添加或删除项的列表方法,只有一个例外——元组缺少 __reversed__ 方法。然而,这只是为了优化;reversed(my_tuple) 可以正确执行。

Table 2-1. Methods and attributes found in list or tuple (methods implemented by object are omitted for brevity)
list tuple

s.__add__(s2)

s + s2—concatenation

s.__iadd__(s2)

s += s2—in-place concatenation

s.append(e)

Append one element after last

s.clear()

Delete all items

s.__contains__(e)

e in s

s.copy()

Shallow copy of the list

s.count(e)

Count occurrences of an element

s.__delitem__(p)

Remove item at position p

s.extend(it)

Append items from iterable it

s.__getitem__(p)

s[p]—get item at position

s.__getnewargs__()

Support for optimized serialization with pickle

s.index(e)

Find position of first occurrence of e

s.insert(p, e)

Insert element e before

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值