Python 是少数(或至少是最早)包含函数特性的非函数语言之一。虽然 Guido van Rossum 几次试图删除其中一些,但它们已经在 Python 社区中根深蒂固,列表理解(dict和set理解很快就会出现)在各种代码中广泛使用。关于代码最重要的事情不应该是你的reduce语句有多酷,或者你如何用一个不可理解的列表将整个函数放在一行中。可读性计数(再次,PEP20!
本章将向您展示 Python 函数式编程提供的一些很酷的技巧,并解释 Python 实现的一些限制。虽然我们将尽量避开 lambda 演算(λ演算),但将简要讨论Y 组合子。
最后几段将列出(并解释)functools和itertools库的用法。如果您熟悉这些库,可以跳过它们,但请注意,其中一些库将在后面关于装饰器的章节中大量使用(第 5 章、装饰器–通过装饰实现代码重用)、生成器(第 6 章、生成器和协同程序–无限,一次一步和性能(第 12 章、性能–跟踪并减少内存和 CPU 使用。
以下是本章涵盖的主题:
- 函数式编程背后的理论
list理解dict理解set理解lambda功能functools(partial和reduce)itertools(accumulate、chain、dropwhile、starmap等)
函数式编程是源于 lambda 演算的一种范式。在不太深入 lambda 演算(λ演算)的情况下,这大致意味着通过使用数学函数来执行计算,从而避免了可变数据和环境状态的变化。严格函数式语言的思想是,所有函数输出仅依赖于输入,而不依赖于任何外部状态。由于 Python 并不是严格意义上的编程语言,这并不一定成立,但坚持这种范式是一个好主意,因为混合使用这些范式可能会导致无法预见的错误,如第 2 章、Python 语法、常见陷阱和样式指南中所述。
即使在函数式编程之外,这也是一个好主意。保持函数的纯功能性(仅依赖于给定的输入)可以使代码更清晰、更容易理解,并且由于依赖关系更少,因此测试也更好。在math模块中可以找到众所周知的示例。这些功能(sin、cos、pow、sqrt等)具有严格依赖于输入的输入和输出。
Pythonlist理解是将函数或过滤器应用于项列表的一种非常简单的方法。如果使用正确,列表理解可能非常有用,但如果不小心,则非常难以阅读。
让我们深入了解几个例子。list理解的基本前提如下:
>>> squares = [x ** 2 for x in range(10)]
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]我们可以通过过滤器轻松扩展此功能:
>>> uneven_squares = [x ** 2 for x in range(10) if x % 2]
>>> uneven_squares
[1, 9, 25, 49, 81]该语法与常规 Python for 循环非常接近,但是if语句和结果的自动存储使得它在某些情况下非常有用。但是,常规 Python 等价物的长度不会太长:
>>> uneven_squares = []
>>> for x in range(10):
... if x % 2:
... uneven_squares.append(x ** 2)
>>> uneven_squares
[1, 9, 25, 49, 81]但必须小心;由于特殊的列表理解结构,某些类型的操作不像您预期的那样明显。这一次,我们正在寻找大于0.5的随机数:
>>> import random
>>> [random.random() for _ in range(10) if random.random() >= 0.5]
[0.5211948104577864, 0.650010512129705, 0.021427316545174158]看到最后那个号码了吗?它实际上小于0.5。这是因为第一个和最后一个随机调用实际上是单独的调用,并返回不同的结果。
解决此问题的一种方法是创建与筛选器分开的列表:
>>> import random
>>> numbers = [random.random() for _ in range(10)]
>>> [x for x in numbers if x >= 0.5]
[0.715510247827078, 0.8426277505519564, 0.5071133900377911]这很明显是可行的,但也不尽如人意。那么还有什么其他选择呢?有一些,但可读性有点问题,所以我不推荐这些解决方案。不过,至少能见到他们一次是件好事。
下面是列表理解中的list理解:
>>> import random
>>> [x for x in [random.random() for _ in range(10)] if x >= 0.5]这里有一个很快就变成了一种无法理解的理解:
>>> import random
>>> [x for _ in range(10) for x in [random.random()] if x >= 0.5]使用这些选项时需要小心,因为双列表理解实际上像嵌套的for循环一样工作,因此它会快速生成大量结果。要详细说明这方面的情况:
>>> [(x, y) for x in range(3) for y in range(3, 5)]
[(0, 3), (0, 4), (1, 3), (1, 4), (2, 3), (2, 4)]这有效地做到了以下几点:
>>> results = []
>>> for x in range(3):
... for y in range(3, 5):
... results.append((x, y))
...
>>> results
[(0, 3), (0, 4), (1, 3), (1, 4), (2, 3), (2, 4)]在某些情况下,它们可能很有用,但我建议您限制它们的使用,因为它们很快就会变得不可读。为了便于阅读,我强烈建议不要在list理解中使用list理解。理解正在发生的事情仍然很重要,所以让我们再看一个例子。以下list理解交换了列和行计数,因此一个 3 x 4 矩阵变成了 4 x 3:
>>> matrix = [
... [1, 2, 3, 4],
... [5, 6, 7, 8],
... [9, 10, 11, 12],
... ]
>>> reshaped_matrix = [
... [
... [y for x in matrix for y in x][i * len(matrix) + j]
... for j in range(len(matrix))
... ]
... for i in range(len(matrix[0]))
... ]
>>> import pprint
>>> pprint.pprint(reshaped_matrix, width=40)
[[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
[10, 11, 12]]即使有额外的缩进,list理解也不是那么可读。当然,对于四个嵌套循环,这是意料之中的。在很少的情况下,嵌套列表理解可能是合理的,但通常我不推荐使用它们。
dict理解与列表理解非常相似,但结果是dict。除此之外,唯一真正的区别是您需要同时返回键和值,而list理解接受任何类型的值。以下是一个基本示例:
>>> {x: x ** 2 for x in range(10)}
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
>>> {x: x ** 2 for x in range(10) if x % 2}
{1: 1, 3: 9, 9: 81, 5: 25, 7: 49}因为输出是一个字典,所以键需要是可散列的,以便dict理解工作。
有趣的是当然,你可以将这两种元素混合在一起,以获得更难以理解的魔力:
>>> {x ** 2: [y for y in range(x)] for x in range(5)}
{0: [], 1: [0], 4: [0, 1], 16: [0, 1, 2, 3], 9: [0, 1, 2]}显然,你需要小心这些。如果使用正确,它们可能非常有用,但是输出很快就会变得不可读,即使有适当的空白。
正如您可以使用花括号({}创建set一样,您也可以使用set理解创建集合。它们的工作方式类似于list理解,但值是唯一的(并且没有排序顺序):
>>> [x*y for x in range(3) for y in range(3)]
[0, 0, 0, 0, 1, 2, 0, 2, 4]
>>> {x*y for x in range(3) for y in range(3)}
{0, 1, 2, 4}与正则集一样,set理解只支持散列类型。
Python 中的lambda语句只是一个匿名函数。由于语法的原因,它比常规函数稍有的限制,但通过它可以做很多事情。和往常一样,可读性很重要,所以一般来说,保持它尽可能简单是一个好主意。更常见的用例之一是sorted功能的sort键:
>>> class Spam(object):
... def __init__(self, value):
... self.value = value
...
... def __repr__(self):
... return '<%s: %s>' % (self.__class__.__name__, self.value)
...
>>> spams = [Spam(5), Spam(2), Spam(4), Spam(1)]
>>> sorted_spams = sorted(spams, key=lambda spam: spam.value)
>>> spams
[<Spam: 5>, <Spam: 2>, <Spam: 4>, <Spam: 1>]
>>> sorted_spams
[<Spam: 1>, <Spam: 2>, <Spam: 4>, <Spam: 5>]虽然在这种情况下,函数可以单独编写,或者Spam的__cmp__方法可能被覆盖,但在许多情况下,这是一种获得您想要的快速排序函数的简单方法。
这并不是说常规函数冗长,而是通过使用匿名函数,您有一个小优势;您没有用额外的功能污染您的本地范围:
>>> def key_function(spam):
... return spam.value
>>> spams = [Spam(5), Spam(2), Spam(4), Spam(1)]
>>> sorted_spams = sorted(spams, key=lambda spam: spam.value)至于样式,请注意,PEP8规定将 lambda 指定给变量是个坏主意。从逻辑上讲,这是正确的。匿名函数的概念是,它就是那个匿名函数。如果你给它一个标识,你应该把它定义为一个普通函数。如果你想保持简短的话,它实际上不会太长。请注意,以下两个语句都被视为不良样式,仅用于示例目的:
>>> def key(spam): return spam.value
>>> key = lambda spam: spam.value在我看来,lambda函数的唯一有效用例是作为函数参数使用的匿名函数,最好只有当它们足够短,可以放在一行上时。
请注意,这一段很容易跳过。它主要是 lambda 语句的数学值的一个示例。
Y 组合子可能是λ-演算中最著名的示例:
所有这些看起来非常复杂,但这也是因为使用了 lambda 演算符号。您应该将此语法
理解为一个匿名(lambda)函数,该函数将x作为输入并返回
。在 Python 中,除了将
替换为 lambda,将.替换为:之外,它的表达方式几乎与原始 lambda 演算中的表达方式完全相同,因此产生了 lambdax: x^2。
在一些代数中,这可以简化为
,或者一个接受f函数并将其应用于自身的函数。该函数的λ-微积分表示法如下:
以下是 Python 表示法:
Y = lambda f: lambda *args: f(Y(f))(*args)以下是较长的版本:
def Y(f):
def y(*args):
y_function = f(Y(f))
return y_function(*args)
return y您可能还不太清楚这一点,所以让我们看一个实际使用它的示例:
>>> Y = lambda f: lambda *args: f(Y(f))(*args)
>>> def factorial(combinator):
... def _factorial(n):
... if n:
... return n * combinator(n - 1)
... else:
... return 1
... return _factorial
>>> Y(factorial)(5)
120下面是一个简短的版本,其中 Y combinator 的功能实际显示为递归但仍然是匿名函数:
>>> Y = lambda f: lambda *args: f(Y(f))(*args)
>>> Y(lambda c: lambda n: n and n * c(n - 1) or 1)(5)
120请注意,和n * c(n – 1)或1部分是较长版本函数中使用的语句的缩写。或者,可以使用 Python 三元运算符编写:
>>> Y = lambda f: lambda *args: f(Y(f))(*args)
>>> Y(lambda c: lambda n: n * c(n - 1) if n else 1)(5)
120你可能想知道整个练习的意义。你不能写一个较短/较简单的阶乘吗?是的,你可以。Y 组合子的重要性在于它可以应用于任何函数,并且非常接近数学定义。
Y 组合器的最后一个示例将通过几行中的quicksort定义给出:
>>> quicksort = Y(lambda f:
... lambda x: (
... f([item for item in x if item < x[0]])
... + [y for y in x if x[0] == y]
... + f([item for item in x if item > x[0]])
... ) if x else [])
>>> quicksort([1, 3, 5, 4, 1, 3, 2])
[1, 1, 2, 3, 3, 4, 5]虽然 Y combinator 很可能在 Python 中没有太多实际用途,但它确实显示了lambda语句的威力以及 Python 与数学定义的接近程度。本质上,区别只是在符号上,而不是在功能上。
除了list/dict/set理解之外,Python 还有一些(更高级的)函数,在功能性编码时非常方便。functools库是返回可调用对象的函数集合。其中一些函数被用作装饰器(我们将在第 5 章中介绍更多内容,装饰器–通过装饰实现代码重用),但我们将要讨论的那些函数被用作直接函数,以使您的生活更轻松。
partial函数非常方便向经常使用但无法(或不想)重新定义的函数添加一些默认参数。对于面向对象的代码,您通常可以处理类似的情况,但是对于过程代码,您通常必须重复您的参数。让我们以第 3 章、容器和集合中的heapq函数—以正确的方式存储数据为例:
>>> import heapq
>>> heap = []
>>> heapq.heappush(heap, 1)
>>> heapq.heappush(heap, 3)
>>> heapq.heappush(heap, 5)
>>> heapq.heappush(heap, 2)
>>> heapq.heappush(heap, 4)
>>> heapq.nsmallest(3, heap)
[1, 2, 3]几乎所有的heapq函数都需要heap参数,为什么不为它设置一个快捷方式呢?这就是functools.partial的用武之地:
>>> import functools
>>> import heapq
>>> heap = []
>>> push = functools.partial(heapq.heappush, heap)
>>> smallest = functools.partial(heapq.nsmallest, iterable=heap)
>>> push(1)
>>> push(3)
>>> push(5)
>>> push(2)
>>> push(4)
>>> smallest(3)
[1, 2, 3]看起来有点干净,对吗?在本例中,两个版本都相当简短且可读,但它是一个方便的函数。
为什么我们应该使用partial而不是编写lambda参数?嗯,主要是为了方便,但它也有助于解决第 2 章、python 语法、常见陷阱和样式指南中讨论的后期绑定问题。此外,部分函数可以被 pickle,而lambda语句不能。
reduce函数实现了一种称为fold的数学技术。它基本上对第一个和第二个元素应用一个函数,使用结果与第三个元素一起应用,并持续到列表用尽为止。
reduce函数受多种语言支持,但大多数情况下使用不同的名称,如curry、fold、accumulate或aggregate。Python 实际上已经支持reduce很长一段时间了,但是自从 Python 3 以来,它已经从全局范围转移到functools库。使用reduce语句可以很好地简化一些代码;然而,它是否可读仍有争议。
最常用的reduce示例之一是计算阶乘,这确实很简单:
>>> import operator
>>> import functools
>>> functools.reduce(operator.mul, range(1, 6))
120前面的代码使用了operator.mul而不是lambda a, b: a * b。虽然它们产生相同的结果,但前者可以更快。
在内部,reduce功能将执行以下操作:
>>> import operator
>>> f = operator.mul
>>> f(f(f(f(1, 2), 3), 4), 5)
120为了进一步澄清这一点,让我们这样看:
>>> iterable = range(1, 6)
>>> import operator
# The initial values:
>>> a, b, *iterable = iterable
>>> a, b, iterable
(1, 2, [3, 4, 5])
# First run
>>> a = operator.mul(a, b)
>>> b, *iterable = iterable
>>> a, b, iterable
(2, 3, [4, 5])
# Second run
>>> a = operator.mul(a, b)
>>> b, *iterable = iterable
>>> a, b, iterable
(6, 4, [5])
# Third run
>>> a = operator.mul(a, b)
>>> b, *iterable = iterable
>>> a, b, iterable
(24, 5, [])
# Fourth and last run
>>> a = operator.mul (a, b)
>>> a
120或者使用deque集合的使用一个简单的while循环:
>>> import operator
>>> import collections
>>> iterable = collections.deque(range(1, 6))
>>> value = iterable.popleft()
>>> while iterable:
... value = operator.mul(value, iterable.popleft())
>>> value
120树是案例中reduce功能真正闪耀的地方。还记得使用第 3 章中的defaultdict、容器和集合的单行树定义吗?以正确的方式存储数据?访问该对象内部的密钥的好方法是什么?给定树项目的路径,我们可以使用reduce轻松访问其中的项目:
>>> import json
>>> import functools
>>> import collections
>>> def tree():
... return collections.defaultdict(tree)
# Build the tree:
>>> taxonomy = tree()
>>> reptilia = taxonomy['Chordata']['Vertebrata']['Reptilia']
>>> reptilia['Squamata']['Serpentes']['Pythonidae'] = [
... 'Liasis', 'Morelia', 'Python']
# The actual contents of the tree
>>> print(json.dumps(taxonomy, indent=4))
{
"Chordata": {
"Vertebrata": {
"Reptilia": {
"Squamata": {
"Serpentes": {
"Pythonidae": [
"Liasis",
"Morelia",
"Python"
]
}
}
}
}
}
}
# The path we wish to get
>>> path = 'Chordata.Vertebrata.Reptilia.Squamata.Serpentes'
# Split the path for easier access
>>> path = path.split('.')
# Now fetch the path using reduce to recursively fetch the items
>>> family = functools.reduce(lambda a, b: a[b], path, taxonomy)
>>> family.items()
dict_items([('Pythonidae', ['Liasis', 'Morelia', 'Python'])])
# The path we wish to get
>>> path = 'Chordata.Vertebrata.Reptilia.Squamata'.split('.')
>>> suborder = functools.reduce(lambda a, b: a[b], path, taxonomy)
>>> suborder.keys()
dict_keys(['Serpentes'])最后,一些人可能会想知道为什么 Python 只有fold_left而没有fold_right。在我看来,你并不真的需要两者,因为你可以很容易地逆转操作。
常规的reduce-fold left操作:
fold_left = functools.reduce(
lambda x, y: function(x, y),
iterable,
initializer,
)反转fold right操作:
fold_right = functools.reduce(
lambda x, y: function(y, x),
reversed(iterable),
initializer,
)虽然这个函数在纯函数式语言中非常有用,因为这些操作最初经常使用,但有计划通过引入 Python3 从 Python 中删除reduce函数。幸运的是,该计划被修改,并没有被删除,而是从reduce移动到functools.reduce。对于reduce可能没有很多有用的案例,但是有一些很酷的用例。特别是使用reduce遍历递归数据结构要容易得多,因为它会涉及更复杂的循环或递归函数。
itertools库包含受函数式语言启发的可移植函数。所有这些都是可移植的,并且以这样一种方式构造,即只需要最小的内存量就可以处理最大的数据集。虽然您可以使用一个简单的函数轻松编写这些函数中的大部分,但我仍然建议您使用itertools库中提供的函数。这些都是快速、高效的内存,也许更重要的是经过测试。
尽管段落标题大写,但功能本身并非如此。注意不要意外地键入Accumulate而不是accumulate。
accumulate函数与reduce函数非常相似,这就是为什么一些语言实际上使用accumulate而不是reduce作为折叠运算符的原因。
两者之间的主要区别在于accumulate函数返回即时结果。这在汇总公司的销售结果时非常有用,例如:
>>> import operator
>>> import itertools
# Sales per month
>>> months = [10, 8, 5, 7, 12, 10, 5, 8, 15, 3, 4, 2]
>>> list(itertools.accumulate(months, operator.add))
[10, 18, 23, 30, 42, 52, 57, 65, 80, 83, 87, 89]需要注意的是,operator.add函数在这种情况下实际上是可选的,因为累积的默认行为是对结果求和。在其他一些语言和库中,此函数称为cumsum(累积和)。
chain函数是一个简单但有用的函数,组合了多个迭代器的结果。非常简单,但如果您有多个列表、迭代器等,也非常有用—只需将它们与一个简单的链组合即可:
>>> import itertools
>>> a = range(3)
>>> b = range(5)
>>> list(itertools.chain(a, b))
[0, 1, 2, 0, 1, 2, 3, 4]需要注意的是有一个chain的小变体,它接受一个包含 iterable 的 iterable,即chain.from_iterable。它们的工作原理几乎相同,只是需要传递一个 iterable 项,而不是传递一个参数列表。您最初的回答可能是,只需将(*args元组解包即可实现这一点,正如我们将在第 6 章中看到的,生成器和协程–无限,一次一步。然而,情况并非总是如此。现在,只要记住,如果你有一个包含 iterables 的 iterable,最简单的方法就是使用itertools.chain.from_iterable。
combinations迭代器产生的结果与数学定义中的完全相同。给定项目列表中具有特定长度的所有组合:
>>> import itertools
>>> list(itertools.combinations(range(3), 2))
[(0, 1), (0, 2), (1, 2)]combinations函数给出给定长度的给定项的所有可能组合。可能的组合数由二项式系数给出,这是许多计算器上的nCr按钮。通常表示如下:
在这种情况下,我们有n=2和k=4。
以下是元素重复的变体:
>>> import itertools
>>> list(itertools.combinations_with_replacement(range(3), 2))
[(0, 0), (0, 1), (0, 2), (1, 1), (1, 2), (2, 2)]combinations_with_repetitions功能与常规combinations功能非常相似,只是项目本身也可以组合。为了计算结果的数量,可以使用前面描述的二项式系数和参数n=n+k-1和k=k。
让我们看一下组合的一个小组合和生成powerset的链:
>>> import itertools
>>> def powerset(iterable):
... return itertools.chain.from_iterable(
... itertools.combinations(iterable, i)
... for i in range(len(iterable) + 1))
>>> list(powerset(range(3)))
[(), (0,), (1,), (2,), (0, 1), (0, 2), (1, 2), (0, 1, 2)]powerset本质上是0到n所有组合的组合结果,也就是说它还包括零项元素(空集,或())、有1项的元素,以及一直到n的元素。powerset中的项目数可使用幂运算符2**n轻松计算。
permutations功能与功能combinations非常相似。唯一真正的区别在于(a, b)被认为与(b, a)不同。换句话说,顺序很重要:
>>> import itertools
>>> list(itertools.permutations(range(3), 2))
[(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]compress功能是您通常不需要的功能之一,但当您确实需要它时,它会非常有用。它对 iterable 应用布尔过滤器,使其仅返回您实际需要的值。这里需要注意的最重要的一点是,它都是延迟执行的,compress将在数据或选择器集合用尽时停止。因此,即使具有无限范围,它也能顺利工作:
>>> import itertools
>>> list(itertools.compress(range(1000), [0, 1, 1, 1, 0, 1]))
[1, 2, 3, 5]dropwhile函数将删除所有结果,直到给定的谓词计算为 true。如果您正在等待设备最终返回预期结果,这可能会很有用。这在这里有点难以演示,因此我将仅展示一个基本用法等待大于3的数字的示例:
>>> import itertools
>>> list(itertools.dropwhile(lambda x: x <= 3, [1, 3, 5, 4, 2]))
[5, 4, 2]正如您可能所料,takewhile函数与此相反。它将简单地返回所有行,直到谓词变为 false:
>>> import itertools
>>> list(itertools.takewhile(lambda x: x <= 3, [1, 3, 5, 4, 2]))
[1, 3]简单地将两者相加,将再次得到原始结果。
count功能与range功能非常相似,但有两个显著差异。
首先,这个范围是无限的,所以不要尝试list(itertools.count())。您肯定会立即耗尽内存,甚至可能冻结您的系统。
第二个区别是,与range函数不同,这里实际上可以使用浮点数,因此不需要整数值。
由于列出整个范围将杀死我们的 Python 解释器,我们将简单地使用zip来限制结果并比较常规range函数的结果。在后面的一段中,我们将看到使用itertools.islice的更方便的选项。count功能有两个可选参数:一个start参数,默认为0,一个step参数,默认为1:
>>> import itertools
# Except for being infinite, the standard version returns the same
# results as the range function does.
>>> for a, b in zip(range(3), itertools.count()):
... a, b
(0, 0)
(1, 1)
(2, 2)
# With a different starting point the results are still the same
>>> for a, b in zip(range(5, 8), itertools.count(5)):
... a, b
(5, 5)
(6, 6)
(7, 7)
# And a different step works the same as well
>>> for a, b in zip(range(5, 10, 2), itertools.count(5, 2)):
... a, b
(5, 5)
(7, 7)
(9, 9)
# Unless you try to use floating point numbers
>>> range(5, 10, 0.5)
Traceback (most recent call last):
...
TypeError: 'float' object cannot be interpreted as an integer
# Which does work for count
>>> for a, b in zip(range(5, 10), itertools.count(5, 0.5)):
... a, b
(5, 5)
(6, 5.5)
(7, 6.0)
(8, 6.5)
(9, 7.0)itertools.islice函数在与itertools.count的结合中也非常有用,我们将在后面的一段中看到。
groupby函数是分组结果的一个非常方便的函数。用法和用例可能很清楚,但在使用此功能时,需要记住一些重要的事情:
- 输入需要按
group参数排序。否则,它将作为一个单独的组添加。 - 结果只能使用一次。因此,在处理组后,它将不再可用。
以下是正确使用groupby的示例:
>>> import itertools
>>> items = [('a', 1), ('a', 2), ('b', 2), ('b', 0), ('c', 3)]
>>> for group, items in itertools.groupby(items, lambda x: x[0]):
... print('%s: %s' % (group, [v for k, v in items]))
a: [1, 2]
b: [2, 0]
c: [3]在某些情况下,您可能会得到意想不到的结果:
>>> import itertools
>>> items = [('a', 1), ('b', 0), ('b', 2), ('a', 2), ('c', 3)]
>>> groups = dict()
>>> for group, items in itertools.groupby(items, lambda x: x[0]):
... groups[group] = items
... print('%s: %s' % (group, [v for k, v in items]))
a: [1]
b: [0, 2]
a: [2]
c: [3]
>>> for group, items in sorted(groups.items()):
... print('%s: %s' % (group, [v for k, v in items]))
a: []
b: []
c: []现在我们看到两组包含a。因此,在尝试分组之前,请确保按分组参数进行排序。此外,第二次在同一组中行走不会产生任何结果。使用groups[group] = list(items)可以很容易地解决这个问题,但是如果您没有意识到这一点,它可能会产生很多意想不到的错误。
在使用itertools函数时,您可能会注意到您无法分割这些对象。这是因为它们是生成器,我们将在第 6 章生成器和协同程序中讨论这一主题—无限,一次一步。幸运的是,itertools库还具有对这些对象进行切片的功能—islice。
以前面的itertools.counter为例:
>>> import itertools
>>> list(itertools.islice(itertools.count(), 2, 7))
[2, 3, 4, 5, 6]因此,代替常规的slice:
itertools.count()[:10]我们在函数中输入slice参数:
itertools.islice(itertools.count(), 10)您应该注意的是,实际上不仅仅是无法分割对象。这不仅是因为切片不起作用,而且也不可能得到长度,至少在不单独计算所有项并使用无限迭代器的情况下是不可能的,即使这是不可能的。从生成器中实际获得的唯一理解是,您可以一次获取一个项目。你甚至不会事先知道你是否在生成器的末端。
出于某种原因,函数式编程是一种让很多人害怕的模式,但实际上它不应该。函数式编程和过程式编程(在 Python 中)之间最重要的区别是思维方式。一切都是使用简单的(通常是数学等价物的翻译)函数执行的,没有任何变量存储。简单地说,函数程序由许多函数组成,这些函数具有简单的输入和输出,而不使用(甚至没有)任何外部范围或上下文来访问。Python 不是一种纯粹的函数式语言,因此很容易作弊并在本地范围之外工作,但不建议这样做。
本章介绍了 Python 中函数式编程的基础知识及其背后的一些数学知识。除此之外,还介绍了许多有用的库,这些库可以通过使用函数式编程以非常方便的方式使用。
最重要的输出应该是以下内容:
- Lambda 语句本身并不坏,但最好让它们只使用局部范围内的变量,并且它们不应超过一行。
- 函数式编程功能非常强大,但很快就会变得不可读。必须小心。
list/dict/set理解非常有用,但它们通常不应该嵌套,为了可读性,它们也应该保持简短。
归根结底,这是一个偏好的问题。为了可读性,我建议在没有明显好处的情况下限制函数范式的使用。话虽如此,如果执行得当,它可能是一件美丽的事情。
接下来是 decorators 方法,用于将函数和类包装到其他函数和/或类中,以修改它们的行为并扩展它们的功能。


