装饰

装饰器 属于 Python

{other: "%{count} 个练习"}

关于 装饰器

装饰器是接收另一个函数作为实参的函数,其目的是扩展或替换传入函数的行为。 如果函数A是装饰器,函数B是它的实参,那么函数A会修改、扩展或替换函数B的行为,而不修改_函数B的代码。 我们说装饰器函数A包装_了函数B。 虽然我们说的是“修改”行为,但被包装的函数其实_并未被改变。行为要么是添加在_被包装函数周围(以及它返回的内容),要么是被包装函数的行为被_替换_成另一种行为。

装饰器是一种高阶函数

一个高阶函数是接收一个或多个函数作为实参、和/或返回一个或多个函数的函数。 一个函数,如果被用作实参或由另一个函数返回,就是一个一等函数。 Python 函数作为一种可调用对象,就是一等函数,可以存入变量或用作实参,就像其他任何值或对象一样。 高阶函数和一等函数共同作用,才让装饰器成为可能。

使用装饰器是什么样子

@符号加在装饰器函数名字的前面,放在要被装饰的函数的正上方,像这样:

@decorator
def decorated_function():
    pass

有些装饰器接受实参:

@decorator_with_arg(name="Bob")
def decorated_function2():
    pass

如果装饰器定义了默认实参,就必须在@decorator()调用中使用括号,装饰器才能正常工作,就像调用任何函数时一样:

@decorator_with_default_arg()
def decorated_function3():
    pass

如果装饰器接收_位置_实参,不提供这些实参就会导致错误,错误信息大致如下:

TypeError: decorator_with_pos_arg() missing 1 required positional argument: 'name'

@decorator语法是语法糖,也可以说是调用_装饰函数_、并把_被装饰的函数_作为实参传给它的简写形式。 下面是一些调用装饰器的其他写法示例:

def function():
    pass

function = decorator(function)


def function2():
    pass

function2 = decorator_with_arg(name="Bob")(function2)


def function3():
    pass

function3 = decorator_with_default_arg()(function3)

编写一个简单的装饰器

大多数装饰器旨在_扩展_或_替换_另一个函数的行为,但有些装饰器可能什么都不做,只是返回它们所包装的函数。

装饰器是至少接收一个实参的函数,即它们所包装的那个函数。 它们通常返回被包装的函数,或者返回一个使用了该被包装函数的表达式的结果。

一个简单的装饰器(只是返回它包装的函数)可以这样写:

>>> def do_nothing(func):
...     return func
...
... @do_nothing
... def function4():
...     return 4
...
>>> print(function4())
4

装饰器也可以只添加副作用,比如用于日志记录的额外信息:

>>> def my_logger(func):
...     print(f"Entering {func.__name__}")
...     return func
...
... @my_logger
... def my_func():
...     print("Hello")
...
>>> my_func()
Entering my_func
Hello

装饰器不返回它自己。 它可能返回它的函数实参、另一个函数,或者一个或多个用来替换传入函数或被装饰函数返回值结果的值。 如果装饰器返回另一个函数,通常返回的是一个内部函数。

内部函数

函数可以定义在另一个函数里面。 这种嵌套的函数叫做内部函数。 装饰器可以用内部函数来包装它的函数实参。 然后装饰器返回这个内部函数。 内部函数接着可能返回最初传入的那个函数实参。

用内部函数实现校验的装饰器

下面是一个用装饰器做校验的例子:

def my_validator(func):
    def my_wrapper(world):
        print(f"Entering {func.__name__} with {world} argument")
        
        if world == "Pluto":
            print("Pluto is not a planet!")
        else:
            return func(world)
    return my_wrapper

@my_validator
def my_func(planet):
    print(f"Hello, {planet}!")

my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!

my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Pluto is not a planet!

第一行定义的是装饰器的func实参。 下一行定义的是装饰器的_内部函数_,它包装了func实参。 由于_内部函数_包装了装饰器的func实参,传给func的实参会同样传给它。 注意,包装函数不必为实参使用与func中定义的相同的名字。 原函数用的是planet,装饰器用的是world,装饰器仍然能正常工作。

内部函数要么返回func,要么在world == "Pluto"时打印一条信息,说明冥王星不是行星。 也可以把它改写成抛出一个ValueError。 所以,内部函数_包装了func,要么返回func,要么做某件事来替代func原本要做的事。 装饰器返回它的_内部函数。 _内部函数_可能返回也可能不返回最初传入的那个函数。 取决于包装函数或_内部函数_中哪段代码被条件执行,可能返回func,可能抛出错误,也可能返回一个属于func返回类型的值。

装饰一个接收任意数量实参的函数

利用*args和**kwargs语法,可以为接收任意数量实参的函数编写装饰器。

下面是一个用于接收任意数量实参的函数的装饰器示例:

def double(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs) * 2
    return wrapper

@double
def add(*args):
    return sum(args)

print(add(2, 3, 4))
#-> 18

print(add(2, 3, 4, 5, 6))
#-> 40

这能把函数实参的返回值翻倍。 如果我们想把返回值变成三倍、四倍等,可以给装饰器本身加一个形参,下一节会演示。

自带形参的装饰器

下面是一个可以配置的装饰器示例,它能把被装饰函数的返回值乘以任意倍数:

def multi(factor=1):
     if factor == 0:
        raise ValueError("factor must not be 0")

     def outer_wrapper(func):
         def inner_wrapper(*args, **kwargs):
             return func(*args, **kwargs) * factor
         return inner_wrapper
     return outer_wrapper

@multi(factor=3)
def add(*args):
    return sum(args)

print(add(2, 3, 4))
#-> 27

print(add(2, 3, 4, 5, 6))
#-> 60

开头几行校验factor不为0。 接着定义外层包装函数。 它的签名与不带参数的装饰器所期望的签名相同。 外层包装函数里有一个内部函数,其签名与原函数相同。 内部包装函数负责把原函数的返回值乘以传给装饰器的实参。 外层包装函数返回内部包装函数,装饰器则返回外层包装函数。

下面是一个带参数的装饰器示例,它控制是否校验传给原函数的实参:

def check_for_pluto(check=True):
    def my_validator(func):
        def my_wrapper(world):
            print(f"Entering {func.__name__} with {world} argument")
            if check and world == "Pluto":
                print("Pluto is not a planet!")
            else:
                return func(world)
        
        return my_wrapper
    return my_validator

@check_for_pluto(check=False)
def my_func(planet):
    print(f"Hello, {planet}!")

my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!

my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Hello, Pluto!

这样可以轻松切换是否检查Pluto,而且无需修改my_func。

通过 GitHub 编辑 该链接会在新窗口或标签页中打开