Python 装饰器

一、装饰器是什么

装饰器本质上是一个“接收函数,返回新函数”的函数。

它常用于在不修改原函数代码的前提下,给函数额外增加能力,例如:

  1. 打印日志
  2. 统计耗时
  3. 权限校验
  4. 参数检查
  5. 缓存结果
  6. 统一异常处理

装饰器语法:

@decorator
def func():
    pass

等价于:

def func():
    pass
 
func = decorator(func)

二、函数也是对象

理解装饰器前,先理解:函数可以像普通变量一样传递。

def hello():
    print("hello")
 
fn = hello
fn()
# output:hello

函数也可以作为另一个函数的参数:

def call_func(func):
    func()
 
def hello():
    print("hello")
 
call_func(hello)
# output:hello

三、最简单的装饰器

def trace(func):
    def wrapper():
        print("函数调用前")
        func()
        print("函数调用后")
 
    return wrapper
 
 
@trace
def hello():
    print("hello")
 
 
hello()

执行过程:

@trace
def hello():
    ...
 
等价于:
hello = trace(hello)

所以最后调用的 hello(),其实已经不是原来的 hello(),而是 trace 返回的 wrapper()

四、装饰带参数的函数

如果原函数有参数,内部的 wrapper 也要接收参数。

def trace(func):
    def wrapper(name):
        print("函数调用前")
        result = func(name)
        print("函数调用后")
        return result
 
    return wrapper
 
 
@trace
def hello(name):
    return f"hello {name}"
 
 
print(hello("Tom"))

更通用的写法是使用 *args**kwargs

def trace(func):
    def wrapper(*args, **kwargs):
        print(f"开始调用: {func.__name__}")
        result = func(*args, **kwargs)
        print(f"结束调用: {func.__name__}")
        return result
 
    return wrapper

这样无论原函数有多少参数,都可以正常转发。

五、保留原函数信息

普通装饰器会让函数名、文档等元信息变成 wrapper

推荐使用 functools.wraps

from functools import wraps
 
 
def trace(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"call {func.__name__}")
        return func(*args, **kwargs)
 
    return wrapper

这是写装饰器时很重要的习惯。

六、带参数的装饰器

有时装饰器本身也需要参数,例如控制日志等级、重复次数、权限角色。

带参数装饰器有三层函数:

