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

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

你有没有好奇过,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}")

是不是很灵活?


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

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

相关推荐
观远数据7 小时前
经营闭环的最后一公里:从看见问题到解决问题之间还差什么
java·开发语言·数据库
Java面试题总结7 小时前
Go 语言大白话入门 6 - 判断与循环
开发语言·后端·golang
m0_617493947 小时前
Python OpenCV 孔洞检测与缺陷分析详解与实战
开发语言·python·opencv
hsg778 小时前
简述:Rust、GeoRust、自主研发GIS平台
开发语言·后端·rust
承渊政道9 小时前
飞算Java炫技赛:Java×Unity数字孪生园区平台的从零落地记录
java·开发语言·unity·技术分享·vibe coding·飞算javaai·飞算java ai炫技赛
wdfk_prog15 小时前
嵌入式面试真题第 10 题:高优化等级下共享状态可见性、内存模型与系统级同步设计
java·linux·开发语言·面试·职场和发展·架构·c
闻道且行之17 小时前
TurboOCR:基于PP-OCRv6的极速Windows离线OCR工具,深度解析3.4GB依赖背后的技术架构
c++·人工智能·python·qt·机器学习·ocr
许彰午18 小时前
95_Python内存管理与垃圾回收
开发语言·python
多加点辣也没关系18 小时前
JavaScript|第13章:原始类型的方法
开发语言·javascript·ecmascript
এ慕ོ冬℘゜18 小时前
深入理解 JavaScript 事件体系:Window、鼠标与键盘事件详解
开发语言·javascript·okhttp