Functools - Python 中高阶函数的威力

源节点: 1865357

这篇文章是作为 数据科学博客马拉松

介绍

Python 标准库有许多很棒的模块来帮助保持你的代码更清晰和更简单,而 functools 是 绝对是其中之一

高速缓存

让我们从 functools 模块的一些最简单但功能强大的功能开始。 让我们从缓存函数(以及装饰器)开始—— lru_cache,cachecached_property. 他们中的第一个—— lru_cache 提供函数执行的最后结果的缓存,或者换句话说,记住它们的工作结果:

from functools import lru_cache
import requests @lru_cache(maxsize=32)
def get_with_cache(url): try: r = requests.get(url) return r.text except: return "Not Found" for url in ["https://google.com/", "https://reddit.com/", "https://google.com/", "https://google.com/"]: get_with_cache(url) print(get_with_cache.cache_info())
# CacheInfo(hits=2, misses=4, maxsize=32, currsize=4)
print(get_with_cache.cache_parameters())
# {'maxsize': 32, 'typed': False}

在此示例中,我们使用装饰器发出 GET 请求并缓存其结果(最多 32 个结果) @lru_cache. 要查看缓存是否真的有效,您可以使用一个方法检查函数缓存信息 cache_info显示缓存命中数和命中数。 装饰器还提供方法 clear_cachecache_parameters分别用于取消缓存的结果和测试参数。

如果您需要更细粒度的缓存,您可以包含一个可选参数 typed=true,它允许您分别缓存不同类型的参数。

functools 中另一个用于缓存的装饰器是一个简单调用的函数 cache. 这是一个简单的包装器 lru_cache省略参数 max_size,减少它,并且不删除旧值。

另一个可用于缓存的装饰器是 cached_property. 顾名思义,它用于缓存类属性的结果。 如果您的属性计算成本很高,但保持不变,则此机制非常有用。

from functools import cached_property class Page: @cached_property def render(self, value): # Do something with supplied value... # Long computation that renders HTML page... return html

这个简单的例子显示。 例如,可以使用缓存属性来缓存需要一遍又一遍地向用户显示的呈现的 HTML 页面。 对于某些数据库查询或冗长的数学计算,也可以这样做。

另一种美 cached_property是它只在查找时运行,所以它允许我们更改属性的值。 更改属性后,之前缓存的值不会改变,而是会计算并缓存新值。 您也可以清除缓存,您需要做的就是删除该属性。

我想以对上述所有装饰器的警告结束本节 - 如果您的函数有一些副作用,或者每次调用时都会创建可变对象,请不要使用它们,因为这些显然不是您想要缓存的函数.

比较和订购

您可能已经知道可以在 Python 中实现比较运算符,例如 <, >=or ==, lt, gtor eq. 然而,意识到每个 eq, lt, le, gtor ge. 好在functools里面有个装饰器 @total_ordering这可以帮助我们解决这个问题,因为我们需要实施的是 eq剩下的方法之一,其余的装饰器会自动生成。

from functools import total_ordering @total_ordering
class Number: def __init__(self, value): self.value = value def __lt__(self, other): return self.value Number(3))
# True
print(Number(1) = Number(15))
# True
print(Number(10) <= Number(2))
# False

这样,我们可以实现所有扩展的比较操作,尽管只有 eq 和手动 lt。 最明显的好处是方便,即您不​​必编写所有这些额外的魔术方法,但减少代码量及其更好的可读性可能更为重要。

超载

我们可能都被告知 Python 中没有重载,但实际上有一种简单的方法可以使用 functools 中的两个函数来实现它,即单分派和/或单分派方法。 这些函数帮助我们实现了所谓的多分派算法,该算法允许动态类型的编程语言(例如 Python)在运行时区分类型。

局部的

我们都使用各种外部库或框架,其中许多提供了需要我们传递回调的函数和接口,例如异步操作或侦听事件。 这并不是什么新鲜事,但是如果我们还需要在回调中传递一些参数呢? 这是它派上用场的地方 functools.partial. 可以使用 partial通过创建具有简化函数签名的新对象来冻结函数的部分(或全部)参数。 使困惑? 让我们看一些实际的例子:

def output_result(result, log=None): if log is not None: log.debug(f"Result is: {result}") def concat(a, b): return a + b import logging
from multiprocessing import Pool
from functools import partial logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger("default") p = Pool()
p.apply_async(concat, ("Hello ", "World"), callback=partial(output_result, log=logger))
p.close()
p.join()

上面的代码显示了如何使用它 partial传递一个函数( output_result) 以及一个参数 ( log=logger) 作为回调。 在这种情况下,我们将使用 multiprocessing.apply_async,它异步计算函数的结果( concat) 并返回回调的结果。 然而, apply_async它总是将结果作为第一个参数传递,如果我们想包含任何额外的参数,就像 log=logger 的情况,我们需要使用 partial。

我们已经考虑了一个相当高级的用例,一个更简单的例子是通常创建一个写入 stderr 而不是 stdout 的函数:

import sys
from functools import partial print_stderr = partial(print, file=sys.stderr)
print_stderr("This goes to standard error output")

使用这个简单的技巧,我们创建了一个新的可调用函数,该函数将始终通过 file=sys.stderr作为输出的命名参数,这允许我们简化我们的代码,而不必每次都指定命名参数的值。

