小白学 Python
AI 编程指南GitHub
© 2026 小白学 Python · 基于 walter201230/Python 教程
课程目录关于本站联系方式隐私政策GitHub

装饰器

完整讲解 · 4 段教学·配 3 道练习题·预计 30 分钟

本页是本章的通读版,可直接读完全部讲解。想动手写代码、跑判分,去 闯关模式。

教学 01 / 04

第十六节:装饰器

上一章学了闭包,这一章我们一步步演变一个需求,来认识 Python 中非常有特色的语法——装饰器(Decorator)。

需求 1:员工打卡

先看一个最简单的打卡函数:

python到闯关页运行这段 →
def punch():
    print('昵称:两点水  部门:研发部  上班打卡成功')

punch()

输出:

昵称:两点水  部门:研发部  上班打卡成功

很朴素。

需求 2:加上日志

产品经理跑过来说:「不行啊,怎么没有日志?打卡前后都给我打印一下提示!」

直接改函数:

python到闯关页运行这段 →
def punch():
    print('[LOG] 开始执行')
    print('昵称:两点水  部门:研发部  上班打卡成功')
    print('[LOG] 执行结束')

punch()

可以是可以——但这样改变了函数本身的功能结构。本来 punch 只是打卡,现在硬塞进了日志。

而且想象一下:如果还有 holiday()、apply_leave()、overtime() 等等很多函数都要加日志——你打算每个函数都写一遍 [LOG] 开始执行 吗?

代码重复 = 坏味道。

需求 3:抽出来

我们知道 Python 函数有两个特点:

  • 函数也是一个对象,可以被传来传去
  • 函数里可以嵌套函数

那就把日志抽出来:

python到闯关页运行这段 →
def punch():
    print('昵称:两点水  部门:研发部  上班打卡成功')

def add_log(func):
    print('[LOG] 开始执行')
    func()
    print('[LOG] 执行结束')

add_log(punch)

输出:

[LOG] 开始执行
昵称:两点水  部门:研发部  上班打卡成功
[LOG] 执行结束

这下 punch 没动过,任何想加日志的函数都可以扔进 add_log。

但调用方式变了——以前是 punch(),现在变成 add_log(punch)。如果项目里 punch() 已经被调用了 100 处,难道全要改成 add_log(punch)?

我们想要的是:不改函数本身,也不改调用方式,就给函数加上日志。

这就是装饰器要解决的事。

教学 02 / 04

一、第一个装饰器

1、装饰器写法

装饰器的写法和闭包很像——只不过它接收的是一个函数:

python到闯关页运行这段 →
def decorator(func):
    def wrapper():
        print('[LOG] 开始执行')
        func()
        print('[LOG] 执行结束')
    return wrapper


def punch():
    print('昵称:两点水  部门:研发部  上班打卡成功')


f = decorator(punch)
f()

输出:

[LOG] 开始执行
昵称:两点水  部门:研发部  上班打卡成功
[LOG] 执行结束

装饰器函数一般做这三件事:

  1. 接收一个函数作为参数
  2. 嵌套一个 wrapper 函数——在里面调用原函数,并附加额外功能
  3. 返回这个 wrapper 函数

2、@ 语法糖

可是认真一看:这写法怎么看都比直接传函数还麻烦啊——还要 f = decorator(punch) 拿到包装后的函数再调用。

这就是为什么 Python 引入了 @ 语法糖——它让定义装饰器、把装饰器调用原函数再把结果赋值给原函数对象名的过程变得非常简洁。

用法:在原函数定义上方加 @装饰器名:

python到闯关页运行这段 →
def decorator(func):
    def wrapper():
        print('[LOG] 开始执行')
        func()
        print('[LOG] 执行结束')
    return wrapper


@decorator
def punch():
    print('昵称:两点水  部门:研发部  上班打卡成功')


punch()

输出:

[LOG] 开始执行
昵称:两点水  部门:研发部  上班打卡成功
[LOG] 执行结束

@decorator 等价于在 def punch() 之后偷偷帮你执行了一句:

python到闯关页运行这段 →
punch = decorator(punch)

所以从此以后,调用 punch() 实际上调的是 wrapper(),外层加了日志,里面再调原 punch。

3、装饰器的核心思想

Python 在引入装饰器时,没有引入任何新的语法特性——它都是基于函数是对象、函数可嵌套这些已有的能力。装饰器不是 Python 特有的概念,每种语言都可以有这种思想;只不过 Python 给了它 @ 这个糖,写起来格外漂亮。

一句话:装饰器 = 不修改原函数和调用方式,给函数额外加一层功能。

练习 1 / 3写一个 @log 装饰器去闯关页做这题 →
教学 03 / 04

