itertools 和 functools : 两个 Python 独行侠

itertools 和 functools : 两个 Python 独行侠

Python 的一件很酷的事情是它对函数式编程的支持,即处理步骤是通过函数完成的。

幸运的是,Python 附带了完整的包,从而增强了它的多范式制胜牌,其中一些(如果不是全部)最初是用 C 实现的。您实际上可以阅读 CPython 存储库中的 Lib / Modules 文件夹中的实现Github Python 项目。[0][1]

在本文中,我将讨论 Python 为我们提供的用于执行各种基于函数的计算的两个函数式编程模块:itertools 和 functools。

我不会复制您在官方文档中找到的相同模式。我只是想提一些您在 Internet 上不常遇到的功能,但不知何故设法在我最需要的时候拯救了我。

怀着对他们的感激之情,让我们开始吧。

1. Itertools

简单地说,itertools 允许有效的循环。

该模块标准化了一组核心的快速、高效的内存工具,这些工具本身或组合使用。它们一起形成了一个“迭代器代数”,使得在纯 Python 中简洁有效地构建专用工具成为可能。

1.1. cycle

我记得我在 Haskell 上看到过类似的东西。显然需要用 Python 实现一个类似的构建块。
每当您想要操作会因异常或指定条件而停止的无限循环循环时,这将是您的选择。
Simple and effective :

>> from itertools import cycle
>> for el in cycle(range(10)): print(el, end="")
>> 0123456789012345 ...
>> ..

最终,如果你运行你的循环而不中断它,你的内存缓冲区会发生一些事情。我不知道,我没试过。

1.2. accumulate

如果您想用尽可能少的代码行设计某种累加器:

>> from itertools import accumulate
>> list(accumulate(range(10)))
>> [0, 1, 3, 6, 10, 15, 21, 28, 36, 45]
>> ...

基本上,累加器中的每个元素都将等于包括其自身在内的所有先前元素的总和。

就我个人而言,使用纯内置函数实现至少需要 6 行代码。但是,由于几乎每个数据帧聚合都需要适当的累加器,因此您可能会喜欢这种方式。

1.3. groupby

实际上,在我的一些项目中以某种方式使用它很有趣。这是一个例子:

>>> from itertools import groupby
>>> data = 'AAAABBBCCDAABBB'
>>> for k, v in groupby(data):
... print(k, len(list(v)))
...
A 4
B 3
C 2
D 1
A 2
B 3

它的操作大致类似于具有更多功能的 numpy.unique 函数。
每次键函数的值发生变化时,都会创建一个中断或一个新组。这与 SQL 的 GROUP BY 形成对比,后者将相似的数据分组而不考虑顺序。因此,在将数据传递给 itertools.groupby 之前首先对数据进行排序很重要。

1.4. pairwise

当您循环遍历一个可迭代对象时,您对单独处理每个元素几乎不感兴趣。 Python 3.10 为更有趣的用例提供了一项新功能。
这是一个 pairwise 的例子:

>>> from itertools import pairwise
>>> list(pairwise('ABCDEFG'))
... ['AB','BC','CD','DE','EF','FG']

执行迭代的额外程度始终是一种特权和奢侈。通过两个元素的连续块迭代您的列表允许更多的增量或减量处理。

1.5. starmap

如果您曾经使用 map 对可迭代对象进行操作,则可以将 starmap 视为扩展为小的可迭代对象的映射运算符。让我们研究一个小例子:

>>> from itertools import starmap
>>> v = starmap(sum, [[range(5)], [range(5, 10)], [range(10, 15)]])
>>> list(v)
[10, 35, 60]
>>> ...

在此示例中, sum 适用于每组参数并生成具有相同编号的结果列表。将每个可迭代对象视为参数而不是普通输入很重要,这就是为什么将每个可迭代对象放在括号之间的原因,否则会引发错误。

任何时候你想要一个函数根据不同的数据组来接受参数,starmap 可能非常有效并且很有表现力。

1.6. zip_longest

zip 也用于 Haskell。它对于并行迭代非常有用。
但是,它期望每个可迭代参数的长度相同。
如果您尝试压缩两个不同长度的可迭代对象,则 zip 将切断较长的迭代部分的超出部分。
zip_longest 将较短的可迭代填充到与较长的相同的长度:

>>> from itertools import zip_longest
>>> list(zip_longest('ABCD', 'xy', fillvalue='-'))
... [('A', 'x'), ('B', 'y'), ('C', '-'), ('D', '-')]

当您最初遇到长度不匹配时,这很实用。然后,您可以轻松地循环遍历您的两个或多个迭代,并确保这将在运行时保持不变。

1.7. product

假设您有一个等待更新的矩阵,您必须先遍历行然后遍历列(反之亦然)。
You would do :

for i in ncolumns: 
for j in nrows :
...

有一种更酷的方法可以将两个循环与 itertools 中称为 product 的功能复合:

>>> from itertools import product
>>> for i,j in product([0,1], [10,11]):
... print(i,j)
...
0 10
0 11
1 10
1 11

product 沿迭代执行笛卡尔积,在矩阵的情况下,您可以像这样替换嵌套循环:

>>> for i, j in product(ncolumns, nrows):
...

几乎没有缺点,这消除了在两个循环之间初始化中间或临时变量的任何可能性。归根结底,这取决于您想要实现的目标。

这可能是我能想到的每一个让我的一天更轻松、更酷的 itertools。我不会对此进行详细说明,因为该库永远不会出人意料,尤其是 itertools 配方:它们是作者精心制作的,目的是充分利用之前查看过的函数,这些函数充当矢量化和高度优化的构建块。在配方中,您实际上可以看到这些功能在多大程度上有可能创建与底层工具集具有相同高性能的更强大的工具。里面发生了一些很有趣的事情,你最好马上检查一下……[0]

