Python中的functools模块

368 阅读6分钟

在这篇文章中,我们将使用functools 模块来研究 Python 中使用的一个重要的函数模式。在编程中,我们经常使用高阶函数。它们是接受另一个函数作为参数的函数,并对其进行操作或返回另一个函数。在 Python 中通常被称为装饰器,它们可以非常有用,因为它们允许扩展一个现有的函数,而不需要对原始函数的源代码进行任何修改。它们为我们的函数充电,并扩展它们的额外功能。

函数模块是用来使用 Python 中内置的高阶函数的。任何在Python中是可调用对象的函数,都可以被认为是使用functools模块的函数。

涵盖的functools函数列表

  1. partial()
  2. partialmethod()
  3. reduce()
  4. wraps()
  5. lru_cache()
  6. cache()
  7. cached_property()
  8. total_ordering()
  9. singledispatch()

解释和使用

现在让我们开始进一步使用functools模块,实际了解每个函数。

1. partial()

partial是一个将另一个函数作为参数的函数。它接受一组参数,包括位置参数和关键字,这些输入被锁定在函数的参数中。

然后partial 返回所谓的partial对象,它的行为就像已经定义了这些参数的原始函数。

它被用来将一个多参数函数转化为一个单参数函数。它们的可读性更强,简单,容易类型化,并提供高效的代码完成。

例如,我们将尝试找出0-10数字的平方,首先用传统的函数,然后用functools的partial()得到同样的结果。这将有助于我们理解其用法。

  • 使用常规函数
def squared(num):
    return pow(num, 2) 
print(list(map(squared, range(0, 10))))
# Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

  • 使用functools中的partial()
from functools import partial

print(list(map(partial(pow, exp=2), range(0, 10))))
# Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

2. partialmethod()

partialmethod函数类似于partial函数,作为类方法的一个部分使用。它被设计成一个方法定义,而不是可以直接调用。简单地说,它是纳入我们自定义定义的类的方法。它可以被用来创建方便的方法,为该方法预先定义一些设定值。让我们看一个使用这个方法的例子。

from functools import partialmethod

class Character:
    def __init__(self):
        self.has_magic = False
    @property
    def magic(self):
        return self.has_magic

    def set_magic(self, magic):
        self.has_magic = bool(magic)

    set_has_magic = partialmethod(set_magic, True)

# Instantiating
witcher = Character()
# Check for Magical Powers
print(witcher.magic)  # False
# Providing Magical Powers to our Witcher
witcher.set_has_magic()
print(witcher.magic)  # True


3. reduce()

这个方法经常被用来输出某种累积值,由一些预先定义的函数计算。它把一个函数作为第一个参数,把一个迭代器作为第二个参数。它还有一个初始化器,如果函数中没有指定初始化器的任何特定值,则默认为0,还有一个迭代器,用来遍历所提供的迭代器的每一项。

  • 使用reduce()
from functools import reduce

# acc - accumulated/initial value (default_initial_value = 0)
# eachItem - update value from the iterable
add_numbers = reduce(lambda acc, eachItem: acc + eachItem, [10, 20, 30])
print(add_numbers)  # 60

4. wraps()

wraps接收一个它所包装的函数,并作为定义包装函数时使用的函数装饰器。它更新包装函数的属性以匹配被包装的函数。如果这个更新没有被传递给包装函数,那么包装函数的元数据就会被删除。 **wraps()**传递给包装函数,那么包装函数的元数据将被返回,而不是原始函数的元数据或属性,这些元数据应该是整个函数的实际元数据。为了避免这些类型的错误程序,wraps()非常有用。

还有一个相关的函数,就是 update_wrapper() 函数,它与wraps()相同。wraps()函数是update_wrapper()函数的语法糖,也是调用update_wrapper()的一个方便函数。

让我们看看上面的例子,以便对该函数有更好的理解。

  • 在不调用wraps的情况下定义一个装饰器函数,并尝试访问元数据
def my_decorator(func):
    def wrapper(*args, **kwargs):
        """
        Wrapper Docstring
        """
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def original_func():
    """
    Original Function Doctstring
    """
    return "Something"

print(original_func())
print(original_func.__name__)
print(original_func.__doc__)

# Output
'''
Something
wrapper

        Wrapper Docstring
'''

  • 定义相同的装饰器函数,调用wraps() ,并尝试访问元数据
from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """
        Wrapper Docstring
        """
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def original_func():
    """
    Original Function Doctstring
    """
    return "Something"
print(original_func())
print(original_func.__name__)
print(original_func.__doc__)

