036、装饰器三部曲(一):无参装饰器的原理与 10 个实用案例

036、装饰器三部曲(一):无参装饰器的原理与 10 个实用案例

上周帮一个同事排查线上接口响应时间异常的问题。他写了一个统计函数执行时间的装饰器,结果发现每次调用都多打印了一行“None”,而且时间统计完全不准。我一看代码,他直接在装饰器内部调用了被装饰函数,却没有返回结果——典型的“装饰器新手翻车现场”。这个场景让我决定把装饰器这个主题拆成三篇来讲,今天先搞定最基础的无参装饰器。

装饰器到底是个什么东西

别被“装饰器”这个名字唬住。本质上,装饰器就是一个接受函数作为参数、返回一个新函数的函数。Python里函数是一等公民,可以像变量一样传来传去,这是装饰器能工作的根基。

看一个最朴素的例子:

def my_decorator(func):
    def wrapper():
        print("调用前,这里可以加日志")
        func()  # 这里踩过坑:别忘了调用原函数
        print("调用后,这里可以加清理逻辑")
    return wrapper

def say_hello():
    print("你好,世界")

say_hello = my_decorator(say_hello)
say_hello()

输出:

调用前,这里可以加日志
你好,世界
调用后,这里可以加清理逻辑

这就是装饰器的原始形态——手动把函数传进去,再手动把新函数赋回去。Python的@语法糖只是帮我们省掉了最后两行赋值操作。

@语法糖是怎么工作的

@改写上面的例子:

def my_decorator(func):
    def wrapper():
        print("调用前")
        func()
        print("调用后")
    return wrapper

@my_decorator
def say_hello():
    print("你好,世界")

say_hello()

@my_decorator等价于执行了say_hello = my_decorator(say_hello)。注意这个执行时机——它发生在函数定义时,而不是函数调用时。很多新手以为装饰器每次调用都会执行,其实只在模块加载时执行一次。

被装饰函数的元信息丢失问题

用上面的装饰器装饰后,say_hello.__name__会变成wrapper而不是say_hello。这在调试和文档生成时很坑。

print(say_hello.__name__)  # 输出: wrapper

解决方案是用functools.wraps

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("调用前")
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def say_hello():
    print("你好,世界")

print(say_hello.__name__)  # 输出: say_hello

@functools.wraps(func)会把原函数的__name____doc____module__等属性复制到wrapper上。别这样写——不写@functools.wraps,除非你明确知道自己在做什么。

10个实用案例

案例1:函数执行时间统计

import time
import functools

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} 执行耗时: {end - start:.4f}秒")
        return result  # 这里踩过坑:不return的话调用方拿不到结果
    return wrapper

@timer
def slow_function():
    time.sleep(0.5)
    return "完成"

result = slow_function()
print(result)  # 输出: 完成

案例2:日志记录

import functools
import logging

logging.basicConfig(level=logging.INFO)

def log_call(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        logging.info(f"调用 {func.__name__},参数: {args}, {kwargs}")
        try:
            result = func(*args, **kwargs)
            logging.info(f"{func.__name__} 返回: {result}")
            return result
        except Exception as e:
            logging.error(f"{func.__name__} 抛出异常: {e}")
            raise
    return wrapper

@log_call
def divide(a, b):
    return a / b

divide(10, 2)  # 正常调用
divide(10, 0)  # 异常调用

案例3:访问控制与权限校验

import functools

def require_admin(func):
    @functools.wraps(func)
    def wrapper(user, *args, **kwargs):
        if not getattr(user, 'is_admin', False):
            raise PermissionError("只有管理员才能执行此操作")
        return func(user, *args, **kwargs)
    return wrapper

class User:
    def __init__(self, name, is_admin=False):
        self.name = name
        self.is_admin = is_admin

@require_admin
def delete_user(admin_user, user_id):
    print(f"管理员 {admin_user.name} 删除了用户 {user_id}")

admin = User("Alice", is_admin=True)
normal = User("Bob")

delete_user(admin, 123)  # 正常
delete_user(normal, 456)  # 抛出异常

案例4:缓存计算结果

import functools

def cache_result(func):
    cache = {}
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        key = (args, tuple(sorted(kwargs.items())))
        if key in cache:
            print(f"命中缓存: {key}")
            return cache[key]
        result = func(*args, **kwargs)
        cache[key] = result
        return result
    return wrapper

@cache_result
def expensive_calc(n):
    print(f"正在计算 {n}...")
    return n * n

print(expensive_calc(5))  # 计算
print(expensive_calc(5))  # 命中缓存

案例5:重试机制

import functools
import time
import random

def retry(max_attempts=3, delay=1):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts:
                        raise
                    print(f"第{attempt}次失败: {e}{delay}秒后重试")
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5)
def unstable_api():
    if random.random() < 0.7:
        raise ConnectionError("网络不稳定")
    return "成功"

