现代 Python 秘籍(三)
原文:
zh.annas-archive.org/md5/185a6e8218e2ea258a432841b73d4359
译者:飞龙
第四章:内置数据结构 - 列表、集合、字典
在本章中,我们将研究以下内容:
-
选择数据结构
-
构建列表 - 文字、附加和理解
-
切片和切割列表
-
从列表中删除 - 删除、移除、弹出和过滤
-
反转列表的副本
-
使用集合方法和运算符
-
从集合中删除项目 - remove(),pop()和 difference
-
创建字典 - 插入和更新
-
从字典中删除 - pop()方法和 del 语句
-
控制字典键的顺序
-
在 doctest 示例中处理字典和集合
-
理解变量、引用和赋值
-
制作对象的浅层和深层副本
-
避免函数参数的可变默认值
介绍
Python 具有丰富的内置数据结构。这些内置结构通常用于进行大量有用的编程。这些集合涵盖了各种常见情况。
我们将概述可用的各种结构以及它们解决的问题。从那里,我们可以详细了解列表、字典和集合。
请注意,我们将内置的元组和字符串设置为与列表结构不同。它们有一些重要的相似之处,也有一些不同之处。在第一章中,数字、字符串和元组,我们强调了字符串和元组的行为更像不可变的数字,而不是可变的集合。
我们还将研究一些与 Python 处理对象引用相关的更高级的主题。我们还将研究与这些数据结构的可变性相关的一些问题。
选择数据结构
Python 提供了许多内置数据结构,帮助我们处理数据集合。确定哪种数据结构适合特定目的可能会令人困惑。
我们如何选择要使用的结构?列表、集合和字典有哪些特点?为什么有元组和冻结集?
准备就绪
在将数据放入集合之前,我们需要考虑如何收集数据,以及一旦我们拥有了集合,我们将如何处理它。最重要的问题始终是我们将如何识别集合中的特定项目。
我们将研究一些需要回答的关键问题。
如何做...
- 编程是否专注于执行成员资格测试?其中一个例子是有效输入值的集合。当用户输入集合中的内容时,他们的输入是有效的,否则是无效的。
简单成员资格建议使用set
:
valid_inputs = {"yes", "y", "no", "n"}
answer = None
while answer not in valid_inputs:
answer = input("Continue? [y, n] ").lower()
set
不按特定顺序保存项目。一旦项目是成员,我们就无法再次添加它:
**>>> valid_inputs = {"yes", "y", "no", "n"}
>>> valid_inputs.add("y")
>>> valid_inputs
{'no', 'y', 'n', 'yes'}**
我们创建了一个名为valid_inputs
的集合,其中包含四个不同的字符串项目。我们不能向已包含y
的集合中再添加y
。集合的内容不会改变。
还要注意,集合中项目的顺序并不完全与我们最初提供它们的顺序相同。集合无法保持任何特定的项目顺序,它只能确定集合中是否存在某个项目。
- 我们是否将通过其在集合中的位置来识别项目?一个例子包括输入文件中的行 - 行号是其在集合中的位置。
当我们必须使用索引或位置来标识项目时,我们必须使用list
:
**>>> month_name_list = ["Jan", "Feb", "Mar", "Apr",
... "May", "Jun", "Jul", "Aug",
... "Sep", "Oct", "Nov", "Dec"]
>>> month_name_list[8]
"Sep"
>>> month_name_list.index("Feb")
1**
我们创建了一个名为month_name_list
的列表,其中包含 12 个字符串项目。我们可以通过提供其位置来选择一个项目。我们还可以使用index()
方法来定位列表中项目的索引。
Python 中的列表始终从位置零开始。元组和字符串也是如此。
如果集合中的项目数量是固定的 - 例如 RGB 颜色有三个值 - 那么我们可能会考虑使用tuple
而不是list
。如果项目数量会增长和变化,那么list
集合比tuple
集合更好。
- 我们将通过一个不是项目位置的键来识别集合中的项目吗? 一个例子可能包括字符串之间的映射 - 单词和表示这些单词频率的整数之间的映射,或者颜色名称和该颜色的 RGB 元组之间的映射。
当我们必须使用非位置键标识项目时,我们使用某种映射。内置映射是dict
。有几个扩展可以添加更多功能:
**>>> scheme = {"Crimson": (220, 14, 60),
... "DarkCyan": (0, 139, 139),
... "Yellow": (255, 255, 00)}
>>> scheme['Crimson']
(220, 14, 60)**
在这个字典中,scheme
,我们创建了从颜色名称到 RGB 颜色元组的映射。当我们使用一个键,例如"Crimson"
,我们可以检索绑定到该键的值。
- 考虑
set
集合中项目的可变性和dict
集合中的键。集合中的每个项目必须是不可变对象。数字、字符串和元组都是不可变的,可以收集到集合中。由于list
、dict
或set
对象是可变的,它们不能作为集合中的项目。例如,无法构建list
项目的set
。
而不是创建list
项目的set
,我们可以将每个list
项目转换为不可变的tuple
。我们可以创建不可变的tuple
项目的set
。
同样,字典键必须是不可变的。我们可以使用数字、字符串或元组作为字典键。我们不能使用list
、set
或另一个可变映射作为字典键。
它是如何工作的...
Python 的每个内置集合都提供一组特定的独特功能。这些集合还提供了大量重叠的功能。对于刚接触 Python 的程序员来说,挑战在于识别每个集合的独特功能。
事实证明,collections.abc
模块提供了一种通过内置集合的路线图。collections.abc
模块定义了支持我们使用的具体类的抽象基类(ABC)。我们将使用这组定义中的名称来指导我们了解这些功能。
从 ABC 中,我们可以看到实际上有六种不同类型的集合:
-
集合:独特的特点是项目要么是成员,要么不是。这意味着无法处理重复项:
-
可变集合:
set
集合 -
不可变集合:
frozenset
集合 -
序列:独特的特点是项目提供了索引位置:
-
可变序列:
list
集合 -
不可变序列:
tuple
集合 -
映射:独特的特点是每个项目都有一个指向值的键:
-
可变映射:
dict
集合 -
不可变映射:有趣的是,没有内置的冻结映射
Python 的库提供了大量这些核心集合类型的附加实现。我们可以在Python 标准库中看到许多这些。
collections
模块包含许多内置集合的变体。这些包括:
-
namedtuple
:为元组中的每个项目提供名称的tuple
。使用rgb_color.red
比rgb_color[0]
更清晰一些。 -
deque
:双端队列。它是一个可变序列,具有从每一端推送和弹出的优化。我们可以使用list
做类似的事情,但deque
更有效。 -
defaultdict
:可以为缺失的键提供默认值的dict
。 -
Counter
:旨在计算键出现次数的dict
。有时被称为多重集或袋子。 -
OrderedDict
:保留创建键的顺序的dict
。 -
ChainMap
:将几个字典组合成单个映射的dict
。
在Python 标准库中还有更多。我们还可以使用heapq
模块,该模块定义了优先级队列实现。bisect
模块包括快速搜索排序列表的方法。这使得列表的性能更接近于字典的快速查找。
还有更多...
我们可以查看这样的数据结构列表:en.wikipedia.org/wiki/List_of_data_structures
。
有一些重要的摘要是数据结构的一部分。文章的不同部分提供了略有不同的数据结构摘要。我们将快速浏览四个分类。
-
数组:有变体实现提供类似的功能。Python 的
list
结构是典型的,并且提供了类似于数组的链表实现的性能。 -
树:通常,树结构可以用来创建集合、顺序列表或键值映射。我们可以将树看作是一种实现技术,而不是具有独特特征集的数据结构。
-
哈希:Python 使用哈希来实现字典和集合。这导致速度快但内存消耗大。
-
图表:Python 没有内置的图表数据结构。然而,我们可以用一个字典来轻松表示图表结构,其中每个节点都有一个相邻节点的列表。
我们可以——稍微聪明一点——在 Python 中实现几乎任何类型的数据结构。要么内置结构具有基本特征,要么我们可以找到一个内置结构,可以被利用起来。
另请参阅
- 有关高级图形操作,请参阅
networkx.github.io
。
构建列表-文字,附加和推导
如果我们决定创建一个使用项目位置的集合-list
,我们有几种构建这个结构的方法。我们将看一些我们可以从单个项目构建list
对象的方法。
在某些情况下,我们需要一个列表,因为它允许重复的值。许多统计操作不需要知道项目的位置。对于这个,多重集将是有用的,但我们没有这个作为内置结构;使用list
而不是多重集是非常常见的。
准备工作
假设我们需要对一些文件大小进行一些统计分析。下面是一个简短的脚本,将为我们提供一些文件的大小:
**>>> import pathlib
>>> home = pathlib.Path('source')
>>> for path in home.glob('*/index.rst'):
... print(path.stat().st_size, path.parent)
2353 source/ch_01_numbers_strings_and_tuples
2889 source/ch_02_statements_and_syntax
2195 source/ch_03_functions
3094 source/ch_04_built_in_data_structures_list_tuple_set_dict
725 source/ch_05_user_inputs_and_outputs
1099 source/ch_06_basics_of_classes_and_objects
690 source/ch_07_more_advanced_class_design
1207 source/ch_08_functional_programming_features
926 source/ch_09_input_output_physical_format_logical_layout
758 source/ch_10_statistical_programming_and_linear_regression
615 source/ch_11_testing
521 source/ch_12_web_services
1320 source/ch_13_application_integration**
我们使用了pathlib.Path
对象来表示文件系统中的目录。glob()
方法扩展与给定模式匹配的所有名称。在这种情况下,我们使用了一个模式'*/index.rst'
。我们可以使用for
语句从文件的 OSstat
数据中显示大小。
我们想要累积一个具有各种文件大小的list
对象。从中我们可以计算总大小和平均大小。我们可以寻找看起来太大或太小的文件。
我们有四种创建list
对象的方法:
- 我们可以使用一系列值围绕在
[]
字符中来创建list
的文字显示。它看起来像这样:[value, ...]
。Python 需要匹配[
和]
来看到一个完整的逻辑行,因此文字可以跨越物理行。有关更多信息,请参阅第二章中的编写长行代码配方,语句和语法。
**[2353, 2889, 2195, 3094, 725,
1099, 690, 1207, 926, 758,
615, 521, 1320]**
-
我们可以使用
list()
函数将其他数据集转换为列表。我们可以转换set
,或dict
的键,或dict
的值。我们将在Slicing and dicing a list配方中看到一个更复杂的例子。 -
我们有一些
list
方法,允许我们一次构建一个list
。这些方法包括append()
,extend()
和insert()
。我们将在本配方的使用 append()方法构建列表部分中查看append()
。我们将在本配方的还有更多...部分中查看其他方法。 -
我们有生成器表达式,可以用来构建
list
对象。一种生成器是列表推导。
如何做...
使用 append()方法构建列表
- 创建一个空列表,
[]
:
**>>> file_sizes = []**
- 通过一些数据源进行迭代。使用
append()
方法将项目附加到列表中:
**>>> home = pathlib.Path('source')
>>> for path in home.glob('*/index.rst'):
... file_sizes.append(path.stat().st_size)
>>> print(file_sizes)
[2353, 2889, 2195, 3094, 725, 1099, 690,
1207, 926, 758, 615, 521, 1320]
>>> print(sum(file_sizes))
18392**
我们使用路径的glob()
方法来查找与给定模式匹配的所有文件。路径的stat()
方法提供了包括大小st_size
在内的 OS stat数据结构,以字节为单位。
当我们打印list
时,Python 会以文字表示法显示它。如果我们需要复制并粘贴列表到另一个脚本中,这很方便。
非常重要的是要注意,append()
方法不返回值。append()
方法改变了list
对象,并且不返回任何东西。
提示
通常,任何改变对象的方法都没有返回值。像append()
,extend()
,sort()
和reverse()
这样的方法没有返回值。它们调整list
对象本身的结构。
append()
方法不返回值。
它会改变list
对象。
令人惊讶的是,经常会看到错误的代码,像这样:a = ['some', 'data']
a = a.append('more data')
这是错误的。这将把a
设置为None
。
正确的方法是这样的陈述,没有任何额外的赋值:
a.append('more data')
编写一个列表推导
列表推导的目标是创建一个对象,其语法角色类似于列表文字:
-
编写包围列表对象的
[]
括号。 -
编写数据的来源。这将包括目标变量。请注意,末尾没有
:
,因为我们不是在写一个完整的语句:
for path in home.glob('*/index.rst')
- 在这个表达式之前加上要评估的目标变量的每个值。同样,由于这是一个简单的表达式,我们不能在这里使用复杂的语句:
path.stat().st_size
for path in home.glob('*/index.rst')
在某些情况下,我们需要添加一个过滤器。这是在for
子句之后的if
子句。我们可以使生成器表达式非常复杂。
这是整个list
对象:
**>>> [path.stat().st_size
... for path in home.glob('*/index.rst')]
[2353, 2889, 2195, 3094, 725, 1099, 690, 1207, 926, 758, 615, 521, 1320]**
现在我们已经创建了一个list
对象,我们可以将其分配给一个变量,并对数据进行其他计算和总结。
列表推导包括一个生成器表达式,称为语言手册中的推导。生成器表达式是附加到for
子句的数据表达式。由于这个生成器是一个表达式,而不是一个完整的语句,它有一些限制。数据表达式会被重复评估,并由for
子句控制。
使用生成器表达式的列表函数
我们将创建一个使用生成器表达式的list
函数:
-
编写包围生成器表达式的
list()
函数。 -
我们将重用列表推导版本的步骤二和步骤三来创建一个生成器表达式。这是生成器表达式:
path.stat().st_size
for path in home.glob('*/index.rst')
这是整个列表对象:
**>>> list(path.stat().st_size
... for path in home.glob('*/index.rst'))
[2353, 2889, 2195, 3094, 725, 1099, 690, 1207, 926, 758, 615, 521, 1320]**
工作原理...
Python 的list
对象具有动态大小。当添加或插入项目,或者使用另一个list
扩展list
时,数组的边界会调整。同样,当弹出或删除项目时,边界会收缩。我们可以非常快速地访问任何项目,访问速度不取决于列表的大小。
在一些罕见的情况下,我们可能需要创建一个具有给定初始大小的list
,然后分别设置项目的值。我们可以使用类似于这样的列表推导来实现:
some_list = [None for i in range(100)]
这将创建一个初始大小为 100 个项目的列表,每个项目都是None
。尽管很少需要这样做,因为列表可以根据需要增长。
列表推导语法和list()
函数都会从生成器中消耗项目并将它们附加到创建一个新的list
对象。
还有更多...
我们创建list
对象的目标是能够对其进行总结。我们可以使用各种 Python 函数来实现这一点。以下是一些例子:
**>>> sizes = list(path.stat().st_size
... for path in home.glob('*/index.rst'))
>>> sum(sizes)
18392
>>> max(sizes)
3094
>>> min(sizes)
521
>>> from statistics import mean
>>> round(mean(sizes), 3)
1414.769**
我们已经使用了内置的sum()
,min()
和max()
来生成这些文档大小的一些描述性统计数据。这些索引文件中哪一个是最小的?我们想知道值列表中最小值的位置。我们可以使用index()
方法来实现:
**>>> sizes.index(min(sizes))
11**
我们已经找到了最小值,然后使用index()
方法来找到该最小值的位置。请记住,索引值从零开始,因此最小的文件是第十二章的文件。
其他扩展列表的方法
我们还可以扩展列表,以及在列表的中间或开头插入。我们有两种方法来扩展列表:我们可以使用+
运算符,也可以使用extend()
方法。以下是一个创建两个列表并使用+
将它们放在一起的示例:
**>>> ch1 = list(path.stat().st_size
... for path in home.glob('ch_01*/*.rst'))
>>> ch2 = list(path.stat().st_size
... for path in home.glob('ch_02*/*.rst'))
>>> len(ch1)
13
>>> len(ch2)
12
>>> final = ch1 + ch2
>>> len(final)
25
>>> sum(final)
104898**
我们已经创建了一个包含名称为ch_01*/*.rst
的文档大小的列表。然后我们创建了一个包含稍有不同名称模式ch_02*/*.rst
的文档大小的第二个列表。然后我们将这两个列表合并成一个最终列表。
我们也可以使用extend()
方法来做到这一点。我们将重复使用这两个列表,并从中构建一个新列表:
**>>> final_ex = []
>>> final_ex.extend(ch1)
>>> final_ex.extend(ch2)
>>> len(final_ex)
25
>>> sum(final_ex)
104898**
我们注意到append()
不返回值。请注意,extend()
也不返回值。extend()
方法会改变list
对象。
我们还可以在列表中的任何特定位置之前插入一个值。insert()
方法接受一个项目的位置;新值将在给定位置之前:
**>>> p = [3, 5, 11, 13]
>>> p.insert(0, 2)
>>> p
[2, 3, 5, 11, 13]
>>> p.insert(3, 7)
>>> p
[2, 3, 5, 7, 11, 13]**
我们已经向list
对象插入了两个新值。与append()
和extend()
一样,insert()
也不返回值。它会改变list
对象。
另请参阅
-
请参阅切片和切块列表的方法,了解复制列表和从列表中选择子列表的方法。
-
请参阅从列表中删除 - 删除、移除、弹出和过滤的方法,以了解从列表中删除项目的其他方法。
-
在反转列表的副本的方法中,我们将研究如何反转列表。
-
本文介绍了 Python 集合内部工作的一些见解:
wiki.python.org/moin/TimeComplexity
。在查看表格时,重要的是要注意O(1)表示成本基本上是恒定的,而O(n)表示成本随着我们尝试处理的项目的索引而变化。这意味着成本随着集合的大小而增加。
切片和切块列表
有许多时候我们想从列表中挑选项目。最常见的一种处理方式是将列表的第一项视为特殊情况。这导致了一种头尾处理,我们将列表的头部与列表尾部的项目区别对待。
我们还可以使用这些技术来制作列表的副本。
准备就绪
我们有一个用于记录大型帆船燃油消耗的电子表格。它的行看起来像这样:
日期 | 发动机启动 | 燃油高度 |
---|---|---|
发动机关闭 | ||
其他注意事项 | ||
10/25/2013 08:24 29 | ||
13:15 27 | ||
风平浪静 - 锚在所罗门岛 | ||
10/26/2013 09:12 27 | ||
18:25 22 | ||
颠簸 - 锚在杰克逊溪 |
燃油高度?是的。没有浮标传感器来估计油箱中的燃油水平。相反,有一个视觉量规,可以直接观察燃油。它以深度英寸为单位进行校准。在实际情况下,油箱是矩形的,因此显示的深度可以很容易地转换为体积 - 31 英寸的深度约为 75 加仑。
重要的是,电子表格数据没有得到适当的规范化。理想情况下,每行都遵循数据的第一正规形式,每行具有相同的内容,每个单元格只有原子值。
我们的数据没有得到适当的规范化。我们有四行标题。这是csv
模块无法直接处理的。我们需要做一些切片来删除其他注意事项中的行。我们希望将每天旅行的两行合并在一起,以便更容易计算经过的时间和使用的英寸数。
我们可以这样读取数据:
**>>> from pathlib import Path
>>> import csv
>>> with Path('code/fuel.csv').open() as source_file:
... reader = csv.reader(source_file)
... log_rows = list(reader)
>>> log_rows[0]
['date', 'engine on', 'fuel height']
>>> log_rows[-1]
['', "choppy -- anchor in jackson's creek", '']**
我们使用了csv
模块来读取日志详情。csv.reader()
是一个可迭代对象。为了将项目收集到一个单独的列表中,我们应用了list()
函数。我们查看了列表中的第一个和最后一个项目,以确认我们确实有一个列表的列表结构。
原始 CSV 文件的每一行都是一个列表。这些列表中的每一个都是一个三项子列表。
对于这个食谱,我们将使用列表索引表达式的扩展来从列表中切片项目。切片和索引一样,跟在[]
字符后面。Python 为我们提供了几种切片表达式的变体。切片可以包括两个或三个值,用:
字符分隔。我们可以写:stop
,start:
,start:stop
,start:stop:step
,或者其他几种变体。默认的步长值是一。默认的起始值是列表的开头,默认的停止值是列表的结尾。
这是我们如何切片和处理原始的行列表,以挑选出我们需要的行:
如何做...
- 我们需要做的第一件事是从行列表中删除四行标题。我们将使用两个部分切片表达式来在第四行处分割列表:
**>>> head, tail = log_rows[:4], log_rows[4:]
>>> head[0]
['date', 'engine on', 'fuel height']
>>> head[-1]
['', '', '']
>>> tail[0]
['10/25/13', '08:24:00 AM', '29']
>>> tail[-1]
['', "choppy -- anchor in jackson's creek", '']**
我们使用log_rows[:4]
和log_rows[4:]
将列表切片成两个部分。head
变量将包含四行标题。我们实际上不想对头部进行任何处理,所以我们忽略了那个变量。然而,tail
变量有我们关心的表的行。
- 我们将使用带步长的切片来挑选有趣的行。切片的
[start::step]
版本将根据步长值选择行。在我们的情况下,我们将取两个切片。一个切片从第零行开始,另一个切片从第一行开始。
这里是每三行的一个切片,从第零行开始:
**>>> tail[0::3]
[['10/25/13', '08:24:00 AM', '29'],
['10/26/13', '09:12:00 AM', '27']]**
我们还想要每三行,从第一行开始:
**>>> tail[1::3]
[['', '01:15:00 PM', '27'], ['', '06:25:00 PM', '22']]**
- 然后这两个切片可以被合并在一起:
**>>> list( zip(tail[0::3], tail[1::3]) )
[(['10/25/13', '08:24:00 AM', '29'], ['', '01:15:00 PM', '27']),
(['10/26/13', '09:12:00 AM', '27'], ['', '06:25:00 PM', '22'])]**
我们将列表切片成了两个并行的组:
-
[0::3]
切片从第一行开始,包括每三行。这将是第零行,第三行,第六行,第九行,依此类推。 -
[1::3]
切片从第二行开始,包括每三行。这将是第一行,第四行,第七行,第十行,依此类推。
我们使用了zip()
函数来交错这两个列表中的序列。这给了我们一个非常接近我们可以处理的三个元组的序列。
- 展平结果:
**>>> paired_rows = list( zip(tail[0::3], tail[1::3]) )
>>> [a+b for a,b in paired_rows]
[['10/25/13', '08:24:00 AM', '29', '', '01:15:00 PM', '27'],
['10/26/13', '09:12:00 AM', '27', '', '06:25:00 PM', '22']]**
我们使用了来自构建列表-文字,附加和理解食谱的列表理解,将每对行中的两个元素组合成一个单独的行。现在我们可以将日期和时间转换为单个的datetime
值。然后我们可以计算时间差来得到船的运行时间,以及计算高度差来估算燃烧的燃料。
它是如何工作的...
切片操作符有几种不同的常见形式:
-
[:]
:起始和结束被隐含。表达式S[:]
将复制序列S。 -
[:stop]
:这将从开头创建一个新的列表,直到停止值之前。 -
[start:]
:这将从给定的起始位置创建一个新的列表,直到序列的末尾。 -
[start:stop]
:这将从起始索引开始选择一个子列表,并在停止索引之前停止。Python 使用半开区间。起始值包括在内,结束值不包括在内。 -
[::step]
:起始和结束被隐含,并包括整个序列。步长-通常不等于一-意味着我们将使用步长从起始位置跳过列表。对于给定的步长s和大小为|L|的列表,索引值为。 -
[start::step]
:给出了起始值,但结束值被隐含。这个想法是起始值是一个偏移量,步长适用于该偏移量。对于给定的起始值a,步长s,和大小为|L|的列表,索引值为。 -
[:stop:step]
:这用于防止处理列表中的最后几个项目。由于给定了步长,处理从零开始。 -
[start:stop:step]
:这将从序列的子集中选择元素。开始之前和结束之后的项目将不会被使用。
切片技术适用于列表、元组、字符串和任何其他类型的序列。这不会导致对象被改变;这将复制项目。
还有更多...
在Reversing a copy of a list方法中,我们将看到对切片表达式的更复杂的使用。
这个复制被称为浅复制,因为我们将有两个包含对相同基础对象的引用的集合。我们将在Making shallow and deep copies of objects方法中详细讨论这一点。
对于这个特定的例子,我们有另一种将多行数据重组为单行数据的方法。我们可以使用一个生成器函数。我们将在第八章中看到函数式编程技术,函数式和反应式编程特性。
另请参阅
-
查看Building lists – literals, appending, and comprehensions方法以了解创建列表的方法
-
查看Deleting from a list – deleting, removing, popping, and filtering方法以了解从列表中移除项目的其他方法
-
在Reversing a copy of a list方法中,我们将看到对列表进行反转
从列表中删除项目 – 删除、移除、弹出和过滤
有很多时候我们想要从list
集合中移除项目。我们可能会从列表中删除项目,然后处理剩下的项目。
删除不需要的项目会产生与使用filter()
创建仅包含所需项目的副本类似的效果。区别在于,过滤后的副本将使用比从列表中删除项目更多的内存。我们将展示从列表中移除不需要的项目的这两种技术。
准备工作
我们有一个用于记录大型帆船燃油消耗的电子表格。它的行看起来像这样:
日期 | 引擎开启 | 燃油高度 |
---|---|---|
引擎关闭 | ||
其他说明 | ||
10/25/2013 | 08:24 | 29 |
13:15 | 27 | |
平静的海域—锚所罗门岛 | ||
10/26/2013 | 09:12 | 27 |
18:25 | 22 | |
波涛汹涌—锚在杰克逊溪 |
有关此数据的更多背景信息,请参阅Slicing and dicing a list方法。
我们可以这样读取数据:
**>>> from pathlib import Path
>>> import csv
>>> with Path('code/fuel.csv').open() as source_file:
... reader = csv.reader(source_file)
... log_rows = list(reader)
>>> log_rows[0]
['date', 'engine on', 'fuel height']
>>> log_rows[-1]
['', "choppy -- anchor in jackson's creek", '']**
我们使用csv
模块读取日志详情。csv.reader()
是一个可迭代对象。为了将项目收集到一个单一列表中,我们应用了list()
函数。我们查看了列表中的第一个和最后一个项目,以确认我们确实有一个列表的列表结构。
原始 CSV 文件的每一行都是一个列表。这些列表中的每一个都有三个项目。
如何做...
我们将看到从列表中删除项目的四种方法:
-
del
语句 -
remove()
方法 -
pop()
方法 -
使用
filter()
函数创建一个拒绝选定行的副本
从列表中删除项目
我们可以使用del
语句从列表中移除项目。
为了方便在交互提示符下跟随示例,我们将复制列表。如果我们从原始的log_rows
列表中删除行,后续的示例可能会难以跟随。在实际程序中,我们不会做这个额外的复制。我们也可以使用log_rows[:]
来复制原始列表。
**>>> tail = log_rows.copy()**
del
语句的样子如下:
**>>> del tail[:4]
>>> tail[0]
['10/25/13', '08:24:00 AM', '29']
>>> tail[-1]
['', "choppy -- anchor in jackson's creek", '']**
del
语句从尾部删除了标题行,留下了我们真正需要处理的行。然后我们可以使用Slicing and dicing a list方法将它们合并并进行总结。
remove()
方法
我们可以使用remove()
方法从列表中移除项目。这会从列表中移除匹配的项目。
我们可能有一个看起来像这样的列表:
**>>> row = ['10/25/13', '08:24:00 AM', '29', '', '01:15:00 PM', '27']**
这里有一个无用的''
字符串:
**>>> row.remove('')
>>> row
['10/25/13', '08:24:00 AM', '29', '01:15:00 PM', '27']**
请注意,remove()
方法不返回值。它会直接改变列表。这是一个适用于可变对象的重要区别。
提示
remove()
方法不返回值。
它改变了列表对象。
看到这样错误的代码实际上是非常常见的:
a = ['some', 'data']
a = a.remove('data')
这是绝对错误的。这将把a
设置为None
。
pop()方法
我们可以使用pop()
方法从列表中删除项目。这将根据它们的索引从列表中删除项目。
我们可能有一个看起来像这样的列表:
**>>> row = ['10/25/13', '08:24:00 AM', '29', '', '01:15:00 PM', '27']**
这里有一个无用的''
字符串:
**>>> target_position = row.index('')
>>> target_position
3
>>> row.pop(target_position)
''
>>> row
['10/25/13', '08:24:00 AM', '29', '01:15:00 PM', '27']**
请注意,pop()
方法有两个作用:
-
它改变了
list
对象 -
它返回被移除的值
filter()函数
我们还可以通过构建传递合适项目并拒绝不合适项目的副本来删除项目。以下是我们如何使用filter()
函数来实现这一点。
- 识别我们希望通过或拒绝的项目的特征。
filter()
函数期望通过数据的规则。该函数的逻辑反函数将拒绝数据。
在我们的情况下,我们希望的行在第二列中有一个数值。我们可以通过一个小的辅助函数最好地检测到这一点。
- 编写过滤测试函数。如果很简单,可以使用 lambda 对象。否则,编写一个单独的函数:
**>>> def number_column(row, column=2):
... try:
... float(row[column])
... return True
... except ValueError:
... return False**
我们使用内置的float()
函数来查看给定的字符串是否是一个合适的数字。如果float()
函数没有引发异常,则数据是有效的数字,我们希望通过这一行。如果引发了异常,则数据不是数字,我们将拒绝这一行。
- 使用
filter()
函数中的数据测试函数(或 lambda):
**>>> tail_rows = list(filter(number_column, log_rows))
>>> len(tail_rows)
4
>>> tail_rows[0]
['10/25/13', '08:24:00 AM', '29']
>>> tail_rows[-1]
['', '06:25:00 PM', '22']**
我们提供了我们的测试,number_column()
和原始数据,log_rows
。filter()
函数的输出是一个可迭代对象。为了从可迭代结果创建一个列表,我们将使用list()
函数。结果只有我们想要的四行;其余的行被拒绝了。
我们并没有真正删除行。我们创建了一个省略这些行的副本。最终结果是一样的。
它是如何工作的...
因为列表是一个可变对象,我们可以从列表中删除项目。这种技术对于元组或字符串不起作用。这三个集合都是序列,但只有列表是可变的。
我们只能删除列表中存在的索引的项目。如果我们尝试删除超出允许范围的索引的项目,将会得到IndexError
异常。
例如:
**>>> row = ['', '06:25:00 PM', '22']
>>> del row[3]
Traceback (most recent call last):
File "<pyshell#38>", line 1, in <module>
del row[3]
IndexError: list assignment index out of range**
还有更多...
有时这种方法不起作用。如果我们在for
语句中使用列表,我们无法从列表中删除项目。
假设我们想要从列表中删除所有偶数项。以下是一个不正常工作的示例:
**>>> data_items = [1, 1, 2, 3, 5, 8, 10,
... 13, 21, 34, 36, 55]
>>> for f in data_items:
... if f%2 == 0: data_items.remove(f)
>>> data_items
[1, 1, 3, 5, 10, 13, 21, 36, 55]**
结果显然不正确。为什么列表中还有一些偶数值的项目?
让我们看看在处理值为八的项目时会发生什么。我们将执行remove()
方法。该值将被移除,并且所有后续的值将向前滑动一个位置。10
将被移动到以前由8
占据的位置。列表的内部索引将向前移动到下一个位置,该位置将有一个13
。10
将永远不会被处理。
如果我们在列表的中间插入,在for
循环中的驱动可迭代对象中也会发生不好的事情。在这种情况下,项目将被处理两次。
我们有两种方法可以避免跳过-删除问题:
- 制作列表的副本:
for f in data_items[:]:
- 使用
while
循环和手动索引:
**>>> data_items = [1, 1, 2, 3, 5, 8, 10,
... 13, 21, 34, 36, 55]
>>> position = 0
>>> while position != len(data_items):
... f= data_items[position]
... if f%2 == 0:
... data_items.remove(f)
... else:
... position += 1
>>> data_items
[1, 1, 3, 5, 13, 21, 55]**
我们设计了一个循环,只有在项目为奇数时才增加位置。如果项目是偶数,则将其删除,并将其他项目向列表中的下一个位置移动。
另请参阅
-
有关创建列表的方法,请参阅构建列表-文字,附加和理解配方
-
有关从列表中复制列表和从列表中选择子列表的方法,请参阅切片和切块列表配方
-
在反转列表的副本配方中,我们将研究如何反转列表
反转列表的副本
偶尔,我们需要反转list
集合中项目的顺序。例如,一些算法产生的结果是倒序的。我们将看看数字转换为特定基数时通常是如何从最低位到最高位生成的。我们通常希望以最高位数字优先显示值。这导致需要反转列表中数字的顺序。
我们有两种方法来反转一个列表。首先是reverse()
方法。然后是这个方便的技巧。
准备工作
假设我们正在进行数字基数之间的转换。我们将看看一个数字在一个基数中是如何表示的,以及我们如何从一个数字计算出那个表示。
任何值v都可以定义为给定基数b中各个数字d[n]的多项式函数:
v = d[n] × b^n + d[n] [-1] × b^n ^(-1) + d[n] [-2] × b^n ^(-2) + ... + d [1] × b + d [0]
有理数有有限数量的数字。无理数将有无限系列的数字。
例如,数字0xBEEF
是一个 16 进制值。数字是{B = 11, E = 14, F = 15},基数b = 16。
48879 = 11 × 16³ + 14 × 16² + 14 × 16 + 15
我们可以重新陈述这个形式,这样计算起来稍微更有效率一些:
v = (...(( d[n] × b + d[n] [-1] ) × b + d[n] [-2] ) × b + ... + d [1] ) × b + d [0]
有许多情况下,基数不是某个数字的一致幂。例如,ISO 日期格式涉及每周 7 天,每天 24 小时,每小时 60 分钟和每分钟 60 秒的混合基数。
给定一个周数、一周中的某一天、一个小时、一分钟和一秒,我们可以计算给定年份内的秒级时间戳t[s]。
t[s] = ((( w × 7 + d ) × 24 + h ) × 60 + m ) × 60 + s
例如:
**>>> week = 13
>>> day = 2
>>> hour = 7
>>> minute = 53
>>> second = 19
>>> t_s = (((week*7+day)*24+hour)*60+minute)*60+second
>>> t_s
8063599**
我们如何反转这个计算?我们如何从整体时间戳中获取各个字段?
我们需要使用divmod
风格的除法。有关背景,请参阅选择真除法和地板除法之间的配方。
将秒级时间戳t[s]转换为单独的周、天和时间字段的算法如下:
t[m],s ← t[s] /60, t[s] mod 60
t[h],m ← t[m] /60, t[m] mod 60
t[d],h ← t[h] /60, t[h] mod 24
w,d ← t[d] /60, t[d] mod 7
这有一个方便的模式,可以导致一个非常简单的实现。它有一个产生值的顺序相反的后果:
**>>> t_s = 8063599
>>> fields = []
>>> for b in 60, 60, 24, 7:
... t_s, f = divmod(t_s, b)
... fields.append(f)
>>> fields.append(t_s)
>>> fields
[19, 53, 7, 2, 13]**
我们已经应用了divmod()
函数四次,从以秒为单位给定的时间戳中提取秒、分钟、小时、天和周。这些顺序是错误的。我们如何将它们反转?
如何做...
我们有两种方法:我们可以使用reverse()
方法,或者我们可以使用[::-1]
切片表达式。这是reverse()
方法:
**>>> fields_copy1 = fields.copy()
>>> fields_copy1.reverse()
>>> fields_copy1
[13, 2, 7, 53, 19]**
我们制作了原始列表的副本,这样我们就可以保留一个未改变的副本,以便与改变后的副本进行比较。这样更容易跟踪示例。我们应用了reverse()
方法来反转列表的副本。
这将改变列表。与其他变异方法一样,它不会返回一个有用的值。使用类似这样的语句是错误的:a = b.reverse()
。a
的值将始终是None
。
这是一个带有负步长的切片表达式:
**>>> fields_copy2 = fields[::-1]
>>> fields_copy2
[13, 2, 7, 53, 19]**
在这个例子中,我们做了一个切片[::-1]
,它使用了一个隐含的开始和结束,步长为-1
。这会选择列表中所有项目的倒序来创建一个新列表。
这个slice
操作绝对不会改变原始列表。这会创建一个副本。检查fields
变量的值,看看它是否没有改变。
它是如何工作的...
正如我们在切片和切块列表配方中指出的,切片表示法非常复杂。使用负步长的切片将创建一个副本(或子集),其中的项目按从右到左的顺序处理,而不是默认的从左到右的顺序。
重要的是要区分这两种方法:
-
reverse()
函数修改了list
对象本身。与append()
和remove()
等方法一样,这个方法没有返回值。因为它改变了列表,所以不会返回值。 -
[::-1]
切片表达式创建一个新的列表。这是原始列表的浅复制,顺序被颠倒。
另请参阅
-
有关浅复制和深复制对象的更多信息,请参阅制作浅复制和深复制对象食谱,了解浅复制是什么,以及为什么我们可能需要进行深复制。
-
有关创建列表的方法,请参阅构建列表-文字,附加和理解食谱
-
有关从列表中复制列表和选择子列表的方法,请参阅切片和切块列表食谱
-
有关从列表中删除项目的其他方法,请参阅从列表中删除-删除,移除,弹出和过滤食谱
使用集合方法和运算符
我们有几种方法来构建set
集合。我们可以使用set()
函数将现有集合转换为集合。我们可以使用add()
方法将项目放入集合。我们还可以使用update()
方法和并集运算符|
来从其他集合创建一个更大的集合。
我们将展示一个使用set
来显示我们是否已经从统计数据池中看到了完整值域的食谱。该食谱将在扫描样本时构建一个set
集合。
在进行探索性数据分析时,我们需要回答一个问题:这些数据是随机的吗?许多数据集中的数据方差是普通噪音。重要的是不要浪费时间对随机数进行复杂的建模和分析。
对于离散或连续的数值数据,如水的深度(以米为单位)或文件的大小(以字节为单位),我们可以使用平均值和标准偏差来查看给定数据集是否是随机的。我们期望样本的均值在由标准偏差测量的边界内与总体均值相匹配。
对于分类数据,如客户 ID 号码或电话号码,我们无法计算平均值或标准偏差。这些值必须以不同的方式进行评估。
确定分类数据的随机性的一种技术是优惠券收集者测试。通过这个测试,我们将看到在找到完整的优惠券集之前必须检查多少项目。顾客访问的顺序是随机的吗?还是在访问顺序中有其他分布?如果数据不是随机的,那么我们可以投资更多的研究来了解原因。
Python 的set
集合对于这个工作至关重要。我们将向set
添加项目,直到我们至少见过每个客户一次。
如果客户随机到达,我们可以预测在企业至少见过每个客户之前的预期访问次数。整个域的预期到达时间是域中每个客户的到达时间之和。这等于客户数量n乘以第 n 个调和数H[n]:
E = n × H[n] = n × ((1/1) + (1/2) + (1/3) + (1/ n ))
这是所有客户被看到之前的预期平均访问次数。如果实际平均到达时间与预期相匹配,这意味着所有客户都在访问;我们不需要再浪费时间研究符合我们期望的数据。如果实际平均值与预期不符,则一些客户访问的频率不如其他客户频繁,我们需要深入研究原因。
准备就绪
我们将使用 Python 的set
来表示优惠券的集合。我们需要一个可能(或可能不)具有正确分布的优惠券的数据集。我们将查看一个包含八个客户的集合。
这是一个模拟顾客以随机顺序到达的函数。顾客以半开区间[0,n]中的数字表示,我们可以说所有顾客c符合规则 0 ≤ c < n。
**>>> import random
>>> def arrival1(n=8):
... while True:
... yield random.randrange(n)**
arrival1()
函数将产生一个无限序列的值。我们在末尾加上了1
,这可能看起来像是拼写错误,但我们使用了1
后缀,以便我们可以创建替代实现。
我们需要对生成的值的数量设置一个上限。以下是一个具有生成样本数量上限的函数:
**>>> def samples(limit, generator):
... for n, value in enumerate(generator):
... if n == limit: break
... yield value**
这个生成函数使用另一个生成器作为项目的来源。这个想法是我们将使用arrival1()
函数。samples()
函数枚举了来自更大集合的项目,并在看到足够的项目时停止。由于arrival1()
函数是无限的,这个边界是必不可少的。
以下是我们如何使用这些函数来模拟顾客的到达。我们将产生一系列顾客 ID 号码:
**>>> random.seed(1)
>>> list(samples(10, arrival1()))
[2, 1, 4, 1, 7, 7, 7, 6, 3, 1]**
我们强制随机数生成器具有特定的种子值,以便我们可以产生一个已知的测试序列。我们将samples()
函数应用于arrival1()
函数,以产生一个包含 10 次顾客访问的序列。第七位顾客似乎有很多重复的业务。顾客零和五根本没有出现。
这只是数据的模拟。企业将使用销售收据来确定顾客访问。网站可能会在数据库中记录访问,或者可能会抓取网络日志来确定实际值的序列。
在我们看到所有八个顾客之前,预期的访问次数是多少?
**>>> from fractions import Fraction
>>> def expected(n=8):
... return n * sum(Fraction(1,(i+1)) for i in range(n))**
这个函数创建了一系列分数 1/1,1/2,直到 1/n。这些分数被求和并乘以n。
**>>> expected(8)
Fraction(761, 35)
>>> round(float(expected(8)))
22**
平均来说,我们需要 22 次顾客访问才能看到我们的八个顾客中的所有人一次。
我们如何使用set
集合来统计在我们看到所有八个顾客之前的实际访问次数?
如何做...
当我们逐个顾客访问时,我们将把顾客 ID 放入一个set
集合中。重复项不会保存在集合中。一旦顾客 ID 成为集合的成员,再次添加该值不会改变集合。我们将总结这个步骤,然后展示完整的函数:
-
从一个空的
set
和一个零计数器开始。 -
开始一个
for
循环来访问所有数据项。 -
将下一个项目添加到
set
中。计数器加一。 -
如果
set
已经完成,可以产生计数。这是需要看到完整集合的顾客数量。产生后,清空set
并将计数器初始化为零,以准备下一个顾客。
以下是函数:
def coupon_collector(n, data):
count, collection = 0, set()
for item in data:
count += 1
collection.add(item)
if len(collection) == n:
yield count
count, collection = 0, set()
这将从零开始计数,并创建一个空集collection
,我们将收集顾客 ID。我们将逐个遍历源数据值序列data
中的每个项目。count
的值显示了有多少访客。变量collection
的值是不同访客的集合。
set
的add()
方法将改变集合以添加一个不同的值。如果该值已经在集合中,则内容不会发生变化。
当集合的大小等于我们的目标人口的大小时,我们就有了一个完整的优惠券集合。我们可以产生count
的值。我们还重置了访问计数,并为我们的优惠券集合创建了一个新的空集。
工作原理...
由于这是一个生成器,我们需要通过从结果创建一个list
对象来捕获数据。以下是我们如何使用coupon_collector()
函数:
from statistics import mean
expected_time = float(expected(n))
data = samples(100, arrival1())
wait_times = list(coupon_collector(n, data))
average_time = mean(wait_times)
我们已经计算了看到所有n
个顾客的预期时间。我们使用samples(100, arrival1())
作为模拟来创建data
变量,其中包含一系列访问。在现实生活中,我们会分析销售收据来收集这一系列访问。
我们对数据应用了收集者测试。这产生了一个值序列,显示了需要多少客户到达才能创建一个完整的 优惠券 或客户 ID 的集合。这个计数序列应该接近预期的访问次数。我们将这个序列分配给变量 wait_times
,因为我们测量了在看到我们样本集中的所有客户之前需要等待的时间。
这让我们可以轻松地将实际数据与预期数据进行比较。我们刚刚看到的函数 arrival1()
产生的平均值与预期值非常接近。由于输入数据是随机的,模拟不会产生与预期完全匹配的值。
收集者测试依赖于收集一组优惠券。在这种情况下,术语 set 是用于精确的数学形式化来最好地表示数据。
给定的项目要么是集合的成员,要么不是。我们不能将其添加到集合中超过一次。例如,我们可以手动创建一个集合并向其添加一个项目:
**>>> collection = set()
>>> collection.add(1)
>>> collection
{1}**
当我们尝试再次添加这个项目时,set
的值不会改变。
**>>> collection.add(1)
>>> collection
{1}
>>> 1 in collection
True**
这是收集优惠券的完美数据表示。
请注意,add()
方法不会返回一个值。它改变了 set
对象。这类似于 list
集合的方法工作方式。通常,改变集合的方法不会返回一个值。唯一的例外是 pop()
方法,它既改变了 set
对象,又返回了弹出的值。
还有更多...
我们有几种方法可以向 set
添加项目:
-
示例使用了
add()
方法。这适用于单个项目。 -
我们可以使用
union()
方法。这类似于一个运算符——它创建一个新的结果set
。它不会改变任何一个操作数集合。 -
我们可以使用
|
并集运算符来计算两个集合的并集。 -
我们可以使用
update()
方法从另一个集合中更新一个集合。这会改变一个集合,并且不会返回一个值。
对于大多数情况,我们需要从要添加的项目创建一个单例 set
。以下是将单个项目 3
添加到一个集合中的示例:
**>>> collection
{1}
>>> item = 3
>>> collection.union( {item} )
{1, 3}
>>> collection
{1}**
在这里,我们从变量 item
的值创建了一个单例集合 {item}
。然后我们使用了 union()
方法来计算一个新的集合,即 collection
和 {item}
的并集。
请注意,union()
返回一个结果对象,并且不会改变原始的 collection
集合。我们需要使用 collection = collection.union({item})
来更新 collection
对象。
这是另一种使用并集运算符 |
的替代方法:
**>>> collection = collection | {item}
>>> collection
{1, 3}**
这与 {1, 3} ∪ {3} ≡ {1, 3} 的常见数学表示法相似。
我们也可以使用 update()
方法:
**>>> collection.update( {4} )
>>> collection
{1, 3, 4}**
这个方法改变了 set
对象。因为它改变了集合,所以它不会返回一个值。
Python 有许多集合运算符。这些是我们可以在复杂表达式中使用的普通运算符符号:
-
|
用于并集,通常排版为 A ∪ B -
&
用于交集,通常排版为 A ∩ B -
^
用于对称差,通常排版为 A Δ B -
-
用于减法,通常排版为 A - B
另请参阅
- 在 从集合中移除项目 - remove、pop 和 difference 配方中,我们将看看如何通过移除或替换项目来更新一个集合
从集合中移除项目 - remove()、pop() 和 difference
Python 给了我们几种方法来从 set
集合中移除项目。我们可以使用 remove()
方法来移除特定的项目。我们可以使用 pop()
方法来移除一个任意的项目。
此外,我们可以使用集合交集、差集和对称差运算符 &
、-
和 ^
来计算一个新的集合,这个新的集合是给定输入集合的子集。
准备工作
有时我们会有包含复杂和各种格式的行的日志文件。这是一个来自长而复杂的日志的小片段:
**>>> log = '''
... [2016-03-05T09:29:31-05:00] INFO: Processing ruby_block[print IP] action run (@recipe_files::/home/slott/ch4/deploy.rb line 9)
... [2016-03-05T09:29:31-05:00] INFO: Installed IP: 111.222.111.222
... [2016-03-05T09:29:31-05:00] INFO: ruby_block[print IP] called
...
... - execute the ruby block print IP
... [2016-03-05T09:29:31-05:00] INFO: Chef Run complete in 23.233811181 seconds
...
... Running handlers:
... [2016-03-05T09:29:31-05:00] INFO: Running report handlers
... Running handlers complete
... [2016-03-05T09:29:31-05:00] INFO: Report handlers complete
... Chef Client finished, 2/2 resources updated in 29.233811181 seconds
... '''**
我们需要在日志中找到 IP: 111.222.111.222
行。
这是我们将要做的:
**>>> import re
>>> pattern = re.compile(r"IP: \d+\.\d+\.\d+\.\d+")
>>> matches = set( pattern.findall(log) )
>>> matches
{'IP: 111.222.111.222'}**
较大日志文件的问题在于目标行中有真实信息的地方。这些与看似相似但只是示例的行混在一起。我们还会发现像IP: 1.2.3.4
这样的行,这是无关紧要的输出。事实证明,有几种这些我们想要忽略的无关紧要的行。
这是集合交集和集合减法非常有帮助的地方。
如何做...
- 创建一个我们想要忽略的项目集:
**>>> to_be_ignored = {'IP: 0.0.0.0', 'IP: 1.2.3.4'}**
- 收集日志中的所有条目。我们将使用
re
模块进行此操作,如前所示。假设我们的数据包括来自日志其他部分的良好地址以及虚拟和占位符地址:
**>>> matches = {'IP: 111.222.111.222', 'IP: 1.2.3.4'}**
- 使用集合减法形式从匹配集中删除项目。以下是两个示例:
**>>> matches - to_be_ignored
{'IP: 111.222.111.222'}
>>> matches.difference(to_be_ignored)
{'IP: 111.222.111.222'}**
请注意,这两者都是返回新集合作为其结果的运算符。这两者都不会改变基础集合对象。
我们经常在这样的语句中使用这些:
**>>> valid_matches = matches - to_be_ignored
>>> valid_matches
{'IP: 111.222.111.222'}**
这将把结果集分配给一个新变量valid_matches
,这样我们就可以对这个新集合进行所需的处理。
在这种情况下,如果项目不在集合中,它不会引发KeyError
异常。
它是如何工作的...
set
对象仅跟踪成员资格。项目要么在set
中,要么不在set
中。我们指定要删除的项目。删除项目不依赖于索引位置或键值。
因为我们有set
运算符,我们可以从目标set
中删除任何set
中的项目。我们不需要逐个处理项目。
还有更多...
我们有几种方法可以从集合中删除项目:
-
在示例中,我们使用了
difference()
方法和-
运算符。difference()
方法的行为类似于运算符,并创建一个新的集合。 -
我们还可以使用
difference_update()
方法。这将就地改变一个集合。它不返回值。 -
可以使用
remove()
方法删除单个项目。 -
我们还可以使用
pop()
方法删除任意项目。这在这个示例中并不适用得很好,因为我们无法控制弹出哪个项目。
difference_update()
方法的外观如下:
**>>> valid_matches = matches.copy()
>>> valid_matches.difference_update( to_be_ignored )
>>> valid_matches
{'IP: 111.222.111.222'}**
首先,我们复制了原始的matches
集合。这创建了一个新的集合,我们将其分配给valid_matches
集合。然后,我们应用了difference_update()
方法,从这个集合中删除了不需要的项目。
由于集合被改变,因此不会返回任何值。而且,由于集合是一个副本,这不会修改原始的matches
集合。
我们可以这样使用remove()
方法。请注意,如果集合中不存在项目,remove()
将引发异常。
**>>> valid_matches = matches.copy()
>>> for item in to_be_ignored:
... if item in valid_matches:
... valid_matches.remove(item)
>>> valid_matches
{'IP: 111.222.111.222'}**
我们测试了一下valid_matches
集合中是否存在该项目,然后再尝试删除它。这是避免引发KeyError
异常的一种方法。另一种方法是使用try:
语句来消除异常。
pop()
方法删除一个任意项目。它既改变了集合,又返回了被删除的项目。如果我们尝试从空集合中弹出项目,我们将引发KeyError
异常。
另请参见
- 在使用集合方法和运算符配方中,我们将看看创建集合的其他方法
创建字典-插入和更新
字典是 Python 映射的一种。内置类型dict
类提供了许多常见功能。在collections
模块中定义了这些功能的一些常见变体。
正如我们在选择数据结构配方中所指出的,当我们有一些需要映射到给定值的键时,我们将使用字典。例如,我们可能希望将单词映射到单词的长而复杂的定义。或者将某个值映射到数据集中该值出现的次数。
键和计数字典非常常见。我们将看一个详细的配方,展示如何初始化字典并更新计数器。
在 使用集合方法和运算符 配方中,我们研究了客户到达企业的情况。在那个配方中,我们使用了一个集合来确定企业在收集完整的访问集之前需要多少次访问。
准备工作
在这个配方中,我们将看看如何创建一个显示每个客户访问次数的直方图。为了创建一些有趣的数据,我们将修改在其他配方中使用的样本生成器。
在早期的例子中,我们使用了一个简单的均匀随机数生成器来选择客户的顺序。这是选择生成具有稍微不同分布的随机数的客户的另一种方法:
**>>> def arrival2(n=8):
... p = 0
... while True:
... step = random.choice([-1,0,+1])
... p += step
... yield abs(p) % n**
这使用了一种称为随机游走的技术来生成下一个客户 ID 号。它将从零开始,然后进行三种更改之一。它可能使用相同的客户或两个相邻的客户号中的一个。使用表达式 abs(p) % n
允许我们计算任何整数值,并将数字 p 映射到范围 0 ≤ p < n。
这是一个工具,可以生成一些数据,我们可以用来模拟客户到达:
>>> import random
>>> from ch04_r06 import samples, arrival2
>>> random.seed(1)
>>> list( samples(10, arrival2(8)) )
[1, 0, 1, 1, 2, 2, 2, 2, 1, 1]
这向我们展示了 arrival2()
函数如何模拟倾向于围绕客户零的起始值聚集的客户。如果我们在 使用集合方法和运算符 配方中使用这个来进行优惠券收集器测试,我们会发现这个生成器创建的样本数据在这个测试中表现得非常糟糕。这种聚集到达时间意味着我们必须在收集到所有八个不同的客户之前看到非常多的客户。
直方图统计每个客户的出现次数。我们将使用字典将客户 ID 映射到我们见过客户的次数。
如何做...
- 使用
{}
创建一个空字典。我们也可以使用dict()
创建一个空字典。由于我们将创建一个统计每个客户到达次数的直方图,我们将称其为histogram
:
histogram = {}
-
对于每个客户号,如果是新的,则向字典添加一个空列表。我们可以使用
if
语句来实现这一点,或者我们可以使用字典的setdefault()
方法。我们将首先展示if
语句版本。稍后,我们将看看setdefault()
优化。 -
增加字典中的值。
以下是用于计算字典中出现次数的循环。它通过创建和更新项目来工作:
for customer in source:
if customer not in histogram:
histogram[customer]= 0
histogram[customer] += 1
完成后,我们将统计每个客户的模拟访问总数。
我们可以将其转换为一个方便的条形图来比较频率。我们可以计算一些基本的描述性统计数据,包括均值和标准差,以查看是否有任何客户被过度或不足地代表。
工作原理...
字典的核心特性是从不可变值到任何类型的对象的映射。在这种情况下,我们使用一个不可变的数字作为键,另一个数字作为值。当我们计数时,我们替换与键关联的值。
写起来可能有点不寻常:
histogram[customer] += 1
或者写成:
histogram[customer] = histogram[customer] + 1
并将字典中的值视为替换。当我们写出像 histogram[customer] + 1
这样的表达式时,我们正在从另外两个整数对象计算一个新的整数对象。这个新对象替换了字典中的旧值。
字典键对象的不可变性至关重要。我们不能使用 list
、set
或 dict
作为字典映射中的键。但是,我们可以将列表转换为不可变元组,或者将 set
转换为 frozenset
,以便我们可以使用其中一个更复杂的对象作为键。
还有更多...
我们不必使用 if
语句来添加缺少的键。我们可以使用字典的 setdefault()
方法。我们的循环将如下所示:
histogram = {}
for customer in source:
histogram.setdefault(customer, 0)
histogram[customer] += 1
如果键值 customer
不存在,则提供默认值。如果键存在,则 setdefault()
方法不起作用。
collections
模块提供了许多替代映射,可以用来代替默认的 dict
映射。
-
defaultdict
:这个集合使我们免于明确编写第二步。我们在创建defaultdict
时提供一个初始化函数。我们很快会看一个例子。 -
OrderedDict
:这个集合保留了键最初创建的顺序。我们将把这个保存在控制字典键的顺序配方中。 -
Counter
:这个集合在创建时执行整个键和计数算法。我们很快也会看到这个。
使用defaultdict
类的版本如下:
from collections import defaultdict
def summarize_3(source):
histogram = defaultdict(int)
for item in source:
histogram[item] += 1
return histogram
我们创建了一个defaultdict
实例,它将使用int()
函数初始化任何未知的键值。我们将int
-函数对象-提供给defaultdict
构造函数。defaultdict
将评估给定的函数对象以创建默认值。
这使我们可以简单地使用histogram[item] += 1
。如果item
属性的值先前在字典中,它将被递增。如果item
属性的值尚未在字典中,将评估int
函数,并成为默认值。
我们还可以通过创建一个Counter
对象来做到这一点。我们需要导入Counter
类,以便我们可以从原始数据构建Counter
对象。
**>>> from collections import Counter
>>> def summarize_4(source):
... histogram = Counter(source)
... return histogram**
当我们从数据源创建一个Counter
时,该类将扫描数据并计算不同的出现次数。这个类实现了整个配方。
结果如下:
**>>> import random
>>> from pprint import pprint
>>> random.seed(1)
>>> histogram = summarize_4(samples(1000, arrival2(8)))
>>> pprint(histogram)
Counter({1: 150, 0: 130, 2: 129, 4: 128, 5: 127, 6: 118, 3: 117, 7: 101})**
请注意,Counter
对象以计数值的降序显示值。OrderedDict
对象将按照键创建的顺序显示值。dict
不保持顺序。
如果我们想对键施加顺序,我们可以使用:
**>>> for key in sorted(histogram):
... print(key, histogram[key])
0 130
1 150
2 129
3 117
4 128
5 127
6 118
7 101**
另请参阅
-
在从字典中删除- pop()方法和 del 语句配方中,我们将看看如何通过删除项目来修改字典
-
在控制字典键的顺序配方中,我们将看看如何控制字典中键的顺序
从字典中删除- pop()方法和 del 语句
字典的一个常见用例是作为关联存储:我们可以在键和值对象之间保持关联。这意味着我们可能在字典中对项目进行任何CRUD操作。
-
创建新的键值对
-
检索与键关联的值
-
更新与键关联的值
-
从字典中删除键(和值)
我们有两种常见的变体:
-
我们有内存中的字典
dict
,以及collections
模块中对这个主题的变体。这个集合只在我们的程序运行时存在。 -
我们还在
shelve
和dbm
模块中有持久存储。数据集合是文件系统中的持久文件。
这些非常相似,shelf.Shelf
和dict
对象之间的区别很小。这使我们可以在不对程序进行重大更改的情况下尝试dict
并切换到Shelf
。
服务器进程通常会有多个并发会话。当会话创建时,它们可以被放入dict
或shelf
中。当会话退出时,项目可以被删除或存档。
我们将模拟处理多个请求的服务概念。我们将定义一个在模拟环境中使用单个处理线程的服务。我们将避免并发和多处理考虑。
准备好了
在Craps赌场游戏中,玩家可以(并经常)在游戏中创建和删除多个赌注。规则可能非常复杂,但核心概念包括玩家可能进行的四种赌注:
-
过线投注:对于我们的目的,这是游戏开始时的购买方式。
-
过线赔率投注:这在赌场的游戏表面上没有标记,但这是一个真正的赌注。这个赌注的赔率与过线赌注不同,并且具有一些统计优势。它也可以被移除。
-
来线投注:这可以在游戏进行中放置。
-
come line odds赌注:这也是在游戏中下注的。这也可以被取消。
了解所有这些赌注选择的最佳方法是模拟游戏和玩家。游戏将需要跟踪玩家下注的所有赌注。这可以通过使用一个字典来完成,其中下注被插入,当它们得到回报时被移除,玩家将它们取消,或者游戏结束。
我们将简化模拟的部分,以便我们可以专注于正确使用字典。这最好作为一个类定义来处理,这样我们可以正确地将赌注和游戏规则与玩家规则隔离开来。有关类设计的更多信息,请参见第六章,类和对象的基础。
如何做...
- 创建整个字典对象:
working_bets = {}
-
为我们要插入字典的每个对象定义键和值。例如,键可以是赌注的描述:
come
,pass
,come odds
或pass odds
。值可以是赌注的金额。通常我们避免使用货币,而是使用桌面最低赌注的单位。通常这些都是简单的整数倍数,最常见的是整数值 1 来表示最低赌注。 -
在下注时输入值:
working_bets[bet_name] = bet_amount
具体例子,我们会有working_bets["pass"] = 1
。
- 随着赌注得到回报或取消,删除值。我们可以使用
del
语句或字典的pop()
方法:
del working_bets['come odds']
如果键不存在,这将引发KeyError
异常。
pop()
方法既改变了字典,又返回与键相关联的值。如果键不存在,这将引发KeyError
异常。
amount = working_bets.pop('come odds')
事实证明,pop()
可以给出一个默认值。如果键不存在,它不会引发异常,而是返回默认值。
工作原理...
因为字典是一个可变对象,我们可以从字典中删除键。这将同时删除键和与键相关联的值对象。
如果我们尝试删除一个不存在的键,将引发KeyError
异常。
我们可以用如下语句替换字典中的对象:
working_bets["come"] = 1
working_bets["come"] = None
键—come
—仍然保留在字典中。旧值1
不再需要,并将被新值None
替换。这与删除项目不同。
还有更多...
我们只能删除字典的键。正如我们之前提到的,我们可以将值设置为None
以删除该值,但保留字典中的键。
当我们在for
语句中使用字典时,目标变量将被分配键值。例如:
for bet_name in working_bets:
print(bet_name, working_bets[bet_name])
这将打印出working_bets
字典中与该赌注相关的所有键值,bet_name
和赌注金额。
参见
-
在创建字典-插入和更新的示例中,我们将看看如何创建字典并填充它们的键和值
-
在控制字典键的顺序的示例中,我们将看看如何控制字典中键的顺序
控制字典键的顺序
在创建字典-插入和更新的示例中,我们看了一下创建字典对象的基础知识。在许多情况下,我们会将项目放入字典中,并从字典中单独获取项目。键的顺序甚至不会成为问题。
有些情况下,我们可能想要显示字典的内容。在这种情况下,我们通常希望对键施加一些顺序。例如,当我们使用 Web 服务时,消息通常是以 JSON 表示的字典。在许多情况下,我们希望保持特定顺序的键,以便在调试日志中显示消息时更容易理解。
另一个例子是,当我们使用csv
模块读取数据时,电子表格中的每一行都可以表示为一个字典。在这种情况下,我们几乎总是希望保持键的顺序,以使字典遵循源文件的结构。
准备工作
字典是电子表格中的一行很好的模型。当电子表格有标题行和列标题时,这种模型特别有效。假设我们在电子表格中收集了一些数据,看起来像这样:
final | least | most |
---|---|---|
5 | 0 | 6 |
-3 | -4 | 0 |
-1 | -3 | 1 |
3 | 0 | 4 |
这显示了最终结果,玩家拥有的最少金额和最多金额。我们可以使用csv
模块读取这些数据进行进一步分析:
**>>> from pathlib import Path
>>> import csv
>>> data_path = Path('code/craps.csv')
>>> with data_path.open() as data_file:
... reader = csv.DictReader(data_file)
... data = list(reader)
>>> for row in data:
... print(row)
{'most': '6', 'least': '0', 'final': '5'}
{'most': '0', 'least': '-4', 'final': '-3'}
{'most': '1', 'least': '-3', 'final': '-1'}
{'most': '4', 'least': '0', 'final': '3'}**
电子表格的每一行都是一个字典。然而,每一行都有一些奇怪的地方。虽然不明显,但行中键的顺序与原始.csv
文件中键的顺序不匹配。
为什么?默认的dict
结构不保证键的任何顺序。如果我们想按特定顺序显示键会怎样?
如何做...
我们有两种常见的方法来强制字典键的顺序:
-
创建一个
OrderedDict
:这可以保持键的创建顺序 -
在键上使用
sorted()
:这会将键放入排序顺序
大多数情况下,我们可以简单地使用OrderedDict
而不是dict()
或{}
来创建一个空字典。这将允许我们按所需的顺序创建键。
然而,有时我们不能轻松地用OrderedDict
实例替换dict
实例。我们选择了这个例子,因为我们不能轻易地替换由csv
创建的dict
类。
这是如何强制行的dict
键遵循原始.csv
文件中列的顺序的方法:
-
获取键的首选顺序。对于
DictReader
,读取器对象的fieldnames
属性具有正确的顺序信息。 -
使用生成器表达式按正确顺序创建字段。我们会有类似这样的东西:
((name, raw_row[name]) for name in reader.fieldnames)
- 从生成器创建一个
OrderedDict
。整个顺序如下:
**>>> from collections import OrderedDict
>>> with data_path.open() as data_file:
... reader = csv.DictReader(data_file)
... for raw_row in reader:
... column_sequence = ((name, raw_row[name])
... for name in reader.fieldnames)
... good_row = OrderedDict(column_sequence)
... print(good_row)
OrderedDict([('final', '5'), ('least', '0'), ('most', '6')])
OrderedDict([('final', '-3'), ('least', '-4'), ('most', '0')])
OrderedDict([('final', '-1'), ('least', '-3'), ('most', '1')])
OrderedDict([('final', '3'), ('least', '0'), ('most', '4')])**
这样可以按特定顺序构建字典。
作为优化,我们可以将这两个步骤合并为一个步骤:
OrderedDict((name, raw_row[name]) for name in reader.fieldnames)
这将构建一个raw_row
对象的有序版本。
它是如何工作的...
OrderedDict
类保持键的创建顺序。这个类对于确保结构保持更容易理解的顺序非常方便。
当然,这会有一些性能成本。默认的dict
类为每个键计算一个哈希值,并使用哈希值来定位字典中的空间。这倾向于使用更多内存,但执行速度非常快。
OrderedDict
使用一些额外的存储来保持键的顺序。这在创建键时需要额外的时间。如果键的创建倾向于主导算法,我们会注意到减速。如果键的检索倾向于主导设计,那么使用OrderedDict
时我们不会看到太多变化。
还有更多...
在一些包中——比如pymongo
——有一些替代的有序字典实现。
请参阅api.mongodb.org/python/current/api/bson/son.html
。
bson.son
模块包括SON
类,这是一个非常方便的有序字典。它专注于 Mongo 数据库的需求,但也非常适用于其他应用。
另请参阅
-
在创建字典-插入和更新的示例中,我们将看看如何创建字典。
-
在从字典中删除-使用 pop()方法和 del 语句的示例中,我们将看看如何通过删除项目来修改字典。
在 doctest 示例中处理字典和集合
我们将在这个示例中看一下编写正确测试的一个小方面。我们将在第十一章中看到整体测试,测试。在本章中的数据结构——dict
和set
——在编写正确测试时都包含一些复杂性。
由于dict
键(和set
成员)没有顺序,我们的测试结果会有问题。我们需要有可重复的结果,但没有办法保证集合的顺序。这可能导致测试结果与我们的期望不符。
假设我们的测试期望集合{"Poe", "E", "Near", "A", "Raven"}
。由于集合没有定义的顺序,Python 可以以任何顺序显示这个集合:
**>>> {"Poe", "E", "Near", "A", "Raven"}
{'E', 'Poe', 'Raven', 'Near', 'A'}**
元素是相同的,但来自 Python 的整体输出并不相同。doctest
包依赖于示例的文字输出与 Python 的 REPL 产生的输出完全相同。
我们如何确保我们的 doctest 示例真的有效?
准备工作
让我们看一个涉及“集合”对象的例子:
**>>> words = set(
... '''Beautiful is better than ugly.
... Explicit is better than implicit.
... Simple is better than complex.
... Complex is better than complicated.
... '''.replace('.', ' ').split())
>>> words
{'complicated', 'Simple', 'ugly', 'implicit', 'Beautiful',
'complex', 'is', 'Explicit', 'better', 'Complex', 'than'}**
这个例子很简单。然而,结果往往会在每次处理这个例子时有所不同。事实上,在处理安全算法时,让顺序变化被认为是很重要的。这被称为哈希随机化问题——当哈希值是可预测的时,它可能成为安全漏洞。
当我们使用doctest
模块时,我们需要有完全一致的示例。正如我们将在第十一章中看到的,测试,doctest
模块在定位示例方面很聪明,但在确保实际结果与预期结果匹配方面并不是一个天才。
问题主要是出现在集合和字典中。这两个集合中,由于哈希随机化,无法保证键的顺序。
如何做...
当我们需要确保集合或字典中的项目具有特定顺序时,我们可以将集合转换为排序序列。
我们有两种选择:
-
将集合转换为排序序列
-
将字典转换为排序的(key, value)两元组序列
这两个配方都很相似。这是我们需要做的事情,以将一个集合强制转换为一个规范化的结构:
**>>> list(sorted(words))
['Beautiful', 'Complex', 'Explicit', 'Simple', 'better',
'complex', 'complicated', 'implicit', 'is', 'than', 'ugly']**
对于字典,我们经常会使用这个:
list(sorted(some_dictionary.items()))
这将提取字典中的每个项目作为(key, value)
两元组。元组将按键排序。生成的序列将被转换为列表,以便与预期结果进行比较。
它是如何工作的...
当面对一个不强加顺序的集合时,我们必须找到一个具有两个属性的集合:
-
相同的内容
-
某种一致的顺序
Python 的内置结构是三个主题的变体:
-
序列
-
集合
-
映射
由于唯一具有保证顺序的是序列,我们可以将集合和映射转换为序列。结果表明,使用sorted()
函数很容易做到这一点。
对于集合,我们将对项目进行排序。对于映射,我们将对(key, value)
两元组进行排序。这可以确保我们的示例输出恰好符合要求。
还有更多...
我们将在第十一章中看到几种数据的微小变化,测试:
-
浮点数
-
日期
-
对象 ID 和回溯
-
随机序列
所有这些都需要放入一个具有可预测输出的上下文中,以便测试能够重复工作。两种数据结构,“集合”和“字典”,是本章的主题。我们将在相关章节中涵盖其他变化。
理解变量、引用和赋值
变量真正是如何工作的?当我们将一个可变对象分配给两个变量时会发生什么?我们很容易有两个变量共享对一个公共对象的引用;当共享对象是可变的时,这可能导致潜在的混乱结果。规则很简单,后果通常是显而易见的。
我们将专注于这个规则:Python 共享引用。它不复制数据。
我们需要看看这个关于引用共享的规则意味着什么。
我们将创建两种数据结构,一种是可变的,一种是不可变的。我们将使用两种序列,尽管我们可以使用两种集合做类似的事情:
准备好了我们将创建两种数据结构,一种是可变的,一种是不可变的。我们将使用两种类型的序列,尽管我们也可以用两种类型的集合做类似的事情:
**>>> mutable = [1, 1, 2, 3, 5, 8]**
**>>> immutable = (5, 8, 13, 21)**
可变数据结构可以被改变和共享。不可变数据结构也可以被共享,但很难确定它是否被共享。
我们无法轻松地对映射进行这样的操作,因为 Python 没有提供方便的不可变映射。
如何做到...
- 将每个集合分配给一个额外的变量。这将创建两个对结构的引用:
**>>> mutable_b = mutable
>>> immutable_b = immutable**
现在我们有两个对列表[1, 1, 2, 3, 5, 8]
的引用和两个对元组(5, 8, 13, 21)
的引用。
我们可以使用is
运算符来确认这一点。这确定了两个变量是否指向同一个基础对象:
**>>> mutable_b is mutable
True
>>> immutable_b is immutable
True**
- 对集合的两个引用中的一个进行更改。对于可变结构,我们有
append()
或add()
等方法:
**>>> mutable += [mutable[-2] + mutable[-1]]**
对于列表结构,+=
赋值实际上是内部使用extend()
方法。
我们可以用不可变结构做类似的事情:
**>>> immutable += (immutable[-2] + immutable[-1],)**
由于元组没有像extend()
这样的方法,+=
将构建一个新的元组对象,并用该新对象替换immutable
的值。
- 看看结构的另一个引用:
**>>> mutable_b
[1, 1, 2, 3, 5, 8, 13]
>>> mutable is mutable_b
True
>>> immutable_b
(5, 8, 13, 21)
>>> immutable
(5, 8, 13, 21, 34)**
两个变量mutable
和mutable_b
指向同一个基础对象。因此,我们可以使用任一变量来改变对象,并看到改变反映在另一个变量的值中。
两个变量immutable_b
和immutable
最初指向同一个对象。因为对象无法就地突变,对一个变量的更改意味着一个新对象被分配给该变量。另一个变量仍然牢固地附着在原始对象上。
它是如何工作的...
在 Python 中,变量是附加到对象的标签。我们可以把它们想象成暂时贴在对象上的明亮颜色的粘贴纸。
变量是对基础对象的引用。当我们将一个对象分配给一个变量时,我们给基础对象的引用起了一个名字。当我们在表达式中使用一个变量时,Python 会定位变量所指的对象。
对于可变对象,对象的方法可以修改对象的状态。所有引用对象的变量将反映状态的改变,因为变量只是一个引用,而不是完全的副本。
当我们在赋值语句中使用一个变量时,有两种可能的操作:
-
对于提供适当赋值运算符定义的可变对象,赋值被转换为一个特殊方法;在这种情况下,是
__iadd__
。这个特殊方法将改变对象的内部状态。 -
对于不提供
+=
赋值定义的不可变对象,赋值被转换为=
和+
。+
运算符建立了一个新对象,并将变量名附加到该新对象。先前引用被替换的对象的其他变量不受影响,它们继续引用旧对象。
Python 跟踪对象被引用的次数。当引用次数变为零时,对象不再被任何地方使用,可以从内存中删除。
还有更多...
像 C++或 Java 这样的语言除了对象之外还有原始类型。在这些语言中,+=
语句利用了硬件指令或 Java 虚拟机的特性来调整原始类型的值。
Python 没有这种优化。数字是不可变对象。当我们做这样的事情时:
**>>> a = 355
>>> a += 113**
我们不是在调整对象355
的内部状态。这不依赖于内部的__iadd__
特殊方法。这的行为就像我们写了:
**>>> a = a + 113**
表达式a + 113
被评估,一个新的不可变整数对象被创建。这个新对象被标记为a
。以前分配给a
的旧值不再需要。
另请参阅
- 在制作对象的浅复制和深复制中,我们将看看如何复制可变结构
制作对象的浅复制和深复制
在本章中,我们谈到了赋值语句共享对对象的引用。对象通常不会被复制。当我们写:
a = b
现在我们有两个对同一基础对象的引用。如果b
是一个列表,a
和b
都是对同一个可变列表的引用。
正如我们在理解变量、引用和赋值中看到的,对a
变量的更改会改变a
和b
都引用的列表对象。
大多数情况下,这是我们想要的行为。在极少数情况下,我们实际上希望从一个原始对象创建两个独立的对象。
当两个变量引用同一基础对象时,有两种方法可以断开连接:
-
制作结构的浅复制
-
深复制结构
准备工作
我们必须做特殊安排来复制一个对象。我们已经看到了几种用于复制的语法。
-
序列 -
list
和tuple
:我们可以使用sequence[:]
通过使用空切片表达式来复制一个序列。我们也可以使用sequence.copy()
来复制一个名为sequence
的变量。 -
映射 -
dict
:我们可以使用mapping.copy()
来复制一个名为mapping
的字典。 -
集合 -
set
和frozenset
:我们可以使用someset.copy()
来克隆一个名为someset
的集合。
重要的是这些都是浅复制。
浅意味着两个集合将包含对相同基础对象的引用。如果基础对象是不可变的数字或字符串,则这种区别并不重要。当我们无法改变集合中的项目时,项目将被简单地替换。
如果我们有a = [1, 1, 2, 3]
,我们无法对a[0]
进行任何变异。a[0]
中的数字1
没有内部状态。我们只能替换对象。
然而,当涉及到可变对象的集合时,会出现问题。首先,我们将创建一个对象,然后我们将创建一个副本:
**>>> some_dict = {'a': [1, 1, 2, 3]}
>>> another_dict = some_dict.copy()**
我们必须对字典进行浅复制。这两个副本看起来是一样的,因为它们都包含对相同对象的引用。对于不可变字符串a
有一个共享引用。对于可变列表[1, 1, 2, 3]
也有一个共享引用。我们可以显示another_dict
的值,看看它是否与some_dict
相似。
**>>> another_dict
{'a': [1, 1, 2, 3]}**
当我们更新字典副本中的共享列表时会发生什么:
**>>> some_dict['a'].append(5)
>>> another_dict
{'a': [1, 1, 2, 3, 5]}**
我们对一个可变的list
对象进行了更改,这个对象在some_dict
和another_dict
两个dict
对象之间共享。
我们可以使用id()
函数来查看项目是否共享:
**>>> id(some_dict['a']) == id(another_dict['a'])
True**
因为两个id()
值相同,这些是同一个基础对象。与键a
关联的值在some_dict
和another_dict
中是相同的可变列表。我们还可以使用is
运算符来查看它们是否是同一个对象。
这种变异效果也适用于包含其他list
对象的list
集合:
**>>> some_list = [[2, 3, 5], [7, 11, 13]]
>>> another_list = some_list.copy()
>>> some_list is another_list
False
>>> some_list[0] is another_list[0]
True**
我们复制了一个对象some_list
,并将其分配给变量another_list
。顶层list
对象是不同的,但list
中的项目是共享引用。我们使用is
运算符来显示每个列表中的第一个项目都是对同一基础对象的引用。
因为我们不能创建一个包含可变对象的set
,所以我们不需要考虑制作共享项目的浅复制。
如果我们想要完全断开两个副本之间的连接怎么办?如何进行深复制而不是浅复制?
如何做...
Python 通常通过共享引用来工作。它只会勉强复制对象。默认行为是进行浅复制,共享集合内部项目的引用。这是我们如何进行深复制的方法:
- 导入
copy
库:
**>>> import copy**
- 使用
copy.deepcopy()
函数来复制一个对象以及该对象中包含的所有可变项目:
**>>> some_dict = {'a': [1, 1, 2, 3]}
>>> another_dict = copy.deepcopy(some_dict)**
这将创建没有共享引用的副本。对一个副本的可变内部项目的更改不会在其他任何地方产生任何影响:
**>>> some_dict['a'].append(5)
>>> some_dict
{'a': [1, 1, 2, 3, 5]}
>>> another_dict
{'a': [1, 1, 2, 3]}**
我们更新了some_dict
中的一个项目,但它对another_dict
中的副本没有产生影响。我们可以使用id()
函数看到这些对象是不同的:
**>>> id(some_dict['a']) == id(another_dict['a'])
False**
由于id()
值不同,这些是不同的对象。我们还可以使用is
运算符来查看它们是不同的对象。
它是如何工作的...
制作浅拷贝相对容易。我们可以使用生成器表达式编写我们自己的算法版本:
**>>> copy_of_list = [item for item in some_list]
>>> copy_of_dict = {key:value for key, value in some_dict.items()}**
在list
情况下,新list
的项目是对源列表中项目的引用。同样,在dict
情况下,键和值是对源字典键和值的引用。
deepcopy()
函数使用递归算法来查看每个可变集合的内部。
对于list
,概念上的算法大致如下:
immutable = (numbers.Number, tuple, str, bytes)
def deepcopy_list(some_list:
copy = []
for item in some_list:
if isinstance(item, immutable):
copy.append(item)
else:
copy.append(deepcopy(item))
实际的代码当然不是这样的。它在处理每个不同的 Python 类型的方式上更加聪明。然而,这确实提供了一些关于deepcopy()
函数工作原理的提示。
事实证明还有一些额外的考虑。最重要的考虑是一个包含对自身引用的对象。
我们可以这样做:
a = [1, 2, 3]
a.append(a)
这是一个令人困惑但在技术上有效的 Python 构造。当尝试编写一个天真的递归操作来访问列表中的所有项目时,这将导致问题。为了克服这个问题,使用内部缓存,以便项目只被复制一次。之后,可以在缓存中找到内部引用。
另请参阅
- 在理解变量、引用和赋值配方中,我们将看看 Python 更喜欢创建对对象的引用。
避免函数参数的可变默认值
在第三章中,函数定义,我们看了 Python 函数定义的许多方面。在设计带有可选参数的函数配方中,我们展示了处理可选参数的配方。当时,我们没有深入讨论将对可变结构提供引用作为默认值的问题。我们将仔细研究函数参数的可变默认值的后果。
准备工作
让我们想象一个函数,它可以创建或更新一个可变的Counter
对象。我们将其称为gather_stats()
。
理想情况下,它可能看起来像这样:
**>>> from collections import Counter
>>> from random import randint, seed
>>> def gather_stats(n, samples=1000, summary=Counter()):
... summary.update(
... sum(randint(1,6) for d in range(n))
... for _ in range(samples))
... return summary**
这显示了一个具有两个故事的不好设计的函数。第一个故事没有参数集合。函数创建并返回一组统计数据。这是这个故事的例子:
**>>> seed(1)
>>> s1 = gather_stats(2)
>>> s1
Counter({7: 168, 6: 147, 8: 136, 9: 114, 5: 110, 10: 77, 11: 71, 4: 70, 3: 52, 12: 29, 2: 26})**
第二个故事允许我们提供一个显式的参数值,以便统计数据更新给定的对象。这是这个故事的例子:
**>>> seed(1)
>>> mc = Counter()
>>> gather_stats(2, summary=mc)
Counter...
>>> mc
Counter({7: 168, 6: 147, 8: 136, 9: 114, 5: 110, 10: 77, 11: 71, 4: 70, 3: 52, 12: 29, 2: 26})**
我们已经设置了随机数种子,以确保两个随机值序列是相同的。这样可以很容易地确认,如果我们提供一个Counter
对象或使用默认的Counter
对象,结果是相同的。在第二个例子中,我们向函数提供了一个显式的Counter
对象,命名为mc
。
gather_stats()
函数返回一个值。在编写脚本时,我们只需忽略返回的值。在使用 Python 的交互式 REPL 时,输出会被打印出来。我们显示了Counter...
而不是冗长的输出。
当我们在执行前两个操作后执行以下操作时,问题就出现了:
**>>> seed(1)
>>> s3 = gather_stats(2)
>>> s3
Counter({7: 336, 6: 294, 8: 272, 9: 228, 5: 220, 10: 154, 11: 142, 4: 140, 3: 104, 12: 58, 2: 52})**
请注意,计数加倍了。出现了问题。由于这仅在我们多次使用默认故事时发生,它可能通过单元测试套件并且看起来是正确的。
正如我们在制作对象的浅拷贝和深拷贝配方中看到的,Python 更喜欢共享引用。共享的一个后果是:
**>>> s1 is s3
True**
这意味着两个变量s1
和s2
都是对同一基础对象的引用。看起来我们已经更新了一些共享的集合。
这是否意味着s1
的值改变了?
**>>> s1
Counter({7: 336, 6: 294, 8: 272, 9: 228, 5: 220, 10: 154, 11: 142, 4: 140, 3: 104, 12: 58, 2: 52})**
是的,这个 gather_stats()
函数的默认使用似乎在共享一个单一对象。我们如何避免这种情况?
如何做...
解决这个问题有两种方法:
-
提供一个不可变的默认值
-
改变设计
我们首先看看不可变的默认值。通常改变设计是一个更好的主意。为了看到为什么改变设计更好,我们将展示纯技术解决方案。
当我们为函数提供默认值时,默认对象只会被创建一次,并且永远共享。这里是替代方案:
- 用
None
替换任何可变的默认参数值:
def gather_stats(n, samples=1000, summary=None):
- 添加一个
if
语句来检查参数值是否为None
,并将其替换为一个新的可变对象:
if summary is None: summary = Counter()
这将确保每次函数在没有参数值的情况下被评估时,我们都创建一个新的可变对象。我们将避免一次又一次地共享一个可变对象。
提供可变对象作为函数的默认值的很少有好理由。在大多数情况下,我们应该考虑改变设计,不要使用可变对象作为参数的默认值。在极少数情况下,如果我们真的有一个可以更新对象或创建新对象的复杂算法,我们应该考虑定义两个单独的函数。
我们将重构这个函数,使其看起来像这样:
def create_stats(n, samples=1000):
return update_stats(n, samples, Counter())
def update_stats(n, samples=1000, summary):
summary.update(
sum(randint(1,6) for d in range(n))
for _ in range(samples))
我们创建了两个单独的函数。这将分开两个故事,以避免混淆。可选的可变参数的想法本来就不是一个好主意。
它是如何工作的...
正如我们之前指出的,Python 更喜欢共享引用。它很少创建对象的副本。因此,函数参数值的默认值将是共享对象。Python 不容易创建新的对象。
这个规则非常重要,经常让刚接触 Python 的程序员感到困惑。
提示
不要在函数中使用可变默认值。
可变对象(set
、list
、dict
)不应该是函数参数的默认值。
这个规则适用于核心语言。然而,它并不适用于整个标准库。有些情况下,有一些巧妙的替代方法。
还有更多...
在标准库中,有一些示例展示了一个很酷的技术,它展示了我们如何创建新的默认对象。一个广泛使用的例子是 defaultdict
集合。当我们创建一个 defaultdict
时,我们提供一个无参数函数,用于创建新的字典条目。
当字典中缺少一个键时,给定的函数将被评估以计算一个新的默认值。在 defaultdict(int)
的情况下,我们使用 int()
函数来创建一个不可变对象。正如我们所见,不可变对象的默认值不会引起任何问题,因为不可变对象没有内部状态。
当我们使用 defaultdict(list)
或 defaultdict(set)
时,我们可以看到这种设计模式的真正力量。当一个键缺失时,会创建一个新的空 list
(或 set
)。
defaultdict
使用的评估函数模式并不适用于函数本身的操作方式。大多数情况下,我们为函数参数提供的默认值是不可变对象,比如数字、字符串或元组。必须使用 lambda
来包装一个不可变对象,这当然是可能的,但令人讨厌,因为这是一个很常见的情况。
为了利用这种技术,我们需要修改我们示例函数的设计。我们将不再在函数中更新现有的计数器对象。我们将始终创建一个新的对象。我们可以修改创建的对象的类。
这是一个函数,允许我们在我们不想使用默认的 Counter
类时插入一个不同的类。
**>>> def gather_stats(n, samples=1000, summary_func=lambda x:Counter(x)):
... summary = summary_func(
... sum(randint(1,6) for d in range(n))
... for _ in range(samples))
... return summary**
对于这个版本,我们定义了一个初始化值,它是一个参数的函数。默认情况下,这个单参数函数将应用于随机样本的生成函数。我们可以用另一个单参数函数覆盖这个函数,这个函数将收集数据。这将使用任何可以收集数据的对象构建一个新的对象。
以下是使用list()
的示例:
**>>> seed(1)
>>> gather_stats(2, 12, summary_func=list)
[7, 4, 5, 8, 10, 3, 5, 8, 6, 10, 9, 7]**
在这种情况下,我们提供了list()
函数来创建一个包含各个随机样本的列表。
以下是一个没有参数值的示例。它将创建一个Counter
对象:
**>>> seed(1)
>>> gather_stats(2, 12)
Counter({5: 2, 7: 2, 8: 2, 10: 2, 3: 1, 4: 1, 6: 1, 9: 1})**
在这种情况下,我们使用了默认值。该函数从随机样本创建了一个Counter()
对象。
另请参阅
- 请参阅创建字典-插入和更新配方,其中显示了
defaultdict
的工作原理