2.functools

用最简单的术语来说,functools 是一个用于高阶函数的模块:它们使用函数作为参数和输出。装饰器、属性、缓存是高阶函数的示例,它们作为注释放置在函数定义之上。
让我们看一些其他的例子。

2.1. partial

因此,您需要实现一个在您的程序中将被视为一等公民的函数。问题是,它需要太多参数,因此在调用时会导致异常。
partial 是 functools 的一项功能,它允许通过将单个值分配给至少一个参数来冻结部分参数。

让我们考虑下一组数组和权重:

>>> import numpy as np
>>> x1 = np.array([1,2,1])
>>> w1 = np.array([.2, .3, .2])
>>> n1 = 3
>>>
>>> x2 = np.array([2,1,2])
>>> w2 = np.array([.1, .1, .05])
>>> n2 = 3
>>>

然后让我们考虑这个计算这些数组的加权平均值的函数:

>>> def weighted_means(x1, w1, n1, x2, w2, n2): 
... return np.dot(x1,w1)/n1 , np.dot(x2,w2)/n2

我们将功能付诸实践:

>>> weighted_means(x1, w1, n1, x2, w2, n2)
... (0.3333333333333333, 0.13333333333333333)

假设您想通过冻结 x2、w2 和 n2 来减少变量参数的数量:

>>> from functools import partial
>>> reduced_weighted_means = partial(weighted_means, x2 = x2 , w2 = w2 , n2 = n2)

然后,您可以使用减少了参数数量的新缩减函数:

>>> reduced_weighted_means(x1, w1, n1)
... (0.3333333333333333, 0.13333333333333333)

请注意,它与之前的结果相同,表明该函数就像静态输入固定参数一样工作。当在与参数多样性相关的特定约束下使用该函数时,这似乎是一个完美的解决方法。

2.2. partialmethod

partialmethod 是 partial 的外推,除了它被用作类方法。让我们用一个例子来说明这一点(我承认这是一个非常愚蠢的例子):

from functools import partialmethod

class ModelName:
    
    def __init__(self, algorithm):
        self._algorithm = algorithm
        
    @property
    def algorithm(self):
        return self._algorithm
    
    def set_algorithm(self, algorithm):
        self._algorithm = algorithm
        
    set_cls = partialmethod(set_algorithm, 'classification')
    set_regr = partialmethod(set_algorithm, 'regression')

让我们测试一下:

>>> mn = ModelName('clustering')
>>> mn.set_cls()
>>> print(mn.algorithm)
>>> 'classification'
>>>
>>> mn.set_regr()
>>> print(mn.algorithm)
>>> 'regression'

该类用于存储字符串并将 setter 角色委托给两个部分方法。 set_cls 和 set_regr 就像它们是正确定义的方法一样工作,并将算法设置为不同的值。一个小提示:定义算法属性后不应该有 algorithm.setter。

2.3. singledispatch

假设您定义了一个执行一些指令的简单函数,然后决定使其更通用:

from functools import singledispatch

@singledispatch
def zen_of_python(arg):
    print('Beautiful is better than ugly.')

然后,通过使用 zen_of_python 的 register() 属性,使用其他实现重载该函数,该属性现在用作装饰器。

@zen_of_python.register
def _(arg : int):
    print('There should be one-- and preferably only one obvious way to do it.')

您可以根据参数的类型确定不同的实现:

@zen_of_python.register
def _(arg:float):
    print('Readability counts.')
    
@zen_of_python.register
def _(arg:list):
    print('Namespaces are one honking great idea -- let\'s do more of those!')

注意泛型函数在输入不同类型的参数时的行为:

>>> zen_of_python('hello')
... Beautiful is better than ugly.
>>> zen_of_python(1)
... There should be one-- and preferably only one --obvious way to do it.
>>> zen_of_python(1.0)
... Readability counts.
>>> zen_of_python([1, 2, "a"])
... Namespaces are one honking great idea -- let's do more of those!

结果与我们的实现一致。但是,由于泛型函数还不知道 dict 类型的适当实现,它将跳转到默认实现:

>>> zen_of_python(dict())
... Beautiful is better than ugly.

这东西很酷。我知道。

2.4. singledispatchmethod

我们的最后一位客人:singledispatchmethod,可在类定义中使用。
An example maybe?
假设您有一个根据您输入的参数类型输出字符串的类:

from functools import singledispatchmethod

class Generic:
    @singledispatchmethod
    def generic_method(self, arg):
        raise NotImplementedError("Never heard of this type ..")
        
    @generic_method.register
    def _(self, arg: int):
        return 'First case'
      
    @generic_method.register
    def _(self, arg: float):
        return 'Second case'
    
    @generic_method.register
    def _(self, arg: list):
        return 'Third case'
      

用一个小演示关闭:

>>> gen = Generic()>>> gen.generic_method(1)
...'First case'
>>> gen.generic_method(1.0)
>>> 'Second case'
>>> gen.generic_method([1,2,3])
>>> 'Third case'

让我们尝试一个 dict 类型:

>>> gen.generic_method(dict(a=1, b=2))
Traceback (most recent call last):
...
NotImplementedError: Never heard of this type ..

如果您放置的类型不是在 Generic 类上声明的类型,您将遇到错误。如果你想让它消失,你应该添加一个小而温和的函数来处理它。

References

官方文档——itertools 和 functools[0][1]

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
乘风的头像乘风管理团队
上一篇 2022年4月25日
下一篇 2022年4月25日

相关推荐