Skip to content

Latest commit

 

History

History
815 lines (555 loc) · 29.7 KB

File metadata and controls

815 lines (555 loc) · 29.7 KB

四、函数式编程——可读性与简洁性

Python 是少数(或至少是最早)包含函数特性的非函数语言之一。虽然 Guido van Rossum 几次试图删除其中一些,但它们已经在 Python 社区中根深蒂固,列表理解(dictset理解很快就会出现)在各种代码中广泛使用。关于代码最重要的事情不应该是你的reduce语句有多酷,或者你如何用一个不可理解的列表将整个函数放在一行中。可读性计数(再次,PEP20

本章将向您展示 Python 函数式编程提供的一些很酷的技巧,并解释 Python 实现的一些限制。虽然我们将尽量避开 lambda 演算(λ演算),但将简要讨论Y 组合子

最后几段将列出(并解释)functoolsitertools库的用法。如果您熟悉这些库,可以跳过它们,但请注意,其中一些库将在后面关于装饰器的章节中大量使用(第 5 章装饰器–通过装饰实现代码重用)、生成器(第 6 章生成器和协同程序–无限,一次一步和性能(第 12 章性能–跟踪并减少内存和 CPU 使用

以下是本章涵盖的主题:

  • 函数式编程背后的理论
  • list理解
  • dict理解
  • set理解
  • lambda功能
  • functoolspartialreduce
  • itertoolsaccumulatechaindropwhilestarmap等)

函数式编程

函数式编程是源于 lambda 演算的一种范式。在不太深入 lambda 演算(λ演算)的情况下,这大致意味着通过使用数学函数来执行计算,从而避免了可变数据和环境状态的变化。严格函数式语言的思想是,所有函数输出仅依赖于输入,而不依赖于任何外部状态。由于 Python 并不是严格意义上的编程语言,这并不一定成立,但坚持这种范式是一个好主意,因为混合使用这些范式可能会导致无法预见的错误,如第 2 章Python 语法、常见陷阱和样式指南中所述。

即使在函数式编程之外,这也是一个好主意。保持函数的纯功能性(仅依赖于给定的输入)可以使代码更清晰、更容易理解,并且由于依赖关系更少,因此测试也更好。在math模块中可以找到众所周知的示例。这些功能(sincospowsqrt等)具有严格依赖于输入的输入和输出。

列表理解

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理解只支持散列类型。

lambda 函数

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函数的唯一有效用例是作为函数参数使用的匿名函数,最好只有当它们足够短,可以放在一行上时。

Y 组合器

请注意,这一段很容易跳过。它主要是 lambda 语句的数学值的一个示例。

Y 组合子可能是λ-演算中最著名的示例:

The Y combinator

所有这些看起来非常复杂,但这也是因为使用了 lambda 演算符号。您应该将此语法The Y combinator理解为一个匿名(lambda)函数,该函数将x作为输入并返回The Y combinator。在 Python 中,除了将The Y combinator替换为 lambda,将.替换为:之外,它的表达方式几乎与原始 lambda 演算中的表达方式完全相同,因此产生了 lambdax: x^2

在一些代数中,这可以简化为The Y combinator,或者一个接受f函数并将其应用于自身的函数。该函数的λ-微积分表示法如下:

The Y combinator

以下是 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函数受多种语言支持,但大多数情况下使用不同的名称,如curryfoldaccumulateaggregate。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库包含受函数式语言启发的可移植函数。所有这些都是可移植的,并且以这样一种方式构造,即只需要最小的内存量就可以处理最大的数据集。虽然您可以使用一个简单的函数轻松编写这些函数中的大部分,但我仍然建议您使用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

组合——Python 中的组合数学

combinations迭代器产生的结果与数学定义中的完全相同。给定项目列表中具有特定长度的所有组合:

>>> import itertools
>>> list(itertools.combinations(range(3), 2))
[(0, 1), (0, 2), (1, 2)]

combinations函数给出给定长度的给定项的所有可能组合。可能的组合数由二项式系数给出,这是许多计算器上的nCr按钮。通常表示如下:

combinations – combinatorics in Python

在这种情况下,我们有n=2k=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-1k=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本质上是0n所有组合的组合结果,也就是说它还包括零项元素(空集,或())、有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/takewhile–使用功能选择项目

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–将您的已排序项目分组

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)可以很容易地解决这个问题,但是如果您没有意识到这一点,它可能会产生很多意想不到的错误。

islice–切割任何可折叠的

在使用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 方法,用于将函数和类包装到其他函数和/或类中,以修改它们的行为并扩展它们的功能。