流畅的 Python 第二版(GPT 重译)(一)(1)https://developer.aliyun.com/article/1484364
特殊方法概览
Python 语言参考的"数据模型"章节列出了 80 多个特殊方法名。其中一半以上实现了算术、位运算和比较运算符。关于可用内容的概览,请参见下表。
表 1-1 展示了特殊方法名,不包括用于实现中缀运算符或核心数学函数(如abs
)的方法名。本书将涵盖其中大部分方法,包括最新增加的:异步特殊方法如 __anext__
(Python 3.5 新增),以及类定制钩子 __init_subclass__
(Python 3.6 新增)。
表 1-1. 特殊方法名(不包括运算符)
类别 | 方法名 |
字符串/字节表示 | __repr__ __str__ __format__ __bytes__ __fspath__ |
转换为数字 | __bool__ __complex__ __int__ __float__ __hash__ __index__ |
模拟集合 | __len__ __getitem__ __setitem__ __delitem__ __contains__ |
迭代 | __iter__ __aiter__ __next__ __anext__ __reversed__ |
可调用对象或协程执行 | __call__ __await__ |
上下文管理 | __enter__ __exit__ __aexit__ __aenter__ |
实例创建和销毁 | __new__ __init__ __del__ |
属性管理 | __getattr__ __getattribute__ __setattr__ __delattr__ __dir__ |
属性描述符 | __get__ __set__ __delete__ __set_name__ |
抽象基类 | __instancecheck__ __subclasscheck__ |
类元编程 | __prepare__ __init_subclass__ __class_getitem__ __mro_entries__ |
表 1-2 列出了中缀和数值运算符支持的特殊方法。其中最新的名称是 __matmul__
、__rmatmul__
和 __imatmul__
,于 Python 3.5 新增,用于支持 @
作为矩阵乘法的中缀运算符,我们将在第十六章看到。
表 1-2. 运算符的特殊方法名和符号
运算符类别 | 符号 | 方法名 |
一元数值运算 | - + abs() |
__neg__ __pos__ __abs__ |
富比较 | < <= == != > >= |
__lt__ __le__ __eq__ __ne__ __gt__ __ge__ |
算术运算 | + - * / // % @ divmod() round() ** pow() |
__add__ __sub__ __mul__ __truediv__ __floordiv__ __mod__ __matmul__ __divmod__ __round__ __pow__ |
反向算术运算 | (交换运算数的算术运算符) | __radd__ __rsub__ __rmul__ __rtruediv__ __rfloordiv__ __rmod__ __rmatmul__ __rdivmod__ __rpow__ |
增强赋值算术运算 | += -= *= /= //= %= @= **= |
__iadd__ __isub__ __imul__ __itruediv__ __ifloordiv__ __imod__ __imatmul__ __ipow__ |
位运算 | & | ^ << >> ~ |
__and__ __or__ __xor__ __lshift__ __rshift__ __invert__ |
反向位运算 | (交换运算数的位运算符) | __rand__ __ror__ __rxor__ __rlshift__ __rrshift__ |
增强赋值位运算 | &= |= ^= <<= >>= |
__iand__ __ior__ __ixor__ __ilshift__ __irshift__ |
注意
当第一个操作数的相应特殊方法无法使用时,Python 会在第二个操作数上调用反向运算符特殊方法。增强赋值是将中缀运算符与变量赋值组合的简写形式,例如 a += b
。
第十六章详细解释了反向运算符和增强赋值。
为什么 len 不是一个方法
我在 2013 年向核心开发者 Raymond Hettinger 提出了这个问题,他回答的关键是引用了"Python 之禅"中的一句话:"实用性胜过纯粹性。"在"特殊方法的使用方式"中,我描述了当 x
是内置类型的实例时,len(x)
的运行速度非常快。对于 CPython 的内置对象,不调用任何方法:长度直接从 C 结构体中的一个字段读取。获取集合中的项数是一种常见操作,必须为 str
、list
、memoryview
等基本且多样的类型高效地工作。
换句话说,len
之所以不作为方法调用,是因为它作为 Python 数据模型的一部分,与 abs
一样得到特殊对待。但是,借助特殊方法 __len__
,你也可以让 len
适用于你自己的自定义对象。这在内置对象的效率需求和语言的一致性之间取得了合理的平衡。正如"Python 之禅"所言:“特例不足以打破规则。”
注意
如果你认为 abs
和 len
是一元运算符,那么相比于在面向对象语言中期望的方法调用语法,你可能更倾向于原谅它们的函数外观和感觉。事实上,ABC 语言(Python 的直接祖先,开创了其许多特性)有一个相当于 len
的 #
运算符(你会写成 #s
)。当用作中缀运算符时,写作 x#s
,它会计算 x
在 s
中出现的次数,在 Python 中,对于任何序列 s
,都可以用 s.count(x)
获得。
章节总结
通过实现特殊方法,你的对象可以表现得像内置类型一样,从而实现社区认为 Pythonic 的表达性编码风格。
Python 对象的一个基本要求是提供自身的可用字符串表示,一个用于调试和日志记录,另一个用于呈现给终端用户。这就是为什么数据模型中存在特殊方法 __repr__
和 __str__
的原因。
如 FrenchDeck
示例所展示的,模拟序列是特殊方法最常见的用途之一。例如,数据库库通常以类序列集合的形式返回查询结果。第二章的主题是充分利用现有的序列类型。第十二章将介绍如何实现自己的序列,届时我们将创建 Vector
类的多维扩展。
得益于运算符重载,Python 提供了丰富的数值类型选择,从内置类型到 decimal.Decimal
、fractions.Fraction
,都支持中缀算术运算符。NumPy 数据科学库支持对矩阵和张量使用中缀运算符。第十六章将通过增强 Vector
示例来演示如何实现运算符,包括反向运算符和增强赋值。
本书贯穿始终介绍了 Python 数据模型中大多数剩余特殊方法的使用和实现。
延伸阅读
"数据模型"一章,摘自Python 语言参考手册,是本章以及本书大部分内容的权威来源。
Alex Martelli、Anna Ravenscroft 和 Steve Holden 合著的Python in a Nutshell, 3rd ed.(O’Reilly 出版)对数据模型有极佳的阐述。除了实际的 CPython C 源代码外,他们对属性访问机制的描述是我所见过最权威的。Martelli 也是 Stack Overflow 上的高产贡献者,贴出了超过 6,200 个答案。可以在 Stack Overflow 上看到他的用户资料。
David Beazley 有两本书在 Python 3 的背景下详细介绍了数据模型:Python Essential Reference,第 4 版(Addison-Wesley 出版),以及与 Brian K. Jones 合著的Python Cookbook,第 3 版(O’Reilly 出版)。
Gregor Kiczales、Jim des Rivieres 和 Daniel G. Bobrow 合著的The Art of the Metaobject Protocol(MIT 出版社)解释了元对象协议的概念,Python 数据模型就是其中一个例子。
¹ “Jython 的故事”,作为 Samuele Pedroni 和 Noel Rappin 合著的 Jython Essentials(O’Reilly 出版)的前言。
² C 结构体是一种带有命名字段的记录类型。
第二章:序列之阵
你可能已经注意到,提到的几个操作同样适用于文本、列表和表格。文本、列表和表格统称为 “序列”。[…]
FOR
命令也可以通用地作用于序列。Leo Geurts、Lambert Meertens 和 Steven Pembertonm,ABC Programmer’s Handbook¹
在创建 Python 之前,Guido 曾是 ABC 语言的贡献者——一个为初学者设计编程环境的 10 年研究项目。ABC 引入了许多我们现在认为 “Pythonic” 的想法:对不同类型序列的通用操作、内置元组和映射类型、缩进结构、无需变量声明的强类型等等。Python 如此用户友好并非偶然。
Python 从 ABC 继承了对序列的统一处理。字符串、列表、字节序列、数组、XML 元素和数据库结果共享一组丰富的通用操作,包括迭代、切片、排序和连接。
了解 Python 中可用的各种序列可以节省我们重复发明轮子的时间,它们的通用接口激励我们创建正确支持和利用现有和未来序列类型的 API。
本章大部分讨论适用于一般的序列,从熟悉的 list
到 Python 3 中新增的 str
和 bytes
类型。这里还涵盖了列表、元组、数组和队列的具体主题,但 Unicode 字符串和字节序列的详细信息出现在 第四章。此外,这里的想法是涵盖已准备好使用的序列类型。创建你自己的序列类型是 第十二章 的主题。
本章将主要涵盖以下主题:
- 列表推导式和生成器表达式基础
- 将元组用作记录与将元组用作不可变列表
- 序列解包和序列模式
- 从切片读取和向切片写入
- 专门的序列类型,如数组和队列
本章的更新内容
本章最重要的更新是 “使用序列进行模式匹配”。这是 Python 3.10 的新模式匹配特性在第二版中首次出现。
其他变化不是更新,而是对第一版的改进:
- 序列内部结构的新图和描述,对比容器和扁平序列
- 简要比较
list
和tuple
的性能和存储特性 - 包含可变元素的元组的注意事项,以及如何在需要时检测它们
我将命名元组的介绍移至 第五章 的 “经典命名元组”,在那里它们与 typing.NamedTuple
和 @dataclass
进行了比较。
注意
为了给新内容腾出空间并将页数控制在合理范围内,第一版中的 “使用 Bisect 管理有序序列” 一节现在是 fluentpython.com 配套网站中的一篇文章。
内置序列概述
标准库提供了丰富的用 C 实现的序列类型选择:
容器序列
可以容纳不同类型的项目,包括嵌套容器。一些示例:list
、tuple
和 collections.deque
。
扁平序列
持有一种简单类型的项目。一些示例:str
、bytes
和 array.array
。
容器序列存储对其所包含的对象的引用,这些对象可以是任何类型,而扁平序列则在其自身的内存空间中存储其内容的值,而不是作为独立的 Python 对象。参见图 2-1。
图 2-1. 一个tuple
和一个array
的简化内存图,每个包含三个项目。灰色单元格表示每个 Python 对象的内存头——没有按比例绘制。tuple
有一个对其项目的引用数组。每个项目都是一个单独的 Python 对象,可能包含对其他 Python 对象的引用,比如那个两个项目的列表。相比之下,Python array
是一个单一的对象,包含一个 C 语言的三个 double 数组。
因此,扁平序列更紧凑,但它们仅限于保存字节、整数和浮点数等原始机器值。
注意
内存中的每个 Python 对象都有一个带有元数据的头部。最简单的 Python 对象float
有一个值字段和两个元数据字段:
ob_refcnt
:对象的引用计数ob_type
:指向对象类型的指针ob_fval
:一个 Cdouble
,用于保存float
的值
在 64 位 Python 构建中,这些字段中的每一个都占用 8 个字节。这就是为什么一个浮点数组比一个浮点元组更紧凑:数组是一个单一的对象,包含浮点数的原始值,而元组由多个对象组成——元组本身和其中包含的每个float
对象。
对序列类型进行分组的另一种方式是按可变性:
可变序列
例如,list
、bytearray
、array.array
和collections.deque
。
不可变序列
例如,tuple
、str
和bytes
。
图 2-2 有助于可视化可变序列如何继承不可变序列的所有方法,并实现几个额外的方法。内置的具体序列类型实际上并没有子类化Sequence
和MutableSequence
抽象基类(ABC),但它们是注册到这些 ABC 的虚拟子类——我们将在第十三章中看到。作为虚拟子类,tuple
和list
通过了这些测试:
>>> from collections import abc >>> issubclass(tuple, abc.Sequence) True >>> issubclass(list, abc.MutableSequence) True
图 2-2.collections.abc 中一些类的简化 UML 类图(超类在左侧;继承箭头从子类指向超类;斜体名称是抽象类和抽象方法)。
记住这些共同特征:可变与不可变;容器与扁平。它们有助于将你对一种序列类型的了解推广到其他类型。
最基本的序列类型是list
:一个可变容器。我希望你非常熟悉列表,所以我们将直接进入列表推导式,这是一种构建列表的强大方式,但有时会因为语法一开始看起来不寻常而被低估。掌握列表推导式为生成器表达式打开了大门,生成器表达式除了其他用途外,还可以生成元素来填充任何类型的序列。这两者都是下一节的主题。
列表推导式和生成器表达式
构建序列的一个快速方法是使用列表推导式(如果目标是list
)或生成器表达式(对于其他类型的序列)。如果你没有每天使用这些语法形式,我敢打赌你正在错失编写更易读且通常更快的代码的机会。
如果你怀疑我声称这些构造"更具可读性",请继续阅读。我会试着说服你。
提示
为了简洁起见,许多 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. 使用列表推导式从字符串构建 Unicode 码点列表
>>> symbols = '$¢£¥€¤' >>> codes = [ord(symbol) for symbol in symbols] >>> codes [36, 162, 163, 165, 8364, 164]
任何稍微了解 Python 的人都可以读懂示例 2-1。然而,在学习了列表推导式之后,我发现示例 2-2 更具可读性,因为它的意图很明确。
for
循环可用于执行许多不同的事情:扫描序列以计数或选择项目、计算聚合(总和、平均值)或任何其他任务。示例 2-1 中的代码正在构建一个列表。相比之下,列表推导式更加明确。它的目标总是构建一个新列表。
当然,也可能滥用列表推导式来编写真正难以理解的代码。我见过 Python 代码,其中列表推导式仅用于重复代码块以产生副作用。如果你不对生成的列表做任何事情,就不应该使用该语法。此外,尽量保持简短。如果列表推导式跨越两行以上,最好将其拆开或重写为普通的for
循环。运用你的最佳判断:对于 Python,就像对于英语一样,没有明确的清晰写作规则。
语法提示
在 Python 代码中,在[]
、{}
或()
对之间的换行符会被忽略。因此,你可以构建多行列表、列表推导式、元组、字典等,而无需使用\
换行转义符,如果不小心在其后键入空格,它将不起作用。此外,当这些分隔符对用于定义包含以逗号分隔的一系列项的字面量时,尾随逗号将被忽略。因此,例如,在编写多行列表字面量时,在最后一项后面加上逗号是很周到的,这会让下一个编码者更容易向该列表添加一个项目,并在阅读差异时减少噪音。
列表推导式通过过滤和转换项目从序列或任何其他可迭代类型构建列表。内置的filter
和map
可以组合起来做同样的事情,但可读性会受到影响,我们接下来会看到。
列表推导式与 map 和 filter 的对比
列表推导式可以完成map
和filter
函数所做的一切,而无需功能受限的 Python lambda
的扭曲。考虑示例 2-3。
示例 2-3. 通过列表推导式和 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
比等效的列表推导式更快,但 Alex Martelli 指出事实并非如此——至少在前面的示例中不是。Fluent Python代码仓库中的02-array-seq/listcomp_speed.py脚本是一个简单的速度测试,比较了列表推导式与filter/map
。
在第七章中,我将对map
和filter
进行更多说明。现在我们来看看如何使用列表推导式计算笛卡尔积:一个包含由两个或多个列表中所有项构建的元组的列表。
笛卡尔积
列表推导式可以从两个或多个可迭代对象的笛卡尔积构建列表。构成笛卡尔积的项是由每个输入可迭代对象的项构成的元组。结果列表的长度等于输入可迭代对象的长度相乘。参见图 2-3。
图 2-3. 3 个牌面和 4 个花色的笛卡尔积是由 12 对组成的序列。
例如,假设你需要生成一个包含两种颜色和三种尺寸的 T 恤列表。示例 2-4 展示了如何使用列表推导式生成该列表。结果有六个项目。
示例 2-4. 使用列表推导式的笛卡尔积
>>> colors = ['black', 'white'] >>> sizes = ['S', 'M', 'L'] >>> tshirts = [(color, size) for color in colors for size in sizes] # ① >>> tshirts [('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'), ('white', 'M'), ('white', 'L')] >>> for color in colors: # ② ... 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')]
①
这会生成一个按颜色再按大小排列的元组列表。
②
注意结果列表的排列方式,就好像for
循环按照它们在列表推导式中出现的顺序嵌套一样。
③
要按大小再按颜色排列项目,只需重新排列for
子句;在列表推导式中添加一个换行,可以更容易地看出结果的排序方式。
在示例 1-1(第一章)中,我使用以下表达式初始化一副由 4 种花色的 13 种牌面组成的 52 张牌的扑克牌,按花色和点数排序:
self._cards = [Card(rank, suit) for suit in self.suits for rank in self.ranks]
列表推导式是一招鲜吃遍天:它们构建列表。要为其他序列类型生成数据,生成器表达式是不二之选。下一节将简要介绍在构建非列表序列的上下文中使用生成器表达式。
生成器表达式
要初始化元组、数组和其他类型的序列,你也可以从列表推导式开始,但生成器表达式可以节省内存,因为它使用迭代器协议一个接一个地产生项目,而不是构建一个完整的列表来馈送另一个构造函数。
生成器表达式使用与列表推导式相同的语法,但用括号括起来,而不是方括号。
示例 2-5 展示了使用生成器表达式构建元组和数组的基本用法。
示例 2-5. 从生成器表达式初始化元组和数组
>>> symbols = '$¢£¥€¤' >>> tuple(ord(symbol) for symbol in symbols) # ① (36, 162, 163, 165, 8364, 164) >>> import array >>> array.array('I', (ord(symbol) for symbol in symbols)) # ② array('I', [36, 162, 163, 165, 8364, 164])
①
如果生成器表达式是函数调用中的唯一参数,则不需要复制括号。
②
array
构造函数接受两个参数,因此生成器表达式周围的括号是必需的。array
构造函数的第一个参数定义了用于数组中数字的存储类型,我们将在"数组"中看到。
示例 2-6 使用笛卡尔积中的生成器表达式打印出三种尺寸两种颜色的 T 恤衫名册。与示例 2-4 相比,这里从未在内存中构建六个 T 恤衫的列表:生成器表达式每次产生一个项目来馈送 for
循环。如果笛卡尔积中使用的两个列表每个都有一千个项目,使用生成器表达式就可以节省构建一个包含一百万个项目的列表的成本,而这个列表只是用来馈送 for
循环。
示例 2-6. 生成器表达式中的笛卡尔积
>>> colors = ['black', 'white'] >>> sizes = ['S', 'M', 'L'] >>> for tshirt in (f'{c} {s}' for c in colors for s in sizes): # ① ... print(tshirt) ... black S black M black L white S white M white L
①
生成器表达式一个接一个地产生项目;在此示例中,从未生成包含所有六种 T 恤衫变体的列表。
注意
第十七章详细解释了生成器的工作原理。这里的想法只是展示如何使用生成器表达式来初始化列表以外的序列,或生成不需要保存在内存中的输出。
现在我们继续讨论 Python 中另一个基本的序列类型:元组。
元组不仅仅是不可变的列表
一些介绍 Python 的入门文本将元组描述为"不可变的列表",但这并没有充分利用它们。元组具有双重功能:它们可以用作不可变列表,也可以用作没有字段名的记录。这种用法有时会被忽略,所以我们将从这里开始。
元组作为记录
元组保存记录:元组中的每一项保存一个字段的数据,项目的位置赋予了它含义。
如果将元组视为不可变列表,则根据上下文,项目的数量和顺序可能重要,也可能不重要。但是在将元组用作字段集合时,项目的数量通常是固定的,它们的顺序始终很重要。
示例 2-7 显示了用作记录的元组。请注意,在每个表达式中,对元组进行排序都会破坏信息,因为每个字段的含义由其在元组中的位置给出。
示例 2-7. 元组用作记录
>>> lax_coordinates = (33.9425, -118.408056) # ① >>> city, year, pop, chg, area = ('Tokyo', 2003, 32_450, 0.66, 8014) # ② >>> traveler_ids = ('USA', '31195855'), ('BRA', 'CE342567'), ![3 ... ('ESP', 'XDA205856')] >>> for passport in sorted(traveler_ids): # ④ ... print('%s/%s' % passport) # ⑤ ... BRA/CE342567 ESP/XDA205856 USA/31195855 >>> for country, _ in traveler_ids: # ⑥ ... print(country) ... USA BRA ESP
①
洛杉矶国际机场的纬度和经度。
②
关于东京的数据:名称、年份、人口(千人)、人口变化(%)和面积(平方公里)。
③
形式为 (country_code, passport_number)
的元组列表。
④
当我们遍历列表时,passport
绑定到每个元组。
⑤
%
格式化运算符理解元组,并将每个项视为单独的字段。
⑥
for
循环知道如何分别检索元组的项,这称为"解包"。这里我们对第二个项不感兴趣,所以将其赋值给虚拟变量_
。
提示
通常,使用_
作为虚拟变量只是一种约定。它只是一个奇怪但有效的变量名。但是,在match/case
语句中,_
是一个通配符,可以匹配任何值,但不会绑定到一个值。参见"使用序列进行模式匹配"。在 Python 控制台中,前一个命令的结果被赋值给_
,除非结果是None
。
我们通常认为记录是具有命名字段的数据结构。第五章介绍了两种创建具有命名字段的元组的方法。
但通常没有必要费力创建一个类来命名字段,尤其是如果你利用解包并避免使用索引访问字段。在示例 2-7 中,我们在一条语句中将('Tokyo', 2003, 32_450, 0.66, 8014)
赋值给city, year, pop, chg, area
。然后,%
运算符将passport
元组中的每一项分配给print
参数中格式字符串的相应位置。这是元组解包的两个例子。
注意
术语元组解包被 Pythonista 广泛使用,但可迭代解包正在获得关注,如PEP 3132 — 扩展可迭代解包的标题所示。
"解包序列和可迭代对象"不仅详细介绍了元组的解包,还包括序列和可迭代对象的解包。
现在让我们将tuple
类视为list
类的不可变变体。
元组作为不可变列表
Python 解释器和标准库广泛使用元组作为不可变列表,你也应该这样做。这带来了两个主要好处:
清晰度
当你在代码中看到tuple
时,你知道它的长度永远不会改变。
性能
与相同长度的list
相比,tuple
使用更少的内存,并允许 Python 进行一些优化。
但是,请注意tuple
的不可变性仅适用于它所包含的引用。元组中的引用不能被删除或替换。但是,如果其中一个引用指向一个可变对象,并且该对象发生了变化,那么tuple
的值就会改变。下面的代码片段通过创建两个最初相等的元组a
和b
来说明这一点。图 2-4 表示内存中b
元组的初始布局。
图 2-4。元组本身的内容是不可变的,但这只意味着元组持有的引用将始终指向相同的对象。但是,如果其中一个引用对象是可变的(如列表),其内容可能会发生变化。
当b
中的最后一项发生变化时,b
和a
变得不同:
>>> 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])
包含可变项的元组可能是 bug 的根源。正如我们将在"什么是可哈希的"中看到的,一个对象只有在其值不能改变时才是可哈希的。不可哈希的元组不能插入为dict
键或set
元素。
如果你想明确确定一个元组(或任何对象)是否具有固定值,可以使用内置的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 回答"在 Python 中元组比列表更高效吗?"时解释了元组提供的一些性能优势。总结一下,Hettinger 写道:
- 为了评估元组字面量,Python 编译器在一个操作中为元组常量生成字节码;但是对于列表字面量,生成的字节码将每个元素作为单独的常量推送到数据栈,然后构建列表。
- 给定元组
t
,tuple(t)
只是返回对同一个t
的引用。没有必要复制。相比之下,给定列表l
,list(l)
构造函数必须创建l
的新副本。 - 由于具有固定长度,
tuple
实例分配它需要的确切内存空间。另一方面,list
的实例分配时会留有余地,以分摊将来追加的成本。 - 元组中元素的引用存储在元组结构中的数组中,而列表在其他地方保存指向引用数组的指针。当列表增长超过当前分配的空间时,Python 需要重新分配引用数组以腾出空间,因此需要间接寻址。额外的间接寻址使 CPU 缓存效率降低。
比较元组和列表方法
当使用元组作为list
的不可变变体时,了解它们的 API 有多相似是很好的。如表 2-1 所示,除了一个例外,tuple
支持所有不涉及添加或删除元素的list
方法——tuple
缺少__reversed__
方法。但是,这只是为了优化;reversed(my_tuple)
可以在没有它的情况下工作。
表 2-1. 在list
或tuple
中找到的方法和属性(为简洁起见,省略了对象实现的方法)
list | tuple | ||
s.__add__(s2) |
● | ● | s + s2 —连接 |
s.__iadd__(s2) |
● | s += s2 —原地连接 |
|
s.append(e) |
● | 在最后追加一个元素 | |
s.clear() |
● | 删除所有元素 | |
s.__contains__(e) |
● | ● | e in s |
s.copy() |
● | 列表的浅拷贝 | |
s.count(e) |
● | ● | 计算元素出现的次数 |
s.__delitem__(p) |
● | 移除位置p 处的元素 |
|
s.extend(it) |
● | 从可迭代对象it 追加元素 |
|
s.__getitem__(p) |
● | ● | s[p] —获取位置p 处的元素 |
s.__getnewargs__() |
● | 支持使用pickle 进行优化的序列化 |
|
s.index(e) |
● | ● | 查找e 第一次出现的位置 |
s.insert(p, e) |
● | 在位置p 的元素之前插入元素e |
|
s.__iter__() |
● | ● | 获取迭代器 |
s.__len__() |
● | ● | len(s) —元素的数量 |
s.__mul__(n) |
● | ● | s * n —重复连接 |
s.__imul__(n) |
● | s *= n —原地重复连接 |
|
s.__rmul__(n) |
● | ● | n * s —反向重复连接^(a) |
s.pop([p]) |
● | 移除并返回最后一个元素或位置p 处的可选元素 |
|
s.remove(e) |
● | 按值移除元素e 的第一次出现 |
|
s.reverse() |
● | 原地反转元素的顺序 | |
s.__reversed__() |
● | 获取从最后到第一个元素的迭代器 | |
s.__setitem__(p, e) |
● | s[p] = e —将e 放在位置p ,覆盖现有元素^(b) |
|
s.sort([key], [reverse]) |
● | 原地排序,可选关键字参数key 和reverse |
|
(a)反向运算符在第十六章中解释。(b)也用于覆盖子序列。参见"赋值给切片"。 |
现在让我们切换到 Python 编程中一个重要的主题:元组、列表和可迭代对象解包。
流畅的 Python 第二版(GPT 重译)(一)(3)https://developer.aliyun.com/article/1484366