装饰器 (中): 进阶篇,解锁框架级玩法

基础看完了?那我们来玩点进阶的。

你有没有好奇过,FastAPI 的@depends()、Flask 的@app.route()都是怎么写的?为啥人家的装饰器能传参数,还能这么灵活?

今天我就把这些进阶玩法教给你,学会了,你也能写出灵活的装饰器,看懂各种框架的源码。

1. 带参数的装饰器,了解一下?

有时候你可能会想,能不能给装饰器自己也传点参数?比如我想要一个重复执行 N 次的装饰器,N 我自己说了算。

这也简单,就是再包一层嘛!

三层函数的秘密

普通的装饰器是两层:外部函数接收原函数,内部函数是包装后的函数。

那带参数的装饰器,就是再加一层:最外层接收装饰器的参数,然后返回真正的装饰器。

我们来看个最经典的例子:

python 复制代码
def repeat(times):
    # 这一层:接收装饰器的参数,比如times=3
    def decorator(func):
        # 这一层:接收原函数,就是普通的装饰器了
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # 这一层:真正的包装函数,执行原函数
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

# 用一下!
@repeat(times=3)
def say_hello(name):
    print(f"Hello, {name}!")

say_hello("Alice")
# 输出:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

看到了吗?@repeat(times=3)其实就是先调用repeat(3),它返回了一个装饰器,然后用这个装饰器去装饰say_hello函数。

等价于:say_hello = repeat(times=3)(say_hello),就这么简单!

更实用的例子:带级别的日志装饰器

我们来写一个更实用的,你可以指定日志的级别,是 info 还是 debug:

python 复制代码
import logging

def log(level="info"):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # 根据你指定的级别来打日志
            msg = f"调用函数: {func.__name__}"
            if level == "debug":
                logging.debug(msg)
            else:
                logging.info(msg)
            return func(*args, **kwargs)
        return wrapper
    return decorator

# 用一下!
@log(level="debug")
def debug_function():
    # 这个函数的日志会打debug级别
    pass

@log()
def normal_function():
    # 这个函数的日志会打默认的info级别
    pass

🌟 这就是框架的秘密:FastAPI 的@depends()、Flask 的@app.route(),本质上都是这个模式!学会了这个,你看很多框架的源码就都能看懂了。

2. 多个装饰器一起用,注意顺序!

你可以给一个函数加多个装饰器,但是这里有个超级容易踩的坑:装饰器的执行顺序,很多人搞反了

我们来看个实际的例子,你就懂了:

python 复制代码
def decorator1(func):
    @functools.wraps(func)
    def wrapper():
        print("进入 decorator1")
        func()
        print("退出 decorator1")
    return wrapper

def decorator2(func):
    @functools.wraps(func)
    def wrapper():
        print("进入 decorator2")
        func()
        print("退出 decorator2")
    return wrapper

@decorator1
@decorator2
def hello():
    print("Hello!")

hello()

你猜输出是什么?

Plain 复制代码
进入 decorator1
进入 decorator2
Hello!
退出 decorator2
退出 decorator1

哦!原来装饰器是从下到上装饰,从上到下执行

也就是说,@decorator1在最上面,它先执行,然后才是@decorator2,最后才是原函数。

等价于:hello = decorator1(decorator2(hello)),所以调用的时候,先调用 decorator1 的 wrapper,然后它调用 decorator2 的 wrapper,然后它调用原函数。

⚠️ 踩坑提醒 :顺序很重要!比如你写 Web 接口,@login_required@permission_required,一定要把@login_required放在上面,不然未登录的用户会先去检查权限,直接报错!

比如:

python 复制代码
# 正确的顺序
@login_required
@permission_required("delete_user")
def delete_user(user_id):
    # ...

这样,用户先检查登录,没登录直接跳登录页,不会去检查权限了。

3. 类装饰器,给类也来个包装

除了函数,装饰器还能用来装饰类!这在写 ORM、写框架的时候特别有用。

类装饰器的原理很简单:它接收一个类,然后返回一个新的类(或者修改后的类)。

