0

0

python如何实现一个装饰器_python装饰器原理与实现方法详解

穿越時空

穿越時空

发布时间:2025-09-14 22:55:01

|

459人浏览过

|

来源于php中文网

原创

Python装饰器利用函数为一等公民和闭包特性,通过@语法为函数添加功能而不修改其代码。如log_calls装饰器可记录函数调用日志,核心是外部函数返回嵌套的wrapper函数,wrapper保留对原函数的引用并扩展行为。functools.wraps确保被装饰函数的元信息不变。带参数的装饰器需多一层函数嵌套,形成“装饰器工厂”,如timer(unit)返回真正的装饰器。类也可作为装饰器,通过实现__call__方法,在实例中保存状态,适用于需维护调用次数或共享资源的场景,如CallCounter统计函数调用次数。

python如何实现一个装饰器_python装饰器原理与实现方法详解

Python装饰器,说白了,就是一种特殊的函数,它的主要工作是去“包裹”或者说“包装”另一个函数,给这个被包装的函数增加额外的功能,但又不需要我们去直接修改被包装函数的源代码。这听起来有点像给一个礼物盒外面再套一层包装纸,里面的礼物(原函数)还是那个礼物,但外面的包装纸(装饰器)给它增添了新的“仪式感”或者说“功能”。它的核心原理,其实就是利用了Python中函数是“一等公民”的特性,以及闭包(closure)的概念,通过

@
这个语法糖,让代码变得非常简洁和易读。

解决方案

要实现一个装饰器,我们通常会定义一个外部函数,这个外部函数接收一个函数作为参数(也就是我们要装饰的那个函数)。在外部函数内部,我们再定义一个嵌套函数(通常命名为

wrapper
),这个
wrapper
函数才是真正执行额外逻辑的地方,它会调用原始函数,并在调用前后做一些事情。最后,外部函数会返回这个
wrapper
函数。

举个最常见的例子,我们想给一个函数加上日志功能,记录它被调用的时间和参数:

import time
import functools

def log_calls(func):
    """
    一个简单的日志装饰器,记录函数调用。
    """
    @functools.wraps(func) # 这一行很重要,它保留了原函数的元信息
    def wrapper(*args, **kwargs):
        print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] 调用函数: {func.__name__},参数: {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] 函数 {func.__name__} 执行完毕,返回: {result}")
        return result
    return wrapper

@log_calls
def add(a, b):
    """计算两个数的和"""
    time.sleep(0.1) # 模拟耗时操作
    return a + b

@log_calls
def greet(name, greeting="Hello"):
    """向指定名字的人打招呼"""
    return f"{greeting}, {name}!"

# 调用被装饰的函数
print(f"结果: {add(10, 20)}")
print(f"结果: {greet('Alice', greeting='Hi')}")

这里,

log_calls
就是我们的装饰器。当我们在
add
函数上方写上
@log_calls
时,Python解释器实际上做了这样的事情:
add = log_calls(add)
。也就是说,
add
这个变量现在指向的不再是原来的
add
函数,而是
log_calls
函数返回的那个
wrapper
函数。当调用
add(10, 20)
时,实际上是调用了
wrapper(10, 20)
wrapper
内部再调用原始的
add
函数。
functools.wraps
的使用是为了让装饰后的函数仍然保持原函数的名称、文档字符串等元信息,这在调试和使用帮助文档时非常有用,不然你看到的函数名就都是
wrapper
了,那可就太让人困惑了。

立即学习Python免费学习笔记(深入)”;

Python装饰器背后的魔法:闭包与函数作为一等公民

在我看来,要真正理解装饰器,就得先搞明白Python里“函数是第一类对象(First-Class Citizen)”这个概念,以及“闭包(Closure)”是什么。这不光是装饰器的基石,也是Python很多高级特性的核心。

函数作为一等公民,意味着函数在Python里和整数、字符串这些数据类型没什么两样。你可以:

  1. 把函数赋值给变量:
    my_func = add
  2. 把函数作为参数传给另一个函数:
    map(str, [1, 2, 3])
  3. 把函数作为另一个函数的返回值:这正是装饰器里外部函数返回
    wrapper
    的关键。
  4. 把函数存储在数据结构里(比如列表或字典)。

而闭包,则是在一个函数内部定义了另一个函数,并且内部函数引用了外部函数的局部变量,当外部函数执行完毕并返回内部函数时,即使外部函数的执行环境已经销毁,内部函数仍然能够“记住”并访问外部函数的那些局部变量。

在我们的

log_calls
例子里:

AIBox 一站式AI创作平台
AIBox 一站式AI创作平台

AIBox365一站式AI创作平台,支持ChatGPT、GPT4、Claue3、Gemini、Midjourney等国内外大模型

下载
  • log_calls
    是外部函数。
  • func
    (也就是被装饰的
    add
    greet
    )是
    log_calls
    的局部变量。
  • wrapper
    是内部函数,它引用了外部函数的局部变量
    func
  • log_calls
    执行完毕并返回
    wrapper
    时,
    wrapper
    就形成了一个闭包,它“捕获”了
    func
    这个变量。所以,即使
    log_calls
    已经执行完了,
    wrapper
    在被调用时依然知道它应该去调用哪个原始函数。

这种机制非常强大,它允许我们在不修改原函数代码的前提下,对其行为进行扩展。这在很多场景下都极其有用,比如权限验证、缓存、性能监控、事务管理等等,都是典型的“横切关注点”,用装饰器来处理简直是天作之合。

如何编写带参数的装饰器?