print(unstable_api())

案例6:输入参数校验

import functools

def validate_positive(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        for i, arg in enumerate(args):
            if isinstance(arg, (int, float)) and arg < 0:
                raise ValueError(f"参数 {i} 必须为正数,收到 {arg}")
        for key, value in kwargs.items():
            if isinstance(value, (int, float)) and value < 0:
                raise ValueError(f"参数 {key} 必须为正数,收到 {value}")
        return func(*args, **kwargs)
    return wrapper

@validate_positive
def sqrt_approx(x):
    return x ** 0.5

print(sqrt_approx(9))  # 正常
print(sqrt_approx(-1))  # 抛出异常

案例7:单例模式

import functools

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 Database:
    def __init__(self):
        print("初始化数据库连接")

db1 = Database()
db2 = Database()
print(db1 is db2)  # 输出: True

案例8:函数注册表

import functools

plugins = {}

def register_plugin(name):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        plugins[name] = wrapper
        return wrapper
    return decorator

@register_plugin("format_json")
def format_json(data):
    import json
    return json.dumps(data, indent=2)

@register_plugin("format_yaml")
def format_yaml(data):
    return str(data)

print(plugins.keys())  # 输出: dict_keys(['format_json', 'format_yaml'])

案例9:调试与堆栈追踪

import functools
import traceback

def debug_call(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"进入 {func.__name__}")
        try:
            result = func(*args, **kwargs)
            print(f"离开 {func.__name__},返回 {result}")
            return result
        except Exception:
            print(f"{func.__name__} 异常:")
            traceback.print_exc()
            raise
    return wrapper

@debug_call
def buggy_function(x):
    return 1 / x

buggy_function(0)

案例10:限流控制

import functools
import time

def rate_limit(max_calls=5, period=10):
    calls = []
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()
            calls[:] = [t for t in calls if now - t < period]
            if len(calls) >= max_calls:
                raise RuntimeError(f"超过限流限制: {period}秒内最多{max_calls}次")
            calls.append(now)
            return func(*args, **kwargs)
        return wrapper
    return decorator

@rate_limit(max_calls=3, period=5)
def api_call():
    print("API调用成功")

for _ in range(4):
    try:
        api_call()
    except RuntimeError as e:
        print(e)
    time.sleep(0.5)

个人经验性建议

  1. 永远在装饰器内部使用@functools.wraps。我见过太多因为没加这个导致调试时函数名混乱的案例,特别是当你用inspect模块或者第三方库做自动化测试时,这个问题会直接炸掉。

  2. 装饰器内部的wrapper函数一定要返回原函数的返回值。这是同事踩坑最多的地方——装饰器里调用了原函数,但没return结果,调用方拿到的永远是None

  3. 注意装饰器的执行顺序。多个装饰器叠加时,执行顺序是从下往上(离函数定义最近的先执行),但调用顺序是从上往下。这个反直觉的设计让很多人翻车,建议先写一个简单的测试验证。

  4. 不要滥用装饰器。装饰器本质上是元编程,过度使用会让代码难以追踪。我一般只在横切关注点(日志、缓存、权限)上使用,业务逻辑尽量保持干净。

  5. 调试装饰器时,直接在wrapper函数里加print或者用pdb.set_trace()。别想着一步到位,装饰器的执行流程和普通函数不一样,逐行打印是最快的方式。

下一篇会讲带参数的装饰器,以及如何用类实现装饰器。如果你在写装饰器时遇到过什么奇葩bug,欢迎在评论区分享,说不定下一篇就会拿你的案例来讲解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值