from functools import wraps
 
 
def repeat(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
 
        return wrapper
 
    return decorator
 
 
@repeat(3)
def hello(name):
    print(f"hello {name}")
 
 
hello("Tom")

执行关系:

@repeat(3)
def hello(...):
    ...
 
等价于:
hello = repeat(3)(hello)

七、多个装饰器叠加

@decorator_a
@decorator_b
def func():
    pass

等价于:

func = decorator_a(decorator_b(func))

距离函数最近的装饰器先包裹,调用时最外层装饰器先执行。

from functools import wraps
 
 
def outer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("outer before")
        result = func(*args, **kwargs)
        print("outer after")
        return result
 
    return wrapper
 
 
def inner(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("inner before")
        result = func(*args, **kwargs)
        print("inner after")
        return result
 
    return wrapper
 
 
@outer
@inner
def hello():
    print("hello")
 
 
hello()

输出顺序:

outer before
inner before
hello
inner after
outer after

八、类装饰器

类也可以实现装饰器,核心是 __call__ 方法。

from functools import wraps
 
 
class Trace:
    def __init__(self, func):
        self.func = func
        wraps(func)(self)
 
    def __call__(self, *args, **kwargs):
        print(f"call {self.func.__name__}")
        return self.func(*args, **kwargs)
 
 
@Trace
def add(a, b):
    return a + b
 
 
print(add(1, 2))

函数装饰器更常见,类装饰器适合需要保存状态的场景。

九、常见内置装饰器

property

把方法包装成属性访问。

class Student:
    def __init__(self, name, score):
        self.name = name
        self.score = score
 
    @property
    def passed(self):
        return self.score >= 60
 
 
student = Student("Tom", 80)
print(student.passed)

staticmethod

静态方法不接收 self,通常用于和类相关但不依赖对象状态的工具函数。

class MathTool:
    @staticmethod
    def add(a, b):
        return a + b
 
 
print(MathTool.add(1, 2))

classmethod

类方法接收 cls,常用于工厂方法。

class User:
    def __init__(self, name):
        self.name = name
 
    @classmethod
    def from_dict(cls, data):
        return cls(data["name"])
 
 
user = User.from_dict({"name": "Tom"})
print(user.name)

十、常见应用案例

计时器

from functools import wraps
from time import perf_counter
 
 
def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = perf_counter()
        result = func(*args, **kwargs)
        end = perf_counter()
        print(f"{func.__name__} cost {end - start:.6f}s")
        return result
 
    return wrapper

简单权限检查

from functools import wraps
 
 
def require_role(role):
    def decorator(func):
        @wraps(func)
        def wrapper(user, *args, **kwargs):
            if user.get("role") != role:
                raise PermissionError("permission denied")
            return func(user, *args, **kwargs)
 
        return wrapper
 
    return decorator

十一、容易踩的坑

忘记 return 原函数结果

错误写法:

def trace(func):
    def wrapper(*args, **kwargs):
        print("call")
        func(*args, **kwargs)
 
    return wrapper

如果原函数有返回值,这种写法会导致返回 None

正确写法:

def trace(func):
    def wrapper(*args, **kwargs):
        print("call")
        return func(*args, **kwargs)
 
    return wrapper

忘记使用 wraps

from functools import wraps

写通用装饰器时,建议总是加 @wraps(func),避免函数名、文档字符串、类型提示等信息丢失。

wrapper 参数写死

如果装饰器只写:

def wrapper():
    return func()

那么它只能装饰无参数函数。通用装饰器应写成:

def wrapper(*args, **kwargs):
    return func(*args, **kwargs)

十二、学习建议

  1. 先理解“函数可以作为参数和返回值”。
  2. 再理解 func = decorator(func)
  3. 普通装饰器熟练后,再学带参数装饰器。
  4. 写实际代码时优先使用 functools.wraps
  5. 装饰器不要写得过重,否则会隐藏真实逻辑。

十三、案例

py装饰器案例

Python 装饰器案例

下面这段代码可以直接复制到一个 .py 文件中运行,覆盖普通装饰器、计时装饰器、带参数装饰器、权限检查和内置装饰器。

from functools import wraps
from time import perf_counter, sleep
 
 
def trace(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"call {func.__name__}")
        return func(*args, **kwargs)
 
    return wrapper
 
 
def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = perf_counter()
        result = func(*args, **kwargs)
        end = perf_counter()
        print(f"{func.__name__} cost {end - start:.6f}s")
        return result
 
    return wrapper
 
 
def repeat(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
 
        return wrapper
 
    return decorator
 
 
def require_role(role):
    def decorator(func):
        @wraps(func)
        def wrapper(user, *args, **kwargs):
            if user.get("role") != role:
                raise PermissionError("permission denied")
            return func(user, *args, **kwargs)
 
        return wrapper
 
    return decorator
 
 
@trace
def add(a, b):
    return a + b
 
 
@timer
def slow_task():
    sleep(0.01)
    return "done"
 
 
@repeat(3)
def say_hi(name):
    print(f"hi {name}")
 
 
@require_role("admin")
def delete_post(user, post_id):
    return f"user {user['name']} deleted post {post_id}"
 
 
class Student:
    def __init__(self, name, score):
        self.name = name
        self.score = score
 
    @property
    def passed(self):
        return self.score >= 60
 
    @classmethod
    def from_dict(cls, data):
        return cls(data["name"], data["score"])
 
    @staticmethod
    def is_valid_score(score):
        return 0 <= score <= 100
 
 
def main():
    print(add(1, 2))
    print(slow_task())
    say_hi("Tom")
 
    admin = {"name": "Lucy", "role": "admin"}
    print(delete_post(admin, 1001))
 
    student = Student.from_dict({"name": "Jack", "score": 72})
    print(student.name, student.passed)
    print(Student.is_valid_score(101))
 
 
if __name__ == "__main__":
    main()
指向原始笔记的链接