# Output
"""
Something
original_func

    Original Function Doctstring
"""

5. lru_cache*(maxsize=128*,typed=False)

这是一个装饰器,它用一个记忆化的可调用函数包裹着一个函数,简单地说,它使函数调用变得高效,同时在执行昂贵的I/O功能操作时,同样的参数被用于那个非常特别的昂贵函数。它本质上是使用最近的maxsize调用的缓存结果。lru_cache()中的LRULeast Recently Used的缩写。

它有一个默认的 maxsize为128,它设置了要缓存或保存的最近一次调用的数量,以便以后在同一操作中使用。typed=False将不同类型的函数参数值缓存在一起,简单地说,这意味着如果我们写type=True,那么整数10浮点数10.0的缓存值将被分别缓存。

lru_cache函数还包含了另外三个函数来执行一些额外的操作,即

  • cache_parameters()- 它返回一个新的dict,显示maxsize和typed的值。它只是为了提供信息,值的变化不会影响它。
  • cache_info()- 它被用来衡量缓存的有效性,以便在需要时重新调整最大尺寸。它输出一个命名的元组,显示被包裹函数的命中率、失误率、最大尺寸和当前尺寸。
  • cache_clear() - 用于清除之前缓存的结果。

在使用**lru_cache()**函数时,也有一些需要注意的地方。

  • 由于使用lru_cache()时,结果的底层存储是一个字典,所以**args和**kwargs必须是可散列的。*
  • 因为只有当一个函数无论执行多少次都返回相同的结果时,缓存才会起作用,所以它应该是一个纯函数,执行后不会产生任何副作用。

为了便于理解,我们将看到打印出斐波那契数列的经典例子。我们知道,对于这种计算,迭代时要反复使用相同的值,以便在找到时输出斐波那契数字。*那么,为这些重复的数字缓存数值似乎是**lru_cache()**的一个很好的用例。*让我们看看它的代码。

代码

from functools import lru_cache
@lru_cache(maxsize=32)
def fibonacci(n):
    if n < 2:
        return n
    print(f"Running fibonacci for {n}")
    return fibonacci(n - 1) + fibonacci(n - 2)

print([fibonacci(n) for n in range(15)])
print(fibonacci.cache_parameters())
print(fibonacci.cache_info())


输出

Running fibonacci for 2
Running fibonacci for 3 
Running fibonacci for 4 
Running fibonacci for 5 
Running fibonacci for 6 
Running fibonacci for 7 
Running fibonacci for 8 
Running fibonacci for 9 
Running fibonacci for 10
Running fibonacci for 11
Running fibonacci for 12
Running fibonacci for 13
Running fibonacci for 14
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]
{'maxsize': 32, 'typed': False}
CacheInfo(hits=26, misses=15, maxsize=32, currsize=15)


解释

我们可以看到,对于15这个范围内的每一个不同的数值,打印语句只提供了一次输出。虽然迭代的次数多了很多,但是通过使用lru_cache(),我们只能够看到唯一的结果被打印出来,重复的结果被缓存起来。在结果的旁边是上述函数被执行后的信息。

6. cache()

这个函数是lru_cache()本身的一个较小和轻量级的形式。但它对缓存值的数量没有固定的界限。因此,我们不需要为它指定最大尺寸。这和使用lru_cache(maxsize=None)是一样的

因为使用这个函数不会忘记正在缓存的值,所以它使cache()比有大小限制的lru_cache()快很多。这是Python 3.9新增加的一个功能。

在使用这个函数的时候,有一点需要注意的是,在实现这个函数的时候,如果有大量的输入,我们最终可能会得到非常大的缓存大小。因此,应该谨慎使用,并且要事先考虑清楚。

7. cached_property()

cached_property()类似于Python中的property(),允许我们把类属性变成属性或管理属性,作为一个内置函数。它还提供了缓存功能 ,在Python 3.8中被引入

计算的值被计算一次,然后作为普通属性保存在该实例的生命中。cache_property()也允许在没有定义setter的情况下进行写入,与property()函数不同。

Cached_property只运行:

  • 如果该属性还没有出现
  • 如果它必须执行查找

一般来说,如果属性已经存在于函数中,它就会像普通的属性一样执行读写操作。为了清除缓存的值,需要删除该属性,这确实允许再次调用cached_property()函数。

  • 如果不使用catched_property(),请看输出结果
class Calculator:
    def __init__(self, *args):
        self.args = args

    @property
    def addition(self):
        print("Getting added result")
        return sum(self.args)

    @property
    def average(self):
        print("Getting average")
        return (self.addition) / len(self.args)

