在本章中,我们将介绍以下配方:
- 搜索、排序、筛选已排序容器中的高性能搜索
- 获取任意 iterable 的 nth元素,同时抓取任意 iterable 的nthth元素
- 将相似项分组将 iterable 拆分为相似项组
- 将多个 iterable 中的数据压缩合并为单个 iterable
- 展平列表将列表列表转换为平面列表
- 产生置换并计算一组元素的所有可能置换
- 二元函数在可数函数中的应用
- 通过缓存函数加快计算速度
- 函数的运算符如何为 Python 运算符保留对可调用项的引用
- 通过预先应用某些函数来减少函数的参数数
- 泛型函数能够根据提供的参数类型更改行为的函数
- 适当的修饰适当地修饰函数以避免丢失其签名和 docstring
- 上下文管理器在您输入和退出代码块时自动运行代码
- 应用可变上下文管理器如何应用可变数量的上下文管理器
在编写软件时,您会发现自己在独立于您正在编写的应用程序类型的情况下一遍又一遍地做很多事情。
除了可能需要在不同应用程序之间重用的全部功能(如登录、日志记录和授权)之外,还有一些小构建块可以在任何类型的软件中重用。
本章将尝试收集一组配方,这些配方可用作可重用代码段,以实现非常常见的操作,这些操作可能需要独立于软件的用途来执行。
搜索元素是编程中非常常见的需求。在容器中查找项目基本上是代码可能执行的最频繁的操作,因此快速可靠非常重要。
排序经常与搜索联系在一起,因为当您知道集合已排序时,通常可以使用更智能的查找解决方案,而排序意味着不断搜索和移动项目,直到它们按顺序排序为止。所以他们经常一起去。
Python 有内置函数来对任何类型的容器进行排序并查找其中的项,即使使用能够利用排序序列的函数也是如此。
对于该配方,需执行以下步骤:
- 采用以下一组元素:
>>> values = [ 5, 3, 1, 7 ]
- 可以通过
in
操作符在序列中查找元素:
>>> 5 in values
True
- 可通过
sorted
功能进行分拣:
>>> sorted_value = sorted(values)
>>> sorted_values
[ 1, 3, 5, 7 ]
- 一旦我们有了一个已排序的容器,我们实际上可以使用
bisect
模块更快地查找包含的条目:
def bisect_search(container, value):
index = bisect.bisect_left(container, value)
return index < len(container) and container[index] == value
bisect_search
可以用来知道一个条目是否在列表中,就像in
操作符所做的那样:
>>> bisect_search(sorted_values, 5)
True
- 但是,优点是,对于许多已排序的条目,它可以快得多:
>>> import timeit
>>> values = list(range(1000))
>>> 900 in values
True
>>> bisect_search(values, 900)
True
>>> timeit.timeit(lambda: 900 in values)
timeit.timeit(lambda: bisect_search(values, 900))
13.61617108999053
>>> timeit.timeit(lambda: bisect_search(values, 900))
0.872136551013682
因此,在我们的示例中,bisect_search
函数比普通查找快 17 倍。
bisect
模块使用二分法搜索在已排序的容器中查找元素的插入点。
如果数组中存在某个元素,则其插入位置正好是该元素所在的位置(因为它应该正好位于该位置):
>>> values = [ 1, 3, 5, 7 ]
>>> bisect.bisect_left(values, 5)
2
如果缺少元素,它将返回下一个更大元素的位置:
>>> bisect.bisect_left(values, 4)
2
这意味着即使对于容器中不存在的元素,我们也将获得一个位置。这就是为什么我们将返回位置的元素与我们正在寻找的元素进行比较。如果两者不同,则表示返回了最近的元素,因此找不到元素本身。
出于同样的原因,如果找不到元素并且它大于容器中包含的最大值,则返回容器本身的长度(因为元素应该在末尾),因此我们还需要确保我们index < len(container)
检查容器中不存在的元素。
到目前为止,我们只对条目本身进行了排序和查找,但在许多情况下,您将拥有复杂的对象,您对排序和搜索对象的特定属性感兴趣。
例如,您可能有一个人员列表,并希望按其姓名排序:
class Person:
def __init__(self, name, surname):
self.name = name
self.surname = surname
def __repr__(self):
return '<Person: %s %s>' % (self.name, self.surname)
people = [Person('Derek', 'Zoolander'),
Person('Alex', 'Zanardi'),
Person('Vito', 'Corleone')
Person('Mario', 'Rossi')]
通过依赖sorted
函数的key
参数,可以按姓名对这些人进行排序,该参数指定了一个可调用项,该可调用项应返回条目应排序的值:
>>> sorted_people = sorted(people, key=lambda v: v.name)
[<Person: Alex Zanardi>, <Person: Derek Zoolander>,
<Person: Mario Rossi>, <Person: Vito Corleone>]
通过key
函数排序比通过比较函数排序快得多。因为key
函数每项只需要调用一次(然后结果被保留),而comparison
函数需要在每次有两项需要比较时反复调用。因此,如果计算我们应该排序的值是昂贵的,key
函数方法可以实现显著的性能改进。
现在的问题是bisect
不允许我们提供密钥,所以为了能够在人员列表上使用bisect
,我们必须首先建立一个keys
列表,在这里我们可以应用bisect
:
>>> keys = [p.name for p in people]
>>> bisect_search(keys, 'Alex')
True
这需要再次遍历列表来构建keys
列表,因此只有在必须查找多个条目(或多次查找同一条目)时才方便,否则在整个列表中进行线性搜索会更快。
注意,即使要使用in
操作符,您也必须构建keys
列表。因此,如果你想在不建立特别列表的情况下搜索一个属性,你将不得不依赖于过滤filter
或列表理解。
随机访问容器是我们经常做的事情,没有太多问题。对于大多数容器类型,这甚至是一个非常便宜的操作。当在另一边使用通用的 iterables 和生成器时,这并不像我们预期的那么容易,它常常以我们将它们转换为列表或丑陋的for
循环而告终。
Python 标准库实际上有一些方法可以使这一点变得非常简单。
使用 iterables 时,itertools
模块是一个宝贵的功能宝库,只需稍加努力即可获得任何 iterables 的*nth*项:
import itertools
def iter_nth(iterable, nth):
return next(itertools.islice(iterable, nth, nth+1))
给定一个随机 iterable,我们可以使用它来获取所需的元素:
>>> values = (x for x in range(10))
>>> iter_nth(values, 4)
4
itertools.islice
函数可以对任何 iterable 进行切片。在我们的特定情况下,我们需要从要查找的元素到下一个元素的切片。
一旦我们得到了包含我们正在寻找的元素的切片,我们就需要从切片本身提取该项。
当islice
作用于 iterables 时,它本身返回一个 iterable。这意味着我们可以使用next
来消费它,因为我们要寻找的物品实际上是第一个切片,使用next
将正确返回我们要寻找的物品。
如果项目超出范围(例如,我们只查找三个项目中的第四个),则会产生一个StopIteration
错误,我们可以像在正常列表中捕捉IndexError
一样捕捉它。
有时,您可能会遇到一个包含多个重复条目的条目列表,您可能希望根据某种属性对类似条目进行分组。
例如,以下是姓名列表:
names = [('Alex', 'Zanardi'),
('Julius', 'Caesar'),
('Anakin', 'Skywalker'),
('Joseph', 'Joestar')]
我们可能想建立一个由名字以相同字符开头的所有人组成的小组,这样我们就可以按字母顺序保存电话簿,而不是把名字随意分散在各处。
itertools
模块同样是一个非常强大的工具,它为我们提供了处理 iterables 所需的基础:
import itertools
def group_by_key(iterable, key):
iterable = sorted(iterable, key=key)
return {k: list(g) for k,g in itertools.groupby(iterable, key)}
给定名称列表,我们可以应用一个键函数来获取名称的第一个字符,以便将所有条目按其分组:
>>> group_by_key(names, lambda v: v[0][0])
{'A': [('Alex', 'Zanardi'), ('Anakin', 'Skywalker')],
'J': [('Julius', 'Caesar'), ('Joseph', 'Joestar')]}
此处的功能核心由itertools.groupby
提供。
此函数用于向前移动迭代器,获取项目,并将其添加到当前组。当面对具有不同键的项时,将创建一个新组。
因此,实际上,它只会将共享同一密钥的附近条目分组:
>>> sample = [1, 2, 1, 1]
>>> [(k, list(g)) for k,g in itertools.groupby(sample)]
[(1, [1]), (2, [2]), (1, [1, 1])]
如您所见,有三组而不是预期的两组,因为第一组1
立即被数字2
打断,因此我们最终得到两组不同的1
。
我们先对元素进行排序,然后再对它们进行分组,原因是排序可以确保相等的元素彼此靠近:
>>> sorted(sample)
[1, 1, 1, 2]
此时,分组函数将创建正确数量的组,因为每个等效元素都有一个块:
>>> sorted_sample = sorted(sample)
>>> [(k, list(g)) for k,g in itertools.groupby(sorted_sample)]
[(1, [1, 1, 1]), (2, [2])]
在现实生活中,我们经常处理复杂的对象,因此group_by_key
函数也接受key
函数。这将说明应为哪些关键元素分组。
由于 sorted 在排序时接受键函数,因此我们知道在分组之前,所有元素都将针对该键进行排序,因此我们将返回正确的组数。
最后,由于groupby
返回一个或多个迭代器(顶部 iterable 中的每个组也是一个迭代器),我们将每个组转换为一个列表,并从这些组中构建一个字典,以便key
可以轻松访问它们。
压缩意味着附加两个不同的 ITerable 以创建一个新的 ITerable,其中包含来自这两个 ITerable 的值。
当您有多个应同时进行的值跟踪时,这非常方便。假设你有名字和姓氏,你只想得到一份名单:
names = [ 'Sam', 'Axel', 'Aerith' ]
surnames = [ 'Fisher', 'Foley', 'Gainsborough' ]
我们想把名字和姓氏拼凑在一起:
>>> people = zip(names, surnames)
>>> list(people)
[('Sam', 'Fisher'), ('Axel', 'Foley'), ('Aerith', 'Gainsborough')]
Zip 将创建一个新的 iterable,其中新创建的 iterable 中的每个项目都是一个集合,该集合是通过为每个提供的 iterable 选择一个项目来创建的。
所以,result[0] = (i[0], j[0])
和result[1] = (i[1], j[1])
等等。如果i
和j
的长度不同,当其中一个用尽时,它将立即停止。
如果您希望继续进行,直到用尽所提供的 iterables 中最长的一个,而不是在最短的 iterables 上停止,您可以依赖itertools.zip_longest
。已用尽的 iterables 中的值将用默认值填充。
当您有多个嵌套列表时,通常只需迭代列表中包含的所有项,而对它们实际存储的深度不太感兴趣。
假设您有以下列表:
values = [['a', 'b', 'c'],
[1, 2, 3],
['X', 'Y', 'Z']]
如果您只想获取其中的所有项目,那么您真的不想在列表中的列表上迭代,然后在每个列表的项目上迭代。我们只需要叶项目,我们根本不关心它们是否在列表中的列表中。
我们要做的是将所有列表连接到一个单独的 iterable 中,该 iterable 将生成项目本身,正如我们所说的迭代器,itertools
模块具有正确的功能,允许我们将所有列表链接起来,就像它们是一个单独的列表一样:
>>> import itertools
>>> chained = itertools.chain.from_iterable(values)
当使用时,生成的chained
迭代器将一个接一个地生成基础项:
>>> list(chained)
['a', 'b', 'c', 1, 2, 3, 'X', 'Y', 'Z']
itertools.chain
函数是一个非常方便的函数,当您必须一个接一个地使用多个 iTerable 时。
默认情况下,它接受这些 iterables 作为参数,因此我们必须:
itertools.chain(values[0], values[1], values[2])
但是,为了方便起见,itertools.chain.from_iterable
将链接所提供参数中包含的条目,而不必逐个显式传递它们。
如果您知道原始列表包含多少项,并且它们具有相同的大小,则很容易应用反向操作。
我们已经知道可以使用zip
合并来自多个来源的条目,所以我们实际上想要做的是将属于同一原始列表的元素压缩在一起,这样我们就可以从chained
返回到原始列表:
>>> list(zip(chained, chained, chained))
[('a', 'b', 'c'), (1, 2, 3), ('X', 'Y', 'Z')]
在本例中,我们有三个项目列表,因此我们必须提供chained
三次。
这是因为zip
将从每个提供的参数中依次使用一个条目。因此,当我们三次提供相同的参数时,我们实际上是在使用前三个条目,然后是下三个条目,然后是最后三个条目。
如果chained
是一个列表而不是迭代器,我们必须从列表中创建一个迭代器:
>>> chained = list(chained)
>>> chained ['a', 'b', 'c', 1, 2, 3, 'X', 'Y', 'Z']
>>> ichained = iter(chained)
>>> list(zip(ichained, ichained, ichained)) [('a', 'b', 'c'), (1, 2, 3), ('X', 'Y', 'Z')]
如果我们不使用ichained
,而是使用原始chained
,结果将与我们想要的相差甚远:
>>> chained = list(chained)
>>> chained
['a', 'b', 'c', 1, 2, 3, 'X', 'Y', 'Z']
>>> list(zip(chained, chained, chained))
[('a', 'a', 'a'), ('b', 'b', 'b'), ('c', 'c', 'c'),
(1, 1, 1), (2, 2, 2), (3, 3, 3),
('X', 'X', 'X'), ('Y', 'Y', 'Y'), ('Z', 'Z', 'Z')]
给定一组元素,如果您曾经觉得需要为这些元素的每个可能排列做些什么,您可能会想知道生成所有这些排列的最佳方法是什么。
Python 在itertools
模块中有各种各样的函数,这些函数有助于排列和组合,它们之间的区别并不总是很容易理解,但是一旦你研究了它们的作用,它们就会变得清晰起来。
笛卡尔积通常是人们在谈论组合和置换时所想到的。
- 给定一组元素,
A
、B
和C
,我们希望提取两个元素的所有可能对,AA
、AB
、AC
等等:
>>> import itertools
>>> c = itertools.product(('A', 'B', 'C'), repeat=2)
>>> list(c)
[('A', 'A'), ('A', 'B'), ('A', 'C'),
('B', 'A'), ('B', 'B'), ('B', 'C'),
('C', 'A'), ('C', 'B'), ('C', 'C')]
- 如果您想省略重复的条目(
AA
、BB
、CC
,您可以使用排列:
>>> c = itertools.permutations(('A', 'B', 'C'), 2)
>>> list(c)
[('A', 'B'), ('A', 'C'),
('B', 'A'), ('B', 'C'),
('C', 'A'), ('C', 'B')]
- 您甚至可能希望确保同一对夫妇不会发生两次(例如
AB
对BA
),在这种情况下,itertools.combinations
可能就是您想要的:
>>> c = itertools.combinations(('A', 'B', 'C'), 2)
>>> list(c)
[('A', 'B'), ('A', 'C'), ('B', 'C')]
因此,通过itertools
模块提供的功能,可以轻松解决组合一组值的大多数需求。
当您需要将函数应用于 iterable 的所有元素并获取结果值时,列表理解和map
是非常方便的工具。但这些函数主要用于应用一元函数并保留一组转换后的值(例如,将1
添加到所有数字中),但如果要应用应同时接收多个元素的函数,它们就不太适合。
相反,归约和累加函数用于从 iterable 接收多个值,并返回单个值(在归约的情况下)或多个值(在累加的情况下)。
此配方的步骤如下所示:
- 最简单的缩减示例是将 iterable 中的所有项相加:
>>> values = [ 1, 2, 3, 4, 5 ]
- 这是
sum
可以轻松完成的事情,但是为了这个例子,我们将使用reduce
:
>>> import functools, operator
>>> functools.reduce(operator.add, values)
15
- 如果您希望保留中间步骤的结果,而不是单一的最终结果,您可以使用
accumulate
:
>>> import itertools
>>> list(itertools.accumulate(values, operator.add))
[1, 3, 6, 10, 15]
accumulate
和reduce
不限于数学用途。虽然这些都是最明显的例子,但它们是非常灵活的功能,其用途的变化取决于它们将要应用的功能。
例如,如果您有多行文本,您也可以使用reduce
计算所有文本的总和:
>>> lines = ['this is the first line',
... 'then there is one more',
... 'and finally the last one.']
>>> functools.reduce(lambda x, y: x + len(y), [0] + lines)
69
或者,如果您有多个词典,则需要折叠:
>>> dicts = [dict(name='Alessandro'), dict(surname='Molina'),
... dict(country='Italy')]
>>> functools.reduce(lambda d1, d2: {**d1, **d2}, dicts)
{'name': 'Alessandro', 'surname': 'Molina', 'country': 'Italy'}
这甚至是访问深度嵌套词典的一种非常方便的方式:
>>> import operator
>>> nesty = {'a': {'b': {'c': {'d': {'e': {'f': 'OK'}}}}}}
>>> functools.reduce(operator.getitem, 'abcdef', nesty)
'OK'
反复运行函数时,避免调用该函数的成本可以大大加快生成代码的速度。
想想一个for
循环或者一个递归函数,它可能需要多次调用该函数。如果不调用它,它可以保留以前调用函数的已知结果,那么它可以使代码更快。
最常见的例子是斐波那契序列。通过将前两个数字相加来计算序列,然后将第二个数字添加到结果中,依此类推。
这意味着在序列1
、1
、2
、3
、5
中,计算5
需要我们计算3 + 2
,这需要我们计算2 + 1
,这需要我们计算1 + 1
。
以递归的方式进行斐波那契序列是最明显的方法,因为它导致了5 = fib(n3) + fib(n2)
,它由3 = fib(n2) + fib(n1)
组成,因此您可以很容易地看到我们必须计算fib(n2)
两次。记住fib(n2)
的结果将允许我们只执行一次这样的计算,然后在下一次调用中重用结果。
以下是此配方的步骤:
- Python 提供了一个内置的 LRU 缓存,我们可以将其用于记忆:
import functools
@functools.lru_cache(maxsize=None)
def fibonacci(n):
'''inefficient recursive version of Fibonacci number'''
if n > 1:
return fibonacci(n-1) + fibonacci(n-2)
return n
- 然后,我们可以使用该函数计算完整序列:
fibonacci_seq = [fibonacci(n) for n in range(100)]
- 结果将是一个包含截至第 100个的所有斐波那契数的列表:
>>> print(fibonacci_seq)
[0, 1, 1, 2, 3, 5, 8, 13, 21 ...
性能上的差异是巨大的。如果我们使用timeit
模块来计时我们的函数,我们可以很容易地看到记忆对性能的帮助有多大。
- 当使用
fibonacci
函数的记忆版本时,计算在不到一毫秒的时间内结束:
>>> import timeit
>>> timeit.timeit(lambda: [fibonacci(n) for n in range(40)], number=1)
0.000033469987101
- 然后,如果我们删除实现了记忆化的
@functools.lru_cache()
,时间会发生根本性的变化:
>>> timeit.timeit(lambda: [fibonacci(n) for n in range(40)], number=1)
89.14927123498637
因此,很容易看出记忆化是如何将性能从 89 秒改为几分之一秒的。
无论何时调用函数,functools.lru_cache
都会将返回值与提供的参数一起保存。
下次调用函数时,将在保存的参数中搜索这些参数,如果找到这些参数,将提供以前返回的值,而不是调用函数。
事实上,这将调用函数的成本更改为仅在字典中查找的成本。
因此,我们第一次调用fibonacci(5)
时,它会被计算,然后下次调用它时,它不会做任何事情,并且之前为5
存储的值会被返回。由于fibonacci(6)
必须调用fibonacci(5)
才能进行计算,因此很容易看出我们是如何为fibonacci(n)
中的n>5
提供主要性能优势的。
同样,正如我们想要整个序列一样,保存的不仅仅是单个调用,而是列表中第一个需要记忆值的调用之后的每个调用。
lru_cache
函数是作为最近使用最少的(LRU)缓存而诞生的,因此默认情况下,它将只保留128
最近使用的缓存,但通过maxsize=None
,我们可以将其用作标准缓存并丢弃其中的 LRU 部分。所有调用都将永久缓存,没有限制。
纯粹对于 Fibonacci 情况,您会注意到将maxsize
设置为大于3
的任何值都不会改变任何内容,因为每个 Fibonacci 数只需要前两次调用就可以计算。
假设您想创建一个简单的计算器。第一步是解析用户将要编写的公式,以便能够执行它。基本公式由一个运算符和两个操作数组成,因此在实践中,您有一个函数及其参数。
但是给定+
、-
等等,我们如何让解析器返回相关函数呢?通常两个数字相加,我们只写n1 + n2
,但我们不能将+
本身传递给任何n1
和n2
来调用。
这是因为+
是一个运算符,而不是一个函数,但它仍然只是 CPython 中执行的一个函数。
我们可以使用operator
模块获取一个可调用的对象,该对象表示我们可以存储或传递的任何 Python 操作符:
import operator
operators = {
'+': operator.add,
'-': operator.sub,
'*': operator.mul,
'/': operator.truediv
}
def calculate(expression):
parts = expression.split()
try:
result = int(parts[0])
except:
raise ValueError('First argument of expression must be numberic')
operator = None
for part in parts[1:]:
try:
num = int(part)
if operator is None:
raise ValueError('No operator proviede for the numbers')
except ValueError:
if operator:
raise ValueError('operator already provided')
operator = operators[part]
else:
result = operator(result, num)
operator = None
return result
我们的calculate
函数充当一个非常基本的计算器(没有运算符优先级、实数、负数等):
>>> print(calculate('5 + 3'))
8
>>> print(calculate('1 + 2 + 3'))
6
>>> print(calculate('3 * 2 + 4'))
10
因此,我们能够在operators
字典中存储四个数学运算符的函数,并根据表达式中遇到的文本进行查找。
在calculate
中,表达式被空格分割,因此5 + 3
变为['5', '+', '3']
。一旦我们有了表达式的三个元素(两个操作数和运算符),我们就可以迭代各个部分,当我们遇到+
时,在operators
字典中查找它,得到应该调用的相关函数,即operator.add
。
operator
模块包含最常见 Python 操作符的函数,从比较(operator.gt
到基于点的属性访问(operator.attrgetter
)。
提供的大多数功能都是与map
、sorted
、filter
等配对的。
我们已经知道,我们可以使用map
将一元函数应用于多个元素,并使用reduce
将二元函数应用于多个元素。
有一整套函数接受 Python 中的可调用函数并将其应用于一组项。
主要的问题是,我们要应用的可调用项通常可能有一个稍微不同的签名,虽然我们可以通过将可调用项包装到另一个可调用项中来解决这个问题,但如果您只想将函数应用于一组项,这不是很方便。
例如,如果要将列表中的所有数字乘以 3,则没有函数将给定参数乘以 3。
我们可以很容易地将operator.mul
调整为一元函数,然后将其传递给map
以将其应用于整个列表:
>>> import functools, operator
>>>
>>> values = range(10)
>>> mul3 = functools.partial(operator.mul, 3)
>>> list(map(mul3, values))
[0, 3, 6, 9, 12, 15, 18, 21, 24, 27]
如您所见,operator.mul
被调用时使用3
和该项作为其参数,因此返回item*3
。
我们通过functools.partial
创建了一个新的mul3
可调用。此可调用函数只调用operator.mul
,将3
作为第一个参数传递,然后将提供给可调用函数的任何参数作为第二个、第三个参数传递给operator.mul
,依此类推。
所以,最终,做mul3(5)
意味着operator.mul(3, 5)
。
这是因为functools.partial
从提供的函数中创建一个新函数,并将提供的参数硬连接起来。
当然,也可以传递关键字参数,这样我们就可以设置任何参数,而不是硬连接第一个参数。
然后通过map
将生成的函数应用于所有数字,从而创建一个新列表,其中所有数字从 0 到 10 乘以 3。
泛型函数是我最喜欢的标准库功能之一。Python 是一种非常动态的语言,通过 duck 类型,您经常能够编写在许多不同条件下工作的代码(不管您是收到列表还是元组),但在某些情况下,您确实需要根据收到的输入拥有两个完全不同的代码基。
例如,我们可能希望有一个函数以人类可读的格式打印所提供字典的内容,但我们也希望它能在元组列表上正常工作,并报告不支持类型的错误。
functools.singledispatch
decorator 允许我们基于参数类型实现一般分派:
from functools import singledispatch
@singledispatch
def human_readable(d):
raise ValueError('Unsupported argument type %s' % type(d))
@human_readable.register(dict)
def human_readable_dict(d):
for key, value in d.items():
print('{}: {}'.format(key, value))
@human_readable.register(list)
@human_readable.register(tuple)
def human_readable_list(d):
for key, value in d:
print('{}: {}'.format(key, value))
调用这三个函数将正确地将请求分派到正确的函数:
>>> human_readable({'name': 'Tifa', 'surname': 'Lockhart'})
name: Tifa
surname: Lockhart
>>> human_readable([('name', 'Nobuo'), ('surname', 'Uematsu')])
name: Nobuo
surname: Uematsu
>>> human_readable(5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in human_readable
ValueError: Unsupported argument type <class 'int'>
用@singledispatch
修饰的函数实际上被参数类型的检查所取代。
对human_readable.register
的每次调用都将记录到一个注册表中,每个参数类型都应使用该注册表:
>>> human_readable.registry
mappingproxy({
<class 'list'>: <function human_readable_list at 0x10464da60>,
<class 'object'>: <function human_readable at 0x10464d6a8>,
<class 'dict'>: <function human_readable_dict at 0x10464d950>,
<class 'tuple'>: <function human_readable_list at 0x10464da60>
})
无论何时调用修饰函数,它都会在注册表中查找参数的类型,并将调用转发给相关函数执行。
用@singledispatch
修饰的函数应该始终是泛型实现,在参数不受显式支持的情况下应该使用泛型实现。
在我们的示例中,这只是抛出一个错误,但它通常会尝试提供一个在大多数情况下都有效的实现。
然后可以向@function.register
注册具体的实现,以覆盖主函数无法覆盖的情况,或者如果主函数只是抛出一个错误,则实际实现该行为。
对于第一次面对装饰器的人来说,装饰器通常并不简单,但一旦你习惯了,装饰器就成为了一个非常方便的工具,可以扩展函数的行为或实现轻量级的面向方面编程。
但即使装饰师成为自然人,成为日常发展的一部分,他们的微妙之处在你第一次面对他们之前并不明显。
当您应用decorator
时,可能不太明显,但通过使用它们,您正在更改decorated
函数的签名,直至函数本身的名称及其文档丢失:
def decorator(f):
def _f(*args, **kwargs):
return f(*args, **kwargs)
return _f
@decorator
def sumtwo(a, b):
"""Sums a and b"""
return a + back
sumtwo
函数用decorator
修饰,但现在,如果我们尝试访问函数文档或名称,它们将无法再访问:
>>> print(sumtwo.__name__)
'_f'
>>> print(sumtwo.__doc__)
None
尽管我们为sumtwo
提供了一个 docstring,并且我们确信它被命名为sumtwo
,但我们需要确保我们的装饰被正确应用,并保留原始函数的属性。
您需要为此配方执行以下步骤:
- Python 标准库提供了一个
functools.wraps
修饰符,可应用于修饰符,使其保留装饰函数的属性:
from functools import wraps
def decorator(f):
@wraps(f)
def _f(*args, **kwargs):
return f(*args, **kwargs)
return _f
- 这里,我们将装饰器应用于函数:
@decorator
def sumthree(a, b):
"""Sums a and b"""
return a + back
- 如您所见,它将正确保留函数的名称和 docstring:
>>> print(sumthree.__name__)
'sumthree'
>>> print(sumthree.__doc__)
'Sums a and b'
如果修饰函数具有自定义属性,则这些属性也将复制到新函数中。
functools.wraps
是一个非常方便的工具,尽最大努力确保装饰后的功能与原始功能完全相同。
但是,虽然函数的属性很容易复制,但函数本身的签名却不容易复制。
因此,检查修饰函数参数不会返回原始参数:
>>> import inspect
>>> inspect.getfullargspec(sumthree)
FullArgSpec(args=[], varargs='args', varkw='kwargs', defaults=None,
kwonlyargs=[], kwonlydefaults=None, annotations={})
因此,报告的参数只是*args
和**kwargs
,而不是a
和b
。要访问实际参数,我们必须通过__wrapped__
属性深入了解底层函数:
>>> inspect.getfullargspec(sumthree.__wrapped__)
FullArgSpec(args=['a', 'b'], varargs=None, varkw=None, defaults=None,
kwonlyargs=[], kwonlydefaults=None, annotations={})
幸运的是,标准库为我们提供了一个inspect.signature
函数:
>>> inspect.signature(sumthree)
(a, b)
因此,当我们想要检查函数的参数以支持修饰函数和未修饰函数时,最好依赖inspect.signature
。
应用装饰也可能与其他装饰程序发生冲突。最常见的例子是classmethod
:
class MyClass(object):
@decorator
@classmethod
def dosum(cls, a, b):
return a+b
试图装饰classmethod
通常不起作用:
>>> MyClass.dosum(3, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
return f(*args, **kwargs)
TypeError: 'classmethod' object is not callable
您需要确保@classmethod
始终是最后一个应用的装饰器,以确保其按预期工作:
class MyClass(object):
@classmethod
@decorator
def dosum(cls, a, b):
return a+b
此时,classmethod
将按预期工作:
>>> MyClass.dosum(3, 3)
6
有太多与装饰器相关的怪癖,以至于 Python 环境中有一些库试图为日常使用正确地实现装饰。如果你不想考虑如何处理它们,你可能想试试wrapt
图书馆,它将为你处理大多数装饰上的奇怪之处。
decorator 可用于确保在您进入和退出函数时执行某些内容,但在某些情况下,您可能希望确保始终在代码块的开头和结尾执行某些内容,而不必将其移动到自己的函数,也不必重写每次应执行的部分。
上下文管理器的存在就是为了解决这一需求,它分解出您必须反复重写的代码来代替try:except:finally:
子句。
上下文管理器最常见的用法可能是关闭上下文管理器,它确保开发人员使用完文件后关闭它们,但标准库使编写新文件变得容易。
对于该配方,需执行以下步骤:
contextlib
提供了与上下文管理器相关的功能,contextlib.contextmanager
可以让编写上下文管理器变得非常简单:
@contextlib.contextmanager
def logentrance():
print('Enter')
yield
print('Exit')
- 然后,创建的上下文管理器可以像任何其他上下文管理器一样使用:
>>> with logentrance():
>>> print('This is inside')
Enter
This is inside
Exit
- 在包装块中引发的异常将传播到上下文管理器,因此可以使用标准的
try:except:finally:
子句处理它们,并进行适当的清理:
@contextlib.contextmanager
def logentrance():
print('Enter')
try:
yield
except:
print('Exception')
raise
finally:
print('Exit')
- 更改的上下文管理器将能够记录异常,而不会干扰异常传播:
>>> with logentrance():
raise Exception('This is an error')
Enter
Exception
Exit
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
raise Exception('This is an error')
Exception: This is an error
使用上下文管理器时,必须依赖于with
语句来应用它们。虽然可以通过使用逗号分隔每条语句来应用多个上下文管理器,但应用数量可变的上下文管理器并不容易:
@contextlib.contextmanager
def first():
print('First')
yield
@contextlib.contextmanager
def second():
print('Second')
yield
编写代码时,必须知道要应用的上下文管理器:
>>> with first(), second():
>>> print('Inside')
First
Second
Inside
但是,如果有时我们只想应用first
上下文管理器,而有时我们想同时应用这两个呢?
contextlib.ExitStack
有多种用途,其中之一是允许我们对一个块应用不同数量的上下文管理器。
例如,我们可能只希望在循环中打印偶数时应用这两个上下文管理器:
from contextlib import ExitStack
for n in range(5):
with ExitStack() as stack:
stack.enter_context(first())
if n % 2 == 0:
stack.enter_context(second())
print('NUMBER: {}'.format(n))
结果将是second
只添加到上下文中,因此对偶数调用:
First
Second
NUMBER: 0
First
NUMBER: 1
First
Second
NUMBER: 2
First
NUMBER: 3
First
Second
NUMBER: 4
如您所见,对于1
和3
,仅打印First
。
当然,当退出通过ExitStack
上下文管理器声明的上下文时,ExitStack
中注册的所有上下文管理器也将退出。