Skip to main content

Command Palette

Search for a command to run...

Python 装饰器:把函数包起来,但不把脑子绕进去

Updated

Python 里有一个很优雅、也很容易把初学者绕晕的东西:装饰器。

它看起来像这样:

@decorator
def hello():
    print("Hello")

很多人第一次看到 @xxx 会以为这是某种神秘语法。其实装饰器的本质很简单:

装饰器就是一个“接收函数,并返回新函数”的函数。

换句话说,它可以在不修改原函数代码的情况下,给原函数增加额外功能。

从一个普通函数开始

先看一个普通函数:

def say_hello():
    print("Hello, Python!")

say_hello()

输出:

Hello, Python!

现在假设我们想在函数执行前后打印一些日志:

def say_hello():
    print("函数开始执行")
    print("Hello, Python!")
    print("函数执行结束")

这样当然可以,但问题是:如果有 10 个函数都要加日志呢?

一个个改,太累,也不优雅。

这时装饰器就派上用场了。

函数也可以当作参数

理解装饰器前,要先知道 Python 里的函数可以像普通变量一样传来传去:

def say_hello():
    print("Hello")

def run_func(func):
    func()

run_func(say_hello)

这里 say_hello 被当作参数传给了 run_func

这就是装饰器成立的基础。

写一个最简单的装饰器

def log(func):
    def wrapper():
        print("函数开始执行")
        func()
        print("函数执行结束")
    return wrapper

这个 log 函数接收一个函数 func,然后在内部定义了一个新函数 wrapper

wrapper 做了三件事:

print("函数开始执行")
func()
print("函数执行结束")

最后,log 返回了这个 wrapper

使用方式如下:

def say_hello():
    print("Hello, Python!")

say_hello = log(say_hello)

say_hello()

输出:

函数开始执行
Hello, Python!
函数执行结束

这就是装饰器的本质。

@ 语法只是简写

上面的代码:

say_hello = log(say_hello)

可以简写成:

@log
def say_hello():
    print("Hello, Python!")

完整代码:

def log(func):
    def wrapper():
        print("函数开始执行")
        func()
        print("函数执行结束")
    return wrapper


@log
def say_hello():
    print("Hello, Python!")


say_hello()

这两种写法完全等价。

所以:

@log
def say_hello():
    ...

本质上就是:

say_hello = log(say_hello)

处理带参数的函数

刚才的装饰器只能装饰没有参数的函数。

如果函数有参数:

def greet(name):
    print(f"Hello, {name}")

原来的装饰器就会报错。

我们需要让 wrapper 接收任意参数:

def log(func):
    def wrapper(*args, **kwargs):
        print("函数开始执行")
        result = func(*args, **kwargs)
        print("函数执行结束")
        return result
    return wrapper

使用:

@log
def greet(name):
    print(f"Hello, {name}")


greet("Mu-Miao")

输出:

函数开始执行
Hello, Mu-Miao
函数执行结束

这里的:

*args
**kwargs

表示可以接收任意位置参数和关键字参数。

这样装饰器就更通用了。

保留原函数信息

还有一个小问题。

看下面这段代码:

print(greet.__name__)

你可能希望输出:

greet

但实际可能输出:

wrapper

因为经过装饰后,greet 实际上已经变成了 wrapper

为了解决这个问题,可以使用 functools.wraps

from functools import wraps

def log(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("函数开始执行")
        result = func(*args, **kwargs)
        print("函数执行结束")
        return result
    return wrapper

这样就能保留原函数的名字、文档字符串等信息。

实际开发中,写装饰器时建议加上 @wraps(func)

一个实用例子:统计函数运行时间

装饰器常用于日志、权限校验、缓存、性能统计等场景。

比如统计函数运行耗时:

import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} 执行耗时:{end - start:.4f} 秒")
        return result
    return wrapper

使用:

@timer
def slow_task():
    time.sleep(1)
    print("任务完成")


slow_task()

输出:

任务完成
slow_task 执行耗时:1.0012 秒

这个例子里,slow_task 本身只负责做任务,计时逻辑由装饰器负责。

这就是装饰器的好处:把额外逻辑和核心逻辑分开。

带参数的装饰器

有时候,我们希望装饰器本身也能接收参数。

比如:

@repeat(3)
def say_hi():
    print("Hi")

希望函数执行 3 次。

这时需要多包一层函数:

from functools import wraps

def repeat(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

使用:

@repeat(3)
def say_hi():
    print("Hi")


say_hi()

输出:

Hi
Hi
Hi

这里有三层函数:

repeat(times)
decorator(func)
wrapper(*args, **kwargs)

可以这样理解:

  • repeat(3) 接收装饰器参数

  • decorator(func) 接收被装饰的函数

  • wrapper() 执行增强后的逻辑

装饰器常见用途

装饰器在实际开发中非常常见,比如:

@login_required
def user_profile():
    ...

表示访问这个函数前需要登录。

@cache
def get_data():
    ...

表示缓存函数结果。

@timer
def train_model():
    ...

表示统计函数执行时间。

在 Flask、FastAPI、Django 等框架里,装饰器也经常出现:

@app.route("/hello")
def hello():
    return "Hello"

这里的 @app.route("/hello") 本质上也是一个装饰器。

总结

Python 装饰器看起来神秘,但核心思想并不复杂。

一句话总结:

装饰器是一个接收函数、返回函数的函数,用来在不修改原函数代码的情况下增强函数功能。

它的基本形式是:

def decorator(func):
    def wrapper(*args, **kwargs):
        # 执行前的逻辑
        result = func(*args, **kwargs)
        # 执行后的逻辑
        return result
    return wrapper

再配合 @decorator 语法,就可以写出非常简洁、优雅、可复用的代码。

刚开始学装饰器时,不用急着记住所有复杂写法。先记住这句话就够了:

@log
def func():
    ...

等价于:

func = log(func)

理解了这一点,装饰器就不再是魔法,而只是 Python 给函数穿上的一件外套。

python

Part 1 of 1

Python是最好的语言