我们来写一个最实用的:自动给类加一个好看的__repr__方法,这样你打印对象的时候,就能自动看到所有的属性了:

python 复制代码
def add_repr(cls):
    # 这就是类装饰器,接收类cls
    @functools.wraps(cls)
    def __repr__(self):
        # 自动生成__repr__,把所有的属性都列出来
        attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
        return f"{cls.__name__}({attrs})"
    # 给类加上这个方法
    cls.__repr__ = __repr__
    return cls

# 用一下!
@add_repr
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

p = Person("Alice", 25)
print(p)
# 输出: Person(name='Alice', age=25)

是不是超方便?不用你自己写__repr__了,加个装饰器就搞定了!

这个在写 ORM 框架的时候特别有用,比如 SQLAlchemy 的模型类,很多都是用类装饰器来自动注册的。

4. 用类来实现装饰器,保存状态更方便

如果你的装饰器需要维护复杂的状态,用类比用闭包更好,代码更清晰,也更容易测试。

因为类可以很自然的保存状态,用self.xxx就可以了,不用像闭包那样搞 nonlocal。

原理也很简单:装饰器接收原函数,存到self.func里,然后实现__call__方法,这样实例就能当函数用了。

我们来写一个计数器装饰器,统计函数被调用了多少次:

python 复制代码
class Counter:
    def __init__(self, func):
        # 初始化的时候,接收原函数
        self.func = func
        self.count = 0  # 状态存在这里
        # 别忘了把原函数的信息复制过来
        functools.update_wrapper(self, func)
    
    def __call__(self, *args, **kwargs):
        # 当你调用装饰后的函数的时候,就会调用这个方法
        self.count += 1
        return self.func(*args, **kwargs)

# 用一下!
@Counter
def add(a, b):
    return a + b

print(add(1, 2))  # 输出: 3
print(add(3, 4))  # 输出: 7
print(add.count)  # 输出: 2,调用了2次!

看到了吗?用类实现的话,状态的维护就非常直观,self.count一眼就能看到,比闭包的 nonlocal 好理解多了。

而且如果你的装饰器还需要带参数,也很简单,把参数放到__init__里就行:

python 复制代码
class Repeat:
    def __init__(self, times):
        self.times = times
    
    def __call__(self, func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(self.times):
                result = func(*args, **kwargs)
            return result
        return wrapper

@Repeat(times=3)
def say_hello(name):
    print(f"Hello, {name}")

是不是很灵活?


好了,进阶的内容差不多就这些了。现在你已经能写各种灵活的装饰器了,不管是带参数的,还是装饰类的,还是用类实现的,都没问题。

不过这些还不够,下一篇我们来点实战的,看看在实际工作中,装饰器都能用来干啥,有哪些你马上就能用到的工具。

相关推荐
huangdong_20 小时前
淘宝商品SKU图自动分类技术深度解析:从DOM解析到智能归档
开发语言·javascript·ecmascript
阿正的梦工坊20 小时前
【Rust】12-借用检查器与非词法生命周期
开发语言·后端·rust
许彰午20 小时前
30_Java Stream流操作全解
java·windows·python
qq_25183645720 小时前
基于java Web网络订餐系统设计与实现 源码文档
java·开发语言·前端
秋920 小时前
3年经验Python后端转AI Engineer:3个月实战转型计划(2026版)
开发语言·人工智能·python
凡人叶枫21 小时前
Effective C++ 条款17:以独立语句将 newed 对象置入智能指针
java·linux·开发语言·c++·算法
飞天狗11121 小时前
零基础JavaWeb入门——第2课:让网页“活”起来 —— JSP是什么?
java·开发语言·前端·后端·web
2601_9563198821 小时前
期货夜盘无人值守监控什么:断线、无成交与拒单信号
python·区块链
CTA终结者21 小时前
期货量化目标仓和净持仓对不齐:天勤 TargetPosTask 与 pos 偏差排查
python·区块链
科技林总21 小时前
解决vllm服务漏扫问题
python·安全