Python 高阶函数的装饰器

Python 高阶函数的装饰器,装饰器的核心思想是将某些原始函数转换成另一种形式。装饰器基于装饰符和原始被装饰函数创建复合函数。

可以通过以下两种方式使用装饰器函数。

以前缀形式创建一个与基函数同名的新函数,如下所示:

@decorator
def original_function():
    pass

以显式运算的形式返回一个新函数(名字可能不同)。

def original_function():
    pass
original_function = decorator(original_function)

以上是针对同一操作的两种语法。前缀表示法的优点是简洁明了。装饰符在前缀位置对于一些读者来说更易读。后缀表示法是显式的,且略微灵活一些。

尽管前缀表示法很常用,但有时会使用后缀表示法,因为我们不希望生成的函数取代原来的函数。我们更希望执行以下命令,以同时使用装饰后和装饰前的函数。

new_function = decorator(original_function)

这将由原始函数构建出一个名为new_function()的新函数。Python函数是头等对象,因此使用 @decorator语法时,原始函数将不再可用。

装饰器是能接收函数作为参数并且返回函数的函数,这简单描述了Python语言的内置特性,但问题是之后如何更新或调整函数的内部代码结构呢?

答案是无须更改。与其纠结于内部代码,不如简单定义一个封装原始函数的新函数。这样能简化参数值或结果的处理,并且无须理会原始函数的核心处理机制。

定义装饰器时会涉及高阶函数的两个阶段,如下所示。

  • 在定义阶段,装饰器函数将封装基函数并返回封装后的函数。作为构建装饰器函数的一部分,在装饰过程中会处理一些一次性求值,例如计算比较复杂的默认值。
  • 在求值阶段,封装函数会(并且通常可以)对基函数进行求值。封装函数可以对参数值进行预处理,也可以对返回值进行后处理(或者两者兼有)。使用封装函数也可以避免调用基函数,例如在管理缓存的情况下,封装可避免调用基函数产生的高昂开销。

简单的装饰器如下所示:

from functools import wraps
from typing import Callable, Optional, Any, TypeVar, cast

FuncType = Callable[..., Any]
F = TypeVar('F', bound=FuncType)

def nullable(function: F) -> F:
    @wraps(function)
    def null_wrapper(arg: Optional[Any]) -> Optional[Any]:
        return None if arg is None else function(arg)
    return cast(F, null_wrapper)

通常使用functools.wraps()函数来确保被装饰的函数能保留原始函数的属性,例如复制__name____doc__属性可以确保生成的装饰器函数具有原始函数的名称和文档字符串。

生成的复合函数,即装饰器定义中的null_wrapper()函数,也是一种高阶函数。它以表达式的形式结合了原始函数,即可调用对象function(),并保留了None值。在生成的null_wrapper()函数中,原始的可调用对象function不再作为显式参数,而是能从null_wrapper()函数定义的上下文中获取其值的自由变量。

装饰器函数的返回值是新创建的函数,名称与原始函数相同。装饰器只返回函数而不处理其中的数据,这一点很重要。装饰器使用的是元编程,即可以创建代码的代码。随后生成的null_wrapper()函数可用于处理实际数据。

请注意,类型提示使用了TypeVar的特性来确保运用装饰器后得到的是Callable类型的对象。类型变量F的类型与原始函数绑定,而装饰器的类型提示声明了结果函数类型应与参数函数相同。通用的装饰器适用于各类函数,因此需要绑定一个类型变量。

可以运用@nullable装饰器来创建复合函数,如下所示:

@nullable
def nlog(x: Optional[float]) -> Optional[float]:
    return math.log(x)

这会创建一个nlog()函数,它是内置math.log()函数的支持空值的版本。整个装饰过程返回了一个调用原始nlog()函数的null_wrapper()函数版本。结果命名为了nlog(),并且具有封装后的函数和原始被封装函数的复合行为。

如下所示使用该复合nlog()函数:

>>> some_data = [10, 100, None, 50, 60]
>>> scaled = map(nlog, some_data)
>>> list(scaled)
[2.302585092994046, 4.605170185988092, None, 3.912023005428146, 4.0943445622221]

我们对一个数据集应用了该函数。输入None值而输出None,并且没有涉及任何异常处理。

 这种示例并不适合单元测试。出于测试的目的,应对数值进行舍入。为此,还需创建一个支持空值的round()函数。

使用装饰器表示法创建一个支持空值的舍入函数,如下所示:

@nullable
def nround4(x: Optional[float]) -> Optional[float]:
    return round(x, 4)

该函数使用和封装了round()函数的部分功能来支持空值。

使用Optional类型定义,方便了模块typing描述支持空值的函数类型和支持空值的结果类型。定义Optional[float]等同于Union[None, float],即None对象和float对象皆可使用。

还可以创建支持空值的舍入函数,如下所示:

nround4 = nullable(lambda x: round(x, 4))

请注意,我们没有在函数定义前使用装饰器,而是将装饰器用于一个匿名函数,这与在函数定义前添加装饰器的效果相同。

可以使用ound4()函数为nlog()函数创建更好的测试用例,如下所示:

>>> some_data = [10, 100, None, 50, 60]
>>> scaled = map(nlog, some_data)
>>> [nround4(v) for v in scaled]
[2.3026, 4.6052, None, 3.912, 4.0943]

这里的结果与平台无关,因此便于文档测试。

对匿名函数使用类型提示可能有一定的挑战。以下代码说明了需要为此做些什么。

nround4l: Callable[[Optional[float]], Optional[float]] = (
    nullable(lambda x: round(x, 4))
)

变量nround4l的类型提示是一个带有[Optional[float]]参数列表的Callable对象,且其返回值类型是Optional[float]。使用Callable类型提示仅适用于位置参数,对于存在关键字参数及其他复杂情况,请参阅http://mypy.readthedocs.io/en/latest/kinds_of_types.html#extended-callable-types。

装饰器@nullable假定所装饰的是一元函数(unary)。为了处理任意参数集合,需要重新考虑这个设计,创建一个更为通用的支持空值的装饰器。

后面将介绍处理None值问题的一种替代方法。PyMonad库定义了一个Maybe对象类,它既可以是某个合适的值,也可以是None值。

使用functoolsupdate_wrapper()函数

装饰器@wraps利用update_wrapper()函数来保留被封装函数的某些属性,通常这就完成了默认情况下我们需要的一切。该函数将原始函数的一组特定属性列表复制到了由装饰器创建的结果函数中。这组特定的属性列表是什么?它由一个全局模块定义。

函数update_wrapper()依赖一个模块的全局变量来决定保留哪些属性。变量WRAPPER_ASSIGNMENTS定义了默认被复制的属性。这组默认被复制的属性列表是:

('__module__', '__name__', '__qualname__', '__doc__', '__annotations__')

很难对该列表做有意义的修改。def语句的内部不允许进行简单的修改或变更,这一点需要注意。

如果要创建callable对象,可以用一个类来提供作为定义的一部分的一些附加属性。这可能导致装饰器必须将这些附加属性从原始被封装的callable对象复制到正在创建的封装函数中。然而,通过面向对象的类设计方法来进行这种修改,会比利用复杂装饰器技术简单一些。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程