my_instance = Calculator(10, 20, 30, 40, 50)

print(my_instance.addition)
print(my_instance.average)

"""
Output

Getting added result
150
Getting average
Getting added result
30.0
"""

  • 使用catched_property(),请看输出结果
from functools import cached_property

class Calculator:
    def __init__(self, *args):
        self.args = args

    @cached_property
    def addition(self):
        print("Getting added result")
        return sum(self.args)

    @property
    def average(self):
        print("Getting average")
        return (self.addition) / len(self.args)

my_instance = Calculator(10, 20, 30, 40, 50)

print(my_instance.addition)
print(my_instance.average)

"""
Output

Getting added result
150
Getting average
30.0
"""

解释一下

我们可以看到,在计算平均值时,Getting added的结果 被打印了两次,因为对于第一个函数,其结果没有被缓存。但是当我们使用**@cached_property装饰器时**,加法的结果已经被缓存,因此直接从内存中使用,以获得平均数。

8. total_ordering()

这个来自functools的高阶函数,当被用作类装饰器时,鉴于我们的类包含一个或多个丰富的比较排序方法,它提供了其余的方法,而没有在我们的类中明确定义。

它的意思是,当我们在类中使用比较dunder/magic方法时**__gt__()**, **__lt__(), __ge__(), __le__()**的时候,如果我们使用define **__eq__()**和其他四个方法中的一个,其他的方法就会自动由 **total_ordering()**来定义的。

需要注意的一件事是,它确实降低了代码的执行速度,并且为没有在我们的类中明确定义的比较方法创建了一个更复杂的堆栈跟踪。

让我们看一个简单的例子。

代码

@functools.total_ordering
class Student:
    def __init__(self, name, marks):
        self.name = name
        self.marks = marks

    def __eq__(self, other):
        return self.marks == other.marks

    def __gt__(self, other):
        return self.marks > other.marks

student_one = Student("John", 50)
student_two = Student("Peter", 70)

print(student_one == student_two)
print(student_one > student_two)

print(student_one >= student_two)
print(student_one < student_two)
print(student_one <= student_two)

"""
Output:

False
False
False
True
True
"""

解释一下。

在上面的代码中,我们导入了 total_ordering() 使用了不同的语法。这与之前所有例子中使用的从functools中导入是一样的。

我们所创建的类只包含两个比较方法。但是通过使用total_ordering()类装饰器,我们使我们的类实例能够自己派生出其他的比较方法。

9.singledispatch()

当我们定义一个函数时,它对不同输入类型的参数实现相同的操作。但是,如果我们想让一个函数在参数的输入类型不同时表现得不同呢?

我们发送一个列表或字符串或其他类型,我们希望根据我们发送的数据有不同的输出。我们怎样才能实现这一点呢?

functools模块有一个 **singledispatch()**装饰器来帮助我们编写这样的函数。其实现是基于被传递的参数类型。

通用函数的 **register()**的属性和**singledispatch()**方法被用来装饰重载的实现。如果实现像静态类型语言一样被注解了类型,那么装饰器就会自动推断出传递的args的类型,否则类型本身就是装饰器的一个参数。

同样,它也可以作为一个类方法通过使用 singledispatchmethod() 来达到同样的效果。

让我们看一个例子来更好地理解它:

from functools import singledispatch

@singledispatch
def default_function(args):
    return f"Default function arguments: {args}"

@default_function.register
def _(args: int) -> int:
    return f"Passed arg is an integer: {args}"

@default_function.register
def _(args: str) -> str:
    return f"Passed arg is a string: {args}"

@default_function.register
def _(args: dict) -> dict:
    return f"Passed arg is a dict: {args}"

print(default_function(55))
print(default_function("hello there"))
print(default_function({"name": "John", "age": 30}))

print(default_function([1, 3, 4, 5, 6]))
print(default_function(("apple", "orange")))

"""
Output:

Passed arg is an integer: 55
Passed arg is a string: hello there
Passed arg is a dict: {'name': 'John', 'age': 30}

Default function arguments: [1, 3, 4, 5, 6]
Default function arguments: ('apple', 'orange') 

"""

解释一下

在上面的例子中,我们可以看到,功能实现是根据所传递的参数类型来完成的。没有定义的类型由默认函数执行,与其他类型不同。

总结

在这篇文章中,我们浏览了Python中functools模块所提供的大部分函数。这些高阶函数提供了一些很好的方法来优化我们的代码,从而产生了干净、高效、易于维护和便于阅读的程序。