专项练习 · 9 分钟阅读

Python 装饰器练习指南:包装函数

装饰器的想法很小,回报却很大:函数可以传给另一个函数,并被它的返回值替换。理解这一点后,日志、计时、缓存和校验就能变成可复用的层,而不是到处复制的代码。

函数也是普通对象

可以把函数赋给名字、作为参数传递,也可以返回它。装饰器正是利用这些能力:接收原函数,并返回一个通常会调用它的可调用对象。

def greet():
    return "hello"

def call(func):
    return func()

print(call(greet))

传递函数

构造包装函数

内部函数通过闭包记住原函数,外层装饰器返回这个内部函数。在被装饰函数调用之前,包装函数里的代码不会运行。

def announce(func):
    def wrapper():
        return "Before: " + func()
    return wrapper

@announce
def greet():
    return "hello"

print(greet())

包装函数稍后运行

读懂 @ 简写

@ 行会在函数定义之后应用。@announce 放在 greet 上方,表示 greet = announce(greet)。此时名字 greet 已指向包装函数,所以包装函数必须显式调用原函数。

def upper(func):
    def wrapper():
        return func().upper()
    return wrapper

@upper
def word():
    return "hi"

print(word())

名字被包装函数替换

用 wraps 保留元数据

没有额外处理时,包装函数的 __name__ 会变成 wrapper。functools.wraps 会复制原函数名、文档字符串和相关属性。调试、文档和检查函数的工具都依赖这些信息。

from functools import wraps

def upper(func):
    @wraps(func)
    def wrapper():
        return func().upper()
    return wrapper

@upper
def word():
    return "hi"

print(word.__name__)

保留原函数身份

转发参数

没有参数的包装函数只能包装没有参数的函数。用 *args 和 **kwargs 接收原函数能接受的值,再原样转发。包装函数自己的逻辑应放在调用原函数之外。

def announce(func):
    def wrapper(*args, **kwargs):
        return "Result: " + str(func(*args, **kwargs))
    return wrapper

@announce
def add(a, b):
    return a + b

print(add(2, 3))

通用包装函数

你的练习清单

常见问题

装饰器可以接收参数吗?

可以。这时需要再加一层:先接收参数,再返回真正的装饰器。

@ 会立刻改变函数吗?

它会在定义时替换名字。包装函数的函数体要等到新名字被调用时才运行。

装饰器只能用于函数吗?

类也可以被装饰,但先从函数开始。只要一个可调用对象能被另一个可调用对象替换,同样的思路就适用。

练习装饰器

12 道题,覆盖包装函数、@ 语法、functools.wraps 和参数转发。

打开练习

继续阅读: