首页 游戏 软件 资讯 排行榜 专题
首页
业界动态
我被 Python 装饰器坑了三个月,直到搞懂它的"套路"

我被 Python 装饰器坑了三个月,直到搞懂它的"套路"

热心网友
40
转载
2026-04-20

别再把装饰器当成高深莫测的高级语法来回避了

装饰器的本质,说到底就是一个函数包裹函数的技巧。想彻底掌握它,只需要搞清楚三件事:谁包裹谁、参数怎么传递、元数据怎么保留。

免费影视、动漫、音乐、游戏、小说资源长期稳定更新! 👉 点此立即查看 👈

先讲一个真实发生过的故事。

去年有个接口日志项目,需求是记录每个API的调用时间、请求参数和返回状态。团队里一位经验丰富的同事建议用装饰器,说写个@log标签就能搞定。听起来很美好,对吧?于是照着网上的文档抄了一段代码,运行——结果直接报错:TypeError: wrapper() missing 1 required positional argument

对着屏幕愣了半天。文档明明就是这样写的,问题出在哪里?

后来排查才发现,抄的那篇博客太老了,用的是Python 2的写法。到了Python 3,连个清晰的错误提示都没有,就扔给你一句“缺少参数”。这个细节差点让项目延期。也正是那次经历让人意识到,对装饰器这种工具,必须从原理到实战彻底搞清楚,不能停留在半懂不懂、照抄代码的阶段。

今天,就把那些踩过的坑、总结的经验、提炼的套路一次性讲清楚。不堆砌概念,不照本宣科,直接聚焦你在实际编码中会遇到的问题。

一、装饰器到底是什么?

很多资料会说“装饰器是一种闭包”,这话没错,但看完你可能还是不会写。

换个更直观的理解方式:装饰器就是函数的“包装膜”。

想象一下去水果店买水果。水果本身没变,但套上包装盒之后,功能就多了:可以保鲜,可以印上品牌Logo,还能加个防伪标签。装饰器干的就是这个事——在不改动原函数内部代码的前提下,为函数增添额外的功能。

最常见的应用场景有哪些?计时、登录校验、日志记录、性能监控。这些需求如果每个函数都手动写一遍,代码会变得冗长且难以维护。装饰器的价值就在于,让你只写一次核心逻辑,然后像贴标签一样应用到任何需要的地方。

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"函数 {func.__name__} 耗时 {end - start:.4f} 秒")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "Done"

slow_function()

看,slow_function本身没有任何改动,但贴上@timer之后,就自动拥有了计时能力。这就是装饰器的核心魅力——增强函数功能,但不污染函数本身的纯净性。

二、它为什么能工作?

很多人会用装饰器,但未必清楚其背后的运作机制。看懂下面这段拆解,你就能自己创造装饰器了:

def slow_function():
    return "Done"

def timer(func):
    def wrapper():
        start = time.time()
        result = func()
        end = time.time()
        print(f"耗时 {end - start:.4f} 秒")
        return result
    return wrapper

# 这一行就是 @timer 的本质
slow_function = timer(slow_function)

装饰器的语法糖@timer,其实就是上面那行手动包装代码的简写形式。

所以,装饰器的本质可以归结为:一个高阶函数,它接收一个函数作为输入,经过包装后,返回另一个函数作为输出。

三、踩坑实录:那些让新手崩溃的瞬间

1. 坑一:*args, **kwargs写错一个符号

def my_decorator(func):
    def wrapper(*args, **kwargs):  # 星号必须带
        print("开始执行")
        return func(*args, **kwargs)  # 这里也是
    return wrapper

*args写成args,或者把**kwargs写成kwargs,Python通常会报一个比较隐晦的错误。关键在于,*是解包操作。func(*args)是把元组里的每个元素拆开传给函数;而func(args)则是把整个元组当作一个参数传进去,函数签名自然就对不上了。

自检方法很简单:用签名复杂的函数来测试你的装饰器。

@my_decorator
def complex_func(a, b=10, *args, **kwargs):
    print(f"a={a}, b={b}, args={args}, kwargs={kwargs}")

complex_func(1, 2, 3, 4, name="test")

2. 坑二:装饰器带参数,写成了套娃

def log(level="INFO"):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"[{level}] 调用 {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@log("ERROR")
def run():
    pass

这里有个记忆诀窍:带参数的装饰器,本质是“工厂的工厂”。

拆解一下这三层各自负责什么:

  • 第一层log(“ERROR”)负责接收参数,并返回一个真正的装饰器函数。
  • 第二层decorator负责接收被装饰的函数。
  • 第三层wrapper才是真正包裹原函数、执行增强逻辑的地方。

所以,@log(“ERROR”)等价于run = log(“ERROR”)(run)

3. 坑三:装饰器改变了原函数的“身份”

@timer
def add(a, b):
    """两数相加,返回结果"""
    return a + b

print(add.__name__)   # 输出什么? wrapper  ← 函数名丢了
print(add.__doc__)    # 输出什么? None    ← 文档丢了

这个问题很关键。很多Web框架会根据函数名和文档字符串来生成路由或进行参数校验。元数据一旦丢失,轻则路由名变成莫名其妙的wrapper,重则导致整个应用行为异常。

解决方法只有一行代码:

import functools

def timer(func):
    @functools.wraps(func)  # 把原函数的元数据复制过来
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"耗时 {end - start:.4f} 秒")
        return result
    return wrapper

加上@functools.wraps(func)这一行,原函数的__name____doc____annotations__等元数据就全部保留下来了。这行代码没有理由不写,务必养成习惯。

4. 坑四:装饰器返回了None

def bad_decorator(func):
    def wrapper(*args, **kwargs):
        print("做一些操作")
        func(*args, **kwargs)  # 没有return!
    return wrapper

@bad_decorator
def get_data():
    return [1, 2, 3]

result = get_data()
print(result)  # 输出:None ← 返回值丢了

记住一个铁律:装饰器内部的wrapper函数,永远要return原函数的执行结果。否则,被装饰的函数就失去了返回值。

四、实战:写一个带缓存的装饰器

import functools, time

def memoize(ttl=300):
    """
    带过期时间的内存缓存装饰器
    ttl: 缓存有效期(秒),默认5分钟
    """
    cache = {}

    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            key = str(args) + str(sorted(kwargs.items()))
            now = time.time()

            if key in cache:
                val, t, expire = cache[key]
                if now - t < expire:
                    print(f"命中缓存 (剩余 {expire - (now - t):.0f}s)")
                    return val

            result = func(*args, **kwargs)
            cache[key] = (result, now, ttl)
            print("缓存未命中,执行函数")
            return result
        return wrapper
    return decorator

@memoize(ttl=10)
def fetch_api(user_id, include_history=True):
    time.sleep(2)  # 假设每次请求耗时2秒
    return {"user_id": user_id, "data": [1, 2, 3]}

data1 = fetch_api(1001)  # 耗时2秒
data2 = fetch_api(1001)  # 0秒返回,命中缓存
data3 = fetch_api(1002)  # 耗时2秒,不同参数

同样的参数在10秒内重复调用,2秒的请求瞬间变成0秒返回。这个模式稍加扩展,配合Redis,就能做成一个分布式的缓存装饰器。

五、类装饰器:不是函数也可以被装饰

1. 单例模式装饰器

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 DatabaseConnection:
    def __init__(self):
        print("创建数据库连接...")
        self.connected = True

db1 = DatabaseConnection()
db2 = DatabaseConnection()
db3 = DatabaseConnection()
print(db1 is db2 is db3)  # True,只初始化一次

虽然调用了三次DatabaseConnection(),但__init__只执行了一次。这种模式非常适合数据库连接池、全局配置管理器、线程池或日志实例等场景。

2. 路由注册装饰器

class Router:
    routes = {}

    @classmethod
    def route(cls, path):
        def decorator(func):
            cls.routes[path] = func
            print(f"注册路由: {path} -> {func.__name__}")
            return func
        return decorator

class MyApp(Router):
    @Router.route("/home")
    def home(self): return "Welcome home"

    @Router.route("/about")
    def about(self): return "About us"

print(MyApp.routes)

3. 多个装饰器的叠加顺序

@log_decorator
@timer_decorator
def process():
    time.sleep(0.1)
    return "Done"

装饰器是从下往上依次应用的。可以想象成从里到外穿衣服:先穿内衣@timer_decorator,再套外套@log_decorator

从调用角度看:process()先经过外套log_decorator,再进入内衣timer_decorator,最后才到达process函数本体。这也解释了为什么内层装饰器必须加@functools.wraps——它确保了外层装饰器看到的是真实的函数名,而不是一个wrapper

六、真实项目的两个实用场景

场景一:重试机制

import functools, time, logging
logger = logging.getLogger(__name__)

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

@retry(max_attempts=3, delay=2)
def call_api(url):
    import random
    if random.random() < 0.7:
        raise ConnectionError("网络超时")
    return "成功"

场景二:权限校验

def requires_auth(permission):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            user = get_current_user()
            if permission not in user.permissions:
                raise PermissionError(f"需要权限: {permission}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@requires_auth("admin")
def delete_user(user_id): pass

@requires_auth("finance")
def export_report(): pass

这样一来,权限校验逻辑和业务逻辑完全分离,修改一处不会影响另一处,代码的清晰度和可维护性大大提升。

七、行动框架

最后,给出一套可行的学习路径:

第一步,理解闭包的原理。装饰器背后站着的就是闭包——即函数可以记住并访问其定义时所在作用域的变量。理解了闭包,装饰器就是顺水推舟。

第二步,动手写。从最简单的@timer开始,重点跑通@functools.wraps这一步。然后尝试带参数的装饰器,再进阶到类装饰器。

第三步,读优质源码。Flask的@app.route、Django的@login_required、FastAPI的路径操作装饰器——这些都是工业级的高质量参考,能让你看到装饰器在真实项目中的优雅用法。

第四步,警惕过度封装。只有当同一段增强逻辑需要在多个地方重复使用时,才值得抽取成装饰器。如果只是某个函数里需要加一行日志,手动写一下就好。

说到底,别再把装饰器当成什么高深莫测的高级语法来回避了。它的本质就是一个函数包裹函数的技巧,搞清楚三件事就够了:谁包裹谁、参数怎么传递、元数据怎么保留。

下次在代码里再遇到@开头的符号,别下意识地跳过。花上五分钟,读一下它的实现,看看它给函数包裹了什么功能。你会发现,看懂别人的装饰器,比你想象的要简单得多。

来源:https://www.51cto.com/article/841096.html
免责声明: 游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

相关攻略

CodeGeeX怎么写Python多线程程序_CodeGeeX辅助实现并发处理逻辑【多线程编程】
AI
CodeGeeX怎么写Python多线程程序_CodeGeeX辅助实现并发处理逻辑【多线程编程】

一、使用自然语言精准提问生成多线程代码 想用自然语言直接“吩咐”CodeGeeX写出可用的多线程程序?关键在于指令要清晰、具体。你得把场景、约束和关键组件都交代清楚,模型才能准确理解你的意图,生成符合threading模块规范的代码。 具体操作起来,可以按这几步走: 1 在你已经装好CodeGee

热心网友
04.20
我被 Python 装饰器坑了三个月,直到搞懂它的"套路"
业界动态
我被 Python 装饰器坑了三个月,直到搞懂它的"套路"

别再把装饰器当成高深莫测的高级语法来回避了 装饰器的本质,说到底就是一个函数包裹函数的技巧。想彻底掌握它,只需要搞清楚三件事:谁包裹谁、参数怎么传递、元数据怎么保留。 先讲一个真实发生过的故事。 去年有个接口日志项目,需求是记录每个API的调用时间、请求参数和返回状态。团队里一位经验丰富的同事建议用

热心网友
04.20
CodeGeeX怎么写Python数据分组代码_CodeGeeX快速实现按条件分类逻辑【数据分组】
AI
CodeGeeX怎么写Python数据分组代码_CodeGeeX快速实现按条件分类逻辑【数据分组】

Python数据分组:五种实用方法详解 在数据处理中,按条件分组是个高频需求。无论是分析用户行为,还是整理业务报表,都离不开它。面对复杂的数据结构或多变的分组逻辑,借助CodeGeeX这类AI编码工具,能快速生成可运行的代码片段,确实能提升效率。那么,Python里到底有哪些好用的分组方法呢?下面这

热心网友
04.20
从正则到 BERT详解Python如何判断文本是否为标题
编程语言
从正则到 BERT详解Python如何判断文本是否为标题

在文档解析(如处理PDF Word文件)或清洗用户生成内容(UGC)时,一个普遍的技术难题是:如何让计算机程序像人类一样,快速准确地从一段文本中识别出标题和正文? 例如,面对以下文本: “2023年全球AI市场规模达到1000亿美元。根据最新报告,增长主要来自生成式AI。” 人类可以立即判断第一句是

热心网友
04.20
TensorFlow模型训练卡住怎么办_Python监控显存与CPU利用率
编程语言
TensorFlow模型训练卡住怎么办_Python监控显存与CPU利用率

先看nvidia-smi和htop比改代码更管用:若GPU-Util长期为0%但Memory-Usage占满,说明GPU在等数据;若GPU-Util持续95%+却无日志输出,可能是Python主线程在map中卡住;同步用htop观察CPU,单核100%锁死且GPU空闲则大概率是map内阻塞逻辑导致。

热心网友
04.18

最新APP

宝宝过生日
宝宝过生日
应用辅助 04-07
台球世界
台球世界
体育竞技 04-07
解绳子
解绳子
休闲益智 04-07
骑兵冲突
骑兵冲突
棋牌策略 04-07
三国真龙传
三国真龙传
角色扮演 04-07

热门推荐

macOS 系统安装 WorkBuddy 提示“已损坏”的解决方法
AI
macOS 系统安装 WorkBuddy 提示“已损坏”的解决方法

双击WorkBuddy app提示“已损坏”实为macOS Gatekeeper拦截:一、右键选择“打开”后点“仍要打开”可临时放行;二、终端执行sudo xattr -r -d com apple quarantine Applications WorkBuddy app清除隔离属性;三、sud

热心网友
04.20
Smartrip
AI
Smartrip

Smartrip 是什么 谈起智能旅行规划,市面上工具不少,但真正能做到从想到出发全程“包办”的却不多。今天要聊的这款 Smartrip,就属于那种能彻底解放你行前准备精力的AI助手。它由 Adeva 团队开发,核心能力在于运用智能算法,深度理解你的个人偏好,然后从海量选项中筛选出最佳的旅行方案并完

热心网友
04.20
2026 小巧便携的充电宝推荐:轻若无物的续航神器,这五款揣兜就走
业界动态
2026 小巧便携的充电宝推荐:轻若无物的续航神器,这五款揣兜就走

小巧便携的充电宝:轻若无物的续航神器,这五款揣兜就走 说到小巧便携的充电宝,大家脑海里浮现的,恐怕就是那些厚度在15毫米以内、重量不超过250克,能轻松塞进牛仔裤口袋或随身小包的“能量块”了。它们精准地解决了传统大容量充电宝“出门像带块砖”的尴尬,让移动补电真正变得轻松。市场数据也印证了这一趋势:根

热心网友
04.20
币安交易所官网最新入口 币安App v8.8.5版官方下载链接
web3.0
币安交易所官网最新入口 币安App v8.8.5版官方下载链接

币安交易所官网最新入口在哪里? 最近,不少朋友都在打听同一个问题:币安交易所的官网最新入口到底在哪儿?别急,这篇文章就来为大家梳理清楚,顺便带你深入了解一下这个平台的核心机制与最新动态。 币安Binance官网直达入口: 币安官方认证App下载包: 平台资产安全保障机制 说到交易平台,安全永远是用户

热心网友
04.20
MATIC今日价格在哪看_如何币安Binance上查看MATIC实时报价
web3.0
MATIC今日价格在哪看_如何币安Binance上查看MATIC实时报价

如何查看MATIC实时价格?五种官方渠道详解 可通过官网、App、行情页、首页组件或API五种方式查看MATIC USDT实时价格:登录后进入现货交易区查深度图与最新价;行情页看涨跌幅与K线;App首页添加价格小组件;开发者调用API获取毫秒级报价。 一、访问币安Binance官网或App主界面 首

热心网友
04.20