有时候,我们希望装饰器本身也能接受一些配置参数,比如一个日志装饰器,我们可能想指定日志级别,或者一个权限装饰器,我们想指定需要的角色。这时,我们的装饰器就需要变成一个“装饰器工厂”,也就是说,一个函数,它接收参数,然后返回一个真正的装饰器。

这个模式会多一层嵌套,看起来可能会有点绕,但理解了前面闭包的概念,这也就水到渠成了。

import time
import functools

def timer(unit="seconds"):
    """
    一个带参数的计时装饰器,可以指定时间单位。
    unit: 'seconds', 'milliseconds', 'microseconds'
    """
    def decorator(func): # 这才是真正的装饰器
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            start_time = time.perf_counter()
            result = func(*args, **kwargs)
            end_time = time.perf_counter()
            duration = end_time - start_time

            if unit == "milliseconds":
                duration *= 1000
                unit_str = "ms"
            elif unit == "microseconds":
                duration *= 1_000_000
                unit_str = "μs"
            else:
                unit_str = "s"

            print(f"函数 {func.__name__} 执行耗时: {duration:.4f} {unit_str}")
            return result
        return wrapper
    return decorator

@timer(unit="milliseconds") # 这里传递了参数
def complex_calculation(n):
    """模拟一个复杂的计算"""
    total = 0
    for i in range(n):
        total += i * i
    time.sleep(0.05) # 额外模拟一点IO耗时
    return total

@timer() # 不传参数时,使用默认单位
def simple_task():
    """一个简单的任务"""
    time.sleep(0.02)
    return "Task Done"

print(f"计算结果: {complex_calculation(100000)}")
print(f"任务状态: {simple_task()}")

这里,

timer
函数就是那个“装饰器工厂”。它接收
unit
参数,然后返回
decorator
函数。
decorator
函数才是我们熟悉的那个接收函数作为参数并返回
wrapper
的结构。当写
@timer(unit="milliseconds")
时,Python解释器首先调用
timer("milliseconds")
,这会返回
decorator
函数。然后,这个返回的
decorator
函数再被用来装饰
complex_calculation
,等价于
complex_calculation = decorator(complex_calculation)
。这样,
unit
这个参数就被
decorator
wrapper
形成的闭包“捕获”了,可以在
wrapper
内部使用。

深入探索:类装饰器与更灵活的状态管理

除了函数装饰器,Python还允许我们使用类来作为装饰器。类装饰器在某些场景下,比如需要维护状态、或者需要更复杂的初始化逻辑时,会显得更加直观和强大。

一个类要作为装饰器,最核心的一点是它需要实现

__call__
方法。这样,类的实例就可以像函数一样被调用。当类被用作装饰器时,
@ClassName
实际上是创建了
ClassName
的一个实例,然后用这个实例来替换被装饰的函数。

import time
import functools

class CallCounter:
    """
    一个类装饰器,用于统计函数被调用的次数。
    """
    def __init__(self, func):
        # 初始化时,接收被装饰的函数
        functools.update_wrapper(self, func) # 同样保留原函数元信息
        self.func = func
        self.count = 0 # 维护调用次数的状态

    def __call__(self, *args, **kwargs):
        # 当被装饰的函数被调用时,实际上是调用了__call__方法
        self.count += 1
        print(f"函数 {self.func.__name__} 已被调用 {self.count} 次。")
        return self.func(*args, **kwargs)

@CallCounter
def calculate_sum(a, b):
    """计算和"""
    return a + b

@CallCounter
def say_hello(name):
    """打招呼"""
    return f"Hello, {name}!"

# 调用被装饰的函数
print(calculate_sum(1, 2))
print(calculate_sum(3, 4))
print(say_hello("World"))
print(calculate_sum(5, 6))

这里,

CallCounter
类被用作装饰器。当
@CallCounter
作用于
calculate_sum
时,Python解释器会执行
calculate_sum = CallCounter(calculate_sum)
。这意味着
calculate_sum
现在不再是原来的函数,而是
CallCounter
类的一个实例。当后续调用
calculate_sum(1, 2)
时,实际上是调用了这个实例的
__call__
方法,从而实现了计数和原始函数调用的逻辑。

类装饰器特别适合需要内部状态或者需要在多个被装饰函数之间共享某些配置或资源的场景。比如,一个数据库连接池的装饰器,或者一个复杂的缓存机制,用类来实现可能会让代码结构更清晰,状态管理也更集中。当然,这并不是说函数装饰器就不能实现有状态的,只是类提供了一种更面向对象的封装方式。选择哪种方式,很多时候取决于具体的需求和个人偏好,但了解它们的原理和适用场景,总能帮助我们做出更明智的决策。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
数据类型有哪几种
数据类型有哪几种

数据类型有整型、浮点型、字符型、字符串型、布尔型、数组、结构体和枚举等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

338

2023.10.31

php数据类型
php数据类型

本专题整合了php数据类型相关内容,阅读专题下面的文章了解更多详细内容。

225

2025.10.31

c语言 数据类型
c语言 数据类型

本专题整合了c语言数据类型相关内容,阅读专题下面的文章了解更多详细内容。

138

2026.02.12

go语言 面向对象
go语言 面向对象

本专题整合了go语言面向对象相关内容,阅读专题下面的文章了解更多详细内容。

58

2025.09.05

java面向对象
java面向对象

本专题整合了java面向对象相关内容,阅读专题下面的文章了解更多详细内容。

63

2025.11.27

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

761

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

221

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1569

2023.10.24

TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

26

2026.03.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
最新Python教程 从入门到精通
最新Python教程 从入门到精通

共4课时 | 22.5万人学习

Django 教程
Django 教程

共28课时 | 5万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号