Python 装饰器与元编程实战

装饰器是 Python 最优雅也最容易被误解的特性之一。它本质上是一个接收函数并返回新函数的高阶函数,却能让你在不修改原代码的前提下给函数加上日志、缓存、权限校验等横切逻辑。本文从最基础的函数装饰器讲起,逐步深入到带参数装饰器、类装饰器,最后触及元编程与元类的核心机制。

Python 装饰器配图:蓝色渐变背景上嵌套立方体层层堆叠,层间齿轮连接,数据流穿层而过,扁平科技插画
装饰器就像给函数套上一层层包装,每层负责一项横切关注点

一、装饰器到底是什么

很多人第一次见到 @decorator 语法时会觉得它是某种魔法。其实它只是语法糖:@decorator 写在函数定义上方,等价于在函数定义之后执行 func = decorator(func)。也就是说,装饰器接收原函数作为参数,返回一个新函数替换掉原函数。

下面这段代码用最直白的方式展示了装饰器的等价关系:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"调用 {func.__name__} 前")
        result = func(*args, **kwargs)
        print(f"调用 {func.__name__} 后")
        return result
    return wrapper

@my_decorator
def greet(name):
    return f"你好,{name}"

# 等价于:
# greet = my_decorator(greet)

print(greet("世界"))
# 输出:
# 调用 greet 前
# 调用 greet 后
# 你好,世界

关键点在于 wrapper 函数用 *args, **kwargs 接收任意参数,再透传给原函数,这样装饰器就可以套在任何签名的函数上而不会报错。

易错提醒:直接写 wrapper 会导致原函数的元信息丢失。greet.__name__ 会变成 "wrapper" 而不是 "greet"。用 functools.wraps 装饰 wrapper 就能保留原函数元信息,这在调试和生成文档时非常重要。

二、带参数的装饰器

当你需要给装饰器本身传参数时(比如 @repeat(times=3)),就需要再包一层。原理是:@repeat(times=3) 先执行 repeat(times=3),它返回一个真正的装饰器,然后这个装饰器再去装饰目标函数。

import functools

def repeat(times=2):
    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_hi():
    print("嗨")

say_hi()
# 打印三行「嗨」

三层嵌套结构从外到内分别是:参数层、装饰器层、包装层。理解了这个三层结构,你就掌握了 Python 装饰器的核心模式。

三、实际场景:日志与缓存装饰器

装饰器最常见的用途是横切关注点。下面是两个可以直接用的实战例子。

日志装饰器:自动记录函数调用的参数和耗时,不需要在每个函数里手写 print。

import time, functools

def log_call(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"[{func.__name__}] 参数={args} 耗时={elapsed:.4f}s")
        return result
    return wrapper

@log_call
def fetch_data(url):
    time.sleep(0.1)
    return {"url": url, "data": "ok"}

fetch_data("https://api.example.com")
# [fetch_data] 参数=('https://api.example.com',) 耗时=0.1002s

缓存装饰器:functools.lru_cache 是 Python 内置的缓存装饰器,对于计算密集且参数重复的函数非常有效。

from functools import lru_cache

@lru_cache(maxsize=128)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(50))  # 极快返回,因为之前的调用结果被缓存了
print(fib.cache_info())
# CacheInfo(hits=49, misses=51, maxsize=128, currsize=51)

四、类装饰器

装饰器不仅能装饰函数,还能装饰类。类装饰器接收一个类作为参数,返回一个新类替换原类。它常用于给类统一添加日志、单例模式、属性校验等行为。

import functools

def add_repr(cls):
    def __repr__(self):
        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 User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

print(User("小明", 25))
# User(name='小明', age=25)

类装饰器还可以用来实现单例模式:

def singleton(cls):
    instances = {}
    @functools.wraps(cls)
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class Config:
    def __init__(self):
        self.debug = True

c1 = Config()
c2 = Config()
print(c1 is c2)  # True

五、元类初探:控制类的创建

元类是 Python 元编程的终极武器。普通的类是创建对象的模板,而元类是创建类的模板。你可以把它理解为:类是元类的实例。默认情况下 Python 用 type 作为所有类的元类,但你可以继承 type 来自定义类的创建过程。

class UpperAttrMeta(type):
    def __new__(cls, name, bases, namespace):
        # 把类中所有非下划线开头的属性名转成大写
        new_namespace = {}
        for key, value in namespace.items():
            if not key.startswith("_"):
                new_namespace[key.upper()] = value
            else:
                new_namespace[key] = value
        return super().__new__(cls, name, bases, new_namespace)

class MyClass(metaclass=UpperAttrMeta):
    name = "测试"
    version = "1.0"

print(MyClass.NAME)      # 测试
print(MyClass.VERSION)    # 1.0
# MyClass.name 会报 AttributeError,因为属性名被大写了

元类最典型的应用场景是 ORM 框架(如 Django 的 Model):在类创建时自动把字段定义转换成数据库列映射。但日常业务开发中,大部分需求用装饰器就能解决,元类属于"知道就行、慎用"的高级特性。

设计建议:优先用装饰器解决横切关注点(日志、缓存、权限、重试),用类装饰器解决类级别行为(单例、自动注册),元类只在框架级设计时才考虑。过度使用元类会让代码难以理解和调试。

六、常见坑与调试技巧

坑一:忘记 functools.wraps。这会导致所有被装饰函数的 __name__ 都变成 wrapper,调试时堆栈信息完全混乱。养成习惯,每个 wrapper 上面都加 @functools.wraps(func)。

坑二:装饰器在导入时就执行。因为 @decorator 在函数定义时就被调用了,如果你在装饰器里做了重活(连接数据库、发网络请求),它会在模块导入时立刻执行,拖慢启动。把重活放到 wrapper 内部。

坑三:堆叠装饰器的顺序。@a 在 @b 上面,等价于 func = a(b(func)),先执行 b 再执行 a。搞反顺序会导致日志输出不符合预期。

def a(func):
    def w(*args, **kw):
        print("a 前")
        r = func(*args, **kw)
        print("a 后")
        return r
    return w

def b(func):
    def w(*args, **kw):
        print("b 前")
        r = func(*args, **kw)
        print("b 后")
        return r
    return w

@a
@b
def hello():
    print("hello")

hello()
# 输出:
# a 前
# b 前
# hello
# b 后
# a 后

七、总结

装饰器是 Python 里"小语法、大威力"的典型代表。一个 @ 符号背后,是高阶函数、闭包和语法糖的精巧组合。从给函数加日志,到用元类控制类的创建,理解装饰器的执行顺序和本质机制,你就能写出更优雅、更可维护的 Python 代码。记住三个原则:用 wraps 保留元信息、把重活放到 wrapper 里、能不用元类就不用元类。