二、装饰带参数的函数

上一节的 punch 是无参数的。可现实中函数往往是有参数的——昵称、部门都不能写死,得通过参数传:

python到闯关页运行这段 →
@decorator
def punch(name, department):
    print(f'昵称:{name}  部门:{department}  上班打卡成功')

punch('两点水', '研发部')

如果还是用之前那个 wrapper()(没参数),punch('两点水', '研发部') 就会报「wrapper() takes 0 positional arguments」——因为真正被调用的是 wrapper,而 wrapper 没接参数。

1、用 *args, **kwargs 接住所有参数

装饰器中的 wrapper 函数可以使用 *args 接收所有位置参数。但仅 *args 不能涵盖关键字参数,所以还要加上 **kwargs。

完整写法:

python到闯关页运行这段 →
def decorator(func):
    def wrapper(*args, **kwargs):
        print('[LOG] 开始执行')
        func(*args, **kwargs)
        print('[LOG] 执行结束')
    return wrapper


@decorator
def punch(name, department):
    print(f'昵称:{name}  部门:{department}  上班打卡成功')


@decorator
def print_info(reason, **kwargs):
    print(reason)
    print(kwargs)


punch('两点水', '研发部')
print_info('请假申请', sex='男', age=99)

输出:

[LOG] 开始执行
昵称:两点水  部门:研发部  上班打卡成功
[LOG] 执行结束
[LOG] 开始执行
请假申请
{'sex': '男', 'age': 99}
[LOG] 执行结束

这样一来,任何参数形态的函数都可以套上这个装饰器了。

2、有返回值的函数怎么办

如果原函数有返回值,wrapper 要把它接住并 return 出去,否则装饰之后返回值就丢了:

python到闯关页运行这段 →
def decorator(func):
    def wrapper(*args, **kwargs):
        print('[LOG] 开始执行')
        result = func(*args, **kwargs)
        print('[LOG] 执行结束')
        return result
    return wrapper


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


print(add(3, 5))

输出:

[LOG] 开始执行
[LOG] 执行结束
8

这就是装饰器最常见、最完整的模板——记下来,以后写各种通用装饰器都按这个套路。

练习 2 / 3@uppercase:改造返回值去闯关页做这题 →
教学 04 / 04

三、带参数的装饰器与 functools.wraps

1、装饰器自己也想带参数

有时我们希望装饰器本身也接受参数——比如「日志带级别」:

python到闯关页运行这段 →
@log(level='INFO')
def hello():
    print('hi')

这就需要三层嵌套:

  • 最外层接收装饰器参数
  • 中间层接收原函数
  • 最内层是真正的 wrapper

代码模板如下:

python到闯关页运行这段 →
def log(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f'[{level}] 开始执行')
            result = func(*args, **kwargs)
            print(f'[{level}] 执行结束')
            return result
        return wrapper
    return decorator


@log(level='INFO')
def hello():
    print('hi')


hello()

输出:

[INFO] 开始执行
hi
[INFO] 执行结束

理解三层结构的关键:@log(level='INFO') 先调用 log('INFO'),得到中间那个 decorator,再用它去装饰 hello。所以本质上还是「decorator(hello)」——只不过 decorator 是临时生成的、带着 level 这个闭包变量。

2、functools.wraps 保留元信息

装饰器有一个副作用:装饰之后,原函数的 __name__、__doc__ 都会被替换成 wrapper 的:

python到闯关页运行这段 →
def decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper


@decorator
def hello():
    """打个招呼"""
    print('hi')


print(hello.__name__)   # wrapper —— 不是 hello!
print(hello.__doc__)    # None —— 文档丢了!

调试时这很烦人。解决办法是用 functools.wraps 把原函数的元信息抄过来:

python到闯关页运行这段 →
import functools


def decorator(func):
    @functools.wraps(func)            # 关键这一行
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper


@decorator
def hello():
    """打个招呼"""
    print('hi')


print(hello.__name__)   # hello
print(hello.__doc__)    # 打个招呼

写装饰器时,给 wrapper 函数加 @functools.wraps(func) 是一个好习惯——它把原函数的名字、文档、签名等信息保留下来,调试和文档生成时都不会出问题。

3、小结

回顾这一章学到的:

  • 装饰器 = 闭包 + @ 语法糖——不改原函数、不改调用方式给函数加功能
  • wrapper 用 *args, **kwargs 接所有参数;有返回值要 return
  • 带参数的装饰器 = 三层嵌套
  • 别忘了 @functools.wraps

接下来在练习里动手写几个常用装饰器吧。

练习 3 / 3@count_calls:统计调用次数去闯关页做这题 →

读完了?动手练一遍才算真会。

去闯关模式练习 →