还有最后一个很好的例子。 我们可以用 partial结合一个鲜为人知的功能 iter通过传入一个可调用对象来创建一个迭代器和 sentinelin iter,可以这样应用:

from functools import partial RECORD_SIZE = 64 # Read binary file...
with open("file.data", "rb") as file: records = iter(partial(file.read, RECORD_SIZE), b'') for r in records: # Do something with the record...

通常,在读取文件时,我们希望遍历行,但在二进制数据的情况下,我们可能需要遍历固定大小的记录。 您可以通过使用创建一个可调用对象来做到这一点 partial读取指定的数据块并将其传入 iter创建一个迭代器。 这个迭代器然后调用 read 函数直到它到达文件的末尾,总是只取指定的块大小( RECORD_SIZE)。 最后,当到达文件末尾时,值 sentinel(乙”) 返回并停止迭代。

装饰

我们在前面的章节中已经讨论了一些装饰器,但不是装饰器来创建更多的装饰器。 一个这样的装饰器是 functools.wraps. 要理解为什么需要它,让我们看一个例子:

def decorator(func): def actual_func(*args, **kwargs): """Inner function within decorator, which does the actual work""" print(f"Before Calling {func.__name__}") func(*args, **kwargs) print(f"After Calling {func.__name__}") return actual_func @decorator
def greet(name): """Says hello to somebody""" print(f"Hello, {name}!") greet("Martin")
# Before Calling greet
# Hello, Martin!
# After Calling greet

这个例子展示了如何实现一个简单的装饰器。 我们包装了一个执行特定任务的函数( actual_func) 与外部装饰器,它本身成为装饰器,然后可以应用于其他功能,例如,就像 greet. 当您调用该函数时, greet 你会看到它打印出来自 actual_func并且靠它自己。 看起来还行吧? 但是如果我们这样做会发生什么:

print(greet.__name__)
# actual_func
print(greet.__doc__)
# Inner function within decorator, which does the actual work

当我们调用装饰函数的名称和文档时,我们意识到它们已经被装饰函数中的值替换了。 这很糟糕,因为当我们使用一些装饰器时,我们无法重写所有的函数名称和文档。 如何解决这个问题? 当然,用functools.wraps:

from functools import wraps def decorator(func): @wraps(func) def actual_func(*args, **kwargs): """Inner function within decorator, which does the actual work""" print(f"Before Calling {func.__name__}") func(*args, **kwargs) print(f"After Calling {func.__name__}") return actual_func @decorator
def greet(name): """Says hello to somebody""" print(f"Hello, {name}!") print(greet.__name__)
# greet
print(greet.__doc__)
# Says hello to somebody

函数的唯一目的 wraps就是复制名称、文档、参数列表等,防止覆盖。 考虑到 wraps它也是一个装饰器,你可以简单地将它添加到我们的actual_func中,问题就解决了!

减少

最后但并非最不重要的模块 functools 是这个 reduce。 也许在其他语言中,你可能知道它是 fold(哈斯克尔)。 该函数接受一个可迭代对象并将其所有值折叠(添加)为一个。 这有很多应用,例如:

from functools import reduce
import operator def product(iterable): return reduce(operator.mul, iterable, 1) def factorial(n): return reduce(operator.mul, range(1, n)) def sum(numbers): # Use `sum` function from standard library instead return reduce(operator.add, numbers, 1) def reverse(iterable): return reduce(lambda x, y: y+x, iterable) print(product([1, 2, 3]))
# 6
print(factorial(5))
# 24
print(sum([2, 6, 8, 3]))
# 20
print(reverse("hello"))
# olleh

从代码中可以看出, reduce 可以将代码简化或压缩为一行,否则会更长。 话虽如此,仅仅为了缩小代码,使其“更聪明”而滥用此功能通常是一个坏主意,因为它很快就会变得可怕和不可读。 出于这个原因,在我看来,它应该谨慎使用。

如果你记得它 reduce 往往把一切都缩短为一行,它可以完美地与 partial:

product = partial(reduce, operator.mul) print(product([1, 2, 3]))
# 6

最后,如果您需要的不仅仅是最终的“折叠”结果,那么您可以使用 accumulate– 来自另一个很棒的模块 itertools. 要计算最大值,可以按如下方式使用:

from itertools import accumulate data = [3, 4, 1, 3, 5, 6, 9, 0, 1] print(list(accumulate(data, max)))
# [3, 4, 4, 4, 5, 6, 9, 9, 9]

结论

正如您所看到的 functools,有许多有用的函数和装饰器可以让您的生活更轻松,但这只是冰山一角。 正如我开头所说,Python标准库中有很多功能可以帮助你写出更好的代码,所以除了我们这里介绍的功能之外,你还可以关注其他模块,比如 operatoror itertool. 如有任何疑问,您可以点击我的评论框。 我会尽力解决您的输入,并希望为您提供所需的输出。 您也可以在 LinkedIn 上联系我:-

https://www.linkedin.com/in/shivani-sharma-aba6141b6/

本文中显示的媒体不归 Analytics Vidhya 所有,由作者自行决定使用。

来源:https://www.analyticsvidhya.com/blog/2021/08/functools-the-power-of-higher-order-functions-in-python/

时间戳记:

更多来自 分析维迪亚