python 之装饰器(decorator)及其分类

一、装饰器的实现过程

1、Decorator(装饰器)和 Wrapper(包装函数)

Decorator(装饰器)即 outer() 外部函数

装饰器(decorator)是一种设计模式,也是 Python 中的一个语言特性,用于在不修改原函数代码的前提下,增强或修改函数的行为

装饰器本质上是一个可调用对象(通常是函数或类),它接收一个函数(或方法)作为参数,并返回一个新的函数或可调用对象。

Wrapper(包装函数)即 inner() 内部函数

wrapper 是装饰器内部定义的一个嵌套函数,它的作用是包装原函数的调用,在调用前后添加额外逻辑(如日志、计时、权限检查等)。

  • wrapper 函数通常接受 *args**kwargs,以兼容任意参数的原函数。
  • 它在装饰器函数中被定义,并作为返回值返回。
  • 它“包裹”了原函数的功能。
def timer_decorator(func):
    def wrapper(*args, **kwargs):  # ← 这就是 wrapper
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} 执行耗时: {end - start:.4f} 秒")
        return result
    return wrapper

给index函数添加统计执行时间的功能

import time

def index():
    time.sleep(3)
    print('from index')

def home():
    time.sleep(2)
    print('from home')

def func():
    time.sleep(2)
    print('from home')

def outer(func): # 外层函数
    def get_time(): # 内层函数
 
        # 1. 在函数执行之前打一个时间点
        start_time = time.time()
        
        func()  # index() 

        # 2. 在函数执行完毕之后在打一个时间点
        end_time = time.time()

        # 3. 两个时间的差值就是函数的实际执行时间
        print("函数执行了:%s秒" % (end_time - start_time))

    return get_time  # 返回内层函数


# 利用闭包的形式来传参
# res = outer(index)  # res:get_time的内存地址
# res()  # get_time()

home = outer(home)
home()

2、解决参数的问题

当执行不同的函数,计算不同函数的执行时间,有的函数有参数,有的函数没参数

def index(name, username):
    time.sleep(3)
    print('from index')

def home(name):
    time.sleep(2)
    print('from home', name)

def outer(func):
    def get_time(*args, **kwargs):
        
        # 1. 在函数执行之前打一个时间点
        start_time = time.time()
        
        func(*args, **kwargs)  # 原有函数
        
        # 2. 在函数执行完毕之后在打一个时间点
        end_time = time.time()

        # 3. 两个时间的差值就是函数的实际执行时间
        print("函数执行了:%s秒" % (end_time - start_time))

    return get_time
index = outer(index)
index('tony', username='tank')            

3、解决返回值问题

index = outer(index) 将index函数作为参数传到 outer 中

outer() 执行 --->  return get_time  ---> res = func()  ---> return res  --->  res = index() 最后res就是原函数的返回值

def home(name):
    time.sleep(2)
    print('from home', name)

def index():
    time.sleep(3)
    print('from index')
    return 'from index'

def outer(func):
    def get_time(*args, **kwargs):
        start_time = time.time()
        res=func(*args, **kwargs)  # index()  func('tony')
        end_time = time.time()
        print("函数执行了:%s秒" % (end_time - start_time))
        return res
    return get_time
index = outer(index)
res=index()
print(res)

4、装饰器模板

def outer(func):
    def inner(*args, **kwargs):
        print('这个是函数执行之前可以添加的功能')
        res = func(*args, **kwargs)
        print('这个是函数执行之后可以添加的功能')
        return res

    return inner


def index():
    print('from index')


index = outer(index)
index()

5、装饰器的语法糖

Python 提供了 @ 语法来简化装饰器的使用:

@decorator
def my_function():
    print("Hello")

等价于:

def my_function():
    print("Hello")

my_function = decorator(my_function)

简单的计时器装饰器

import time

def timer_decorator(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_decorator
def slow_function():
    time.sleep(1)
    print("函数执行完毕")

slow_function()

6、装饰器修复技术(@wrapper)

functools.wraps 函数可以拷贝原函数的属性信息。warps源码中通过使用partial函数复制了原函数的信息。

from functools import wraps


def my_decorator(func):
    @wraps(func)  # ← 保留原函数的元信息
    def wrapper(*args, **kwargs):
        print("调用前")
        result = func(*args, **kwargs)
        print("调用后")
        return result

    return wrapper


@my_decorator
def greet(name):
    """打招呼函数"""
    print(f"Hello, {name}!")


print(greet.__name__)  # 输出: greet(而不是 wrapper)
print(greet.__doc__)  # 输出: 打招呼函数

7、双层语法糖

使用场景:将登录验证、计算函数执行时间两个功能附加给一个原函数

# 双层语法糖
import time

# 登录验证功能
def login_auth(func):
    # func = index
    def auth(*args, **kwargs):
        # 写登录功能
        # 1.
        username = input('username:').strip()
        password = input('password:').strip()

        # 2. 验证用户名和密码是否正确
        if username == 'kevin' and password == '123':
            print('登录成功')
            res = func(*args, **kwargs)
            return res
        else:
            print('用户名或者密码错误')
    return auth

# 计算函数执行时间
def get_time(func):
    # func = index
    def inner(*args, **kwargs):
        # func:index
        # 1. 在函数执行之前打一个时间点
        start_time = time.time()
        res = func(*args, **kwargs)  # index()  func('tony')
        # 2. 在函数执行完毕之后在打一个时间点
        end_time = time.time()

        # 3. 两个时间的差值就是函数的实际执行时间
        print("函数执行了:%s秒" % (end_time - start_time))
        return res
    return inner

@login_auth  # index=login_auth(get_time内部的inner函数名)
@get_time  # get_time内部的inner函数名,等价于get_time(index)
def index():
    time.sleep(2)
    print('from index')

index()  # auth()

从下往上执行,最外层语法糖使用下面的函数名,即index = login_auth(inner)

下面是代码的执行顺序:
  1. 首先,装饰器 @get_time 被执行,它将函数 index 作为参数传递给装饰器函数 get_time。因为 get_time 是一个装饰器函数,它会返回一个新的函数 inner 来替换原来的函数 index
  2. 接着,装饰器 @login_auth 被执行,它将函数 inner 作为参数传递给装饰器函数 login_auth。因为 login_auth 也是一个装饰器函数,它会返回一个新的函数 auth 来替换原来的函数 inner
  3. 现在,函数 index 已经被装饰成 auth 函数,即 index = login_auth(get_time(index))
  4. 当我们调用函数 index() 时,实际上是调用被装饰后的 auth 函数。auth 函数首先执行登录验证,验证通过后再执行原始函数 inner
  5. 函数 inner 中,先记录下函数执行之前的时间,然后再执行原始函数 index(即被装饰的函数)。在执行完原始函数后,再记录下函数执行之后的时间,并计算出函数实际执行的时间。
  6. 最后,函数 auth 返回原始函数 inner 的返回值,即函数 index 的返回值。
因此,整个过程中,函数 index 实际上被双重装饰了,首先是被装饰成计算函数执行时间的函数 inner,然后再被装饰成登录验证的函数 auth。这种双重装饰的方式可以让多个装饰器按照特定顺序依次作用于同一个函数上,从而实现更加复杂的功能。

8、三层语法糖

def outter1(func1):
    print('加载了outter1')
    def wrapper1(*args, **kwargs):
        print('执行了wrapper1')
        res1 = func1(*args, **kwargs)
        return res1

    return wrapper1

def outter2(func2):
    print('加载了outter2')

    def wrapper2(*args, **kwargs):
        print('执行了wrapper2')
        res2 = func2(*args, **kwargs)
        return res2

    return wrapper2

def outter3(func3):
    print('加载了outter3')

    def wrapper3(*args, **kwargs):
        print('执行了wrapper3')
        res3 = func3(*args, **kwargs)
        return res3

    return wrapper3

@outter1  # index =  outter1(wrapper2) = wrapper1
@outter2  # wrapper2 = outter2(wrapper3)
@outter3  # wrapper3 = outter3(index)
def index():
    print('from index')

index()  # wrapper1 被index 调用。index() 函数执行等于 wrapper1()函数执行

执行顺序的理解:

1、outter3 首选会被执行 ------> 加载了outter3 -------> 返回 wrapper3 给到 outter2 作为 func2 传参

2、outter2 接着会被执行------> 加载了outter2 -------> 返回 wrapper2 给到 outter1 作为 func2 传参

3、outter1 接着会被执行------> 加载了outter1 -------> 返回 wrapper1 给到 index ,此时 index = wrapper1

4、index()调用相当于wrapper1(), 此时调用wrapper1()-------> 执行了wrapper1----->func1为wrapper2,

wrapper2()被调用 ---------------------------------->执行了wrapper2----->func2为wrapper3,

wrapper3()被调用---------------------------------->执行了wrapper3----->func3为index 

index()被调用---------------------------------->‘from index’

执行结果与过程过程分析

二、有参装饰器

1、最外层多了一层函数的嵌套,这一层带上形参。

  第二层(decorator)和第三层(wrapper)和之前一样,接受函数名参数和变量

      二层 def decorator(func)                        三层 wrapper(*args, **kwargs)

2、语法糖部分:带上实参

无参情况,@outter就行。有参数@outter('实参')

# 有参装饰器就是带参数的装饰器
# 先前我们学习的装饰器就是无参装饰器:不到参数的装饰器

def outter(type):
    def login_auth(func):
        def auth(*args, **kwargs):
            # 写登录功能
            # 1.接收用户输入账号密码
            username = input('username:').strip()
            password = input('password:').strip()

            # 2. 验证用户名和密码是否正确
            # with open('')
            """
                根据不同的参数用户名和密码来自于不同的位置
            """
            if type == 'file':
                print('用户名和密码来自于文件')
            elif type == 'mysql':
                print('用户名和密码来自于MySQL')
            elif type == 'oracle':
                print('用户名和密码来自于Oracle')

            # if username == 'kevin' and password == '123':
            #     print('登录成功')
            #     res = func(*args, **kwargs)
            #     return res
            # else:
            #     print('用户名或者密码错误')
        return auth
    return login_auth

# 语法糖处添加带上参数,无参装饰器这里不用带()
@outter('file')  # @login_auth
# 如果是index函数认证,用户名和密码来自于文件  login_auth(index, 1)
def index():
    print('from index')


@outter('mysql')
# 如果是home函数认证,用户名和密码来MySQL
def home():
    print('from home')


@outter('oracle')
# 如果是func函数认证,用户名和密码来oracle
def func():
    print('from func')


# 调用函数,原有的调用方式不变
index()
home()
func() 

三、类装饰器

常用于 缓存、计数、重试、单例

1、什么是类装饰器

类装饰器是指:一个类,实现了 __call__ 方法,使得该类的实例可以像函数一样被调用。当这个类被用作装饰器时,它接收被装饰的函数作为参数,并可以控制该函数的调用行为。

✅ 类装饰器的关键点:

  • 类的 __init__(self, func) 方法接收被装饰的函数。
  • 类的 __call__(self, *args, **kwargs) 方法实现函数调用时的包装逻辑。
  • 实例必须是“可调用的”(即实现 __call__)。

2、类装饰器的基本结构

class MyClassDecorator:
    def __init__(self, func):
        self.func = func
        # 可以在这里初始化一些状态变量

    def __call__(self, *args, **kwargs):
        # 调用前的逻辑
        print("调用前处理...")
        result = self.func(*args, **kwargs)
        # 调用后的逻辑
        print("调用后处理...")
        return result


@MyClassDecorator
def my_function(x, y):
    print(f"计算中: {x} + {y}")
    return x + y


# 调用
my_function(3, 4)

3、统计函数调用次数

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"函数 '{self.func.__name__}' 被调用了 {self.count} 次")
        return self.func(*args, **kwargs)


@CountCalls
def say_hello():
    print("Hello!")


say_hello()  # 调用第1次
say_hello()  # 调用第2次
say_hello()  # 调用第3次

4、缓存装饰器(Memoization)

class Cache:
    def __init__(self, func):
        self.func = func
        self.cache = {}

    def __call__(self, *args, **kwargs):
        # 仅支持不可变参数(如元组)作为 key
        key = str(args) + str(sorted(kwargs.items()))
        if key in self.cache:
            print(f"缓存命中: {key}")
            return self.cache[key]
        result = self.func(*args, **kwargs)
        self.cache[key] = result
        print(f"缓存写入: {key}")
        return result


@Cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)


print(fibonacci(5))  # 第一次计算,会缓存中间结果
print(fibonacci(5))  # 第二次直接从缓存读取

5、带参数的类装饰器

有时候我们希望装饰器本身也能接收参数,比如 @retry(max_attempts=3)。这时可以用类作为外层容器。

import time
from functools import wraps

class Retry:
    def __init__(self, max_attempts=3, delay=1):
        self.max_attempts = max_attempts
        self.delay = delay

    def __call__(self, func):
        # 这里返回一个可调用对象(可以是函数或另一个类)
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, self.max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"第 {attempt} 次尝试失败: {e}")
                    if attempt == self.max_attempts:
                        raise e
                    time.sleep(self.delay)
        return wrapper

@Retry(max_attempts=3, delay=0.5)
def unstable_api():
    import random
    if random.random() < 0.7:
        raise ConnectionError("网络不稳定")
    print("请求成功!")
    return "data"

# 测试
try:
    unstable_api()
except:
    print("最终失败")

注意:这个例子中,Retry 是一个类,但它本身不是直接装饰函数,而是先被调用(Retry(...))生成一个实例,该实例再作为函数装饰器使用。

6、类装饰器用于装饰类中的方法

class LogCalls:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        # 这个 __call__ 会被绑定后的方法调用
        print(f"调用方法: {self.func.__name__}")
        return self.func(*args, **kwargs)

    def __get__(self, instance, owner):
        # instance: 实例(如 c)
        # owner: 类(如 Calculator)
        if instance is None:
            return self  # 类直接访问时返回装饰器本身
        # 返回一个绑定到 instance 的可调用对象(这里是偏函数)
        from functools import partial
        return partial(self, instance)


class Calculator:
    @LogCalls
    def add(self, a, b):
        return a + b


c = Calculator()
print(c.add(2, 3))  # 输出:调用方法: add \n 5  

7、用类装饰器实现单例模式(Singleton)

class Singleton:
    def __init__(self, cls):
        self.cls = cls
        self.instance = None

    def __call__(self, *args, **kwargs):
        if self.instance is None:
            self.instance = self.cls(*args, **kwargs)
            # print(self.cls)
        return self.instance


@Singleton
class Database:
    def __init__(self):
        print("创建数据库连接...")
        self.db = None


db1 = Database()
db2 = Database()
print(db1 is db2)  # True

补充 cls的理解

装饰器 @Singleton 会把 Database 这个类作为参数传递给 Singleton 装饰器的 __init__ 方法。

 所以:

class Singleton:
    def __init__(self, cls):        # ← cls 就是被装饰的类(这里是 Database)
        self.cls = cls              # 把类保存下来
        self.instance = None        # 用于保存唯一的实例

    def __call__(self, *args, **kwargs):
        if self.instance is None:
            self.instance = self.cls(*args, **kwargs)  # 实例化这个类
        return self.instance

四、一些实用装饰器

1、property 装饰器

作用:将类中的方法转换为“属性”来访问,实现对私有属性的安全访问(封装),同时保持调用的简洁性。

class Student:
    def __init__(self, name, score):
        self._name = name
        self._score = score  # 私有属性

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, value):
        if not isinstance(value, int) or value < 0 or value > 100:
            raise ValueError("成绩必须是0-100之间的整数")
        self._score = value

    @property
    def name(self):
        return self._name.title()  # 返回首字母大写


# 使用
s = Student("alice", 85)
print(s.score)  # 85,像属性一样访问
s.score = 95  # 使用 setter 设置值
print(s.name)  # Alice

补充:在类中传参的过程

class Student:  定义类后面的() 只能写继承的父类、而不是用来传传参,没有父类则不用写

s = Student("alice", 85)

其实 Python 做了两步:

  1. 调用 Student.__new__ 创建对象(一般不自己写,默认继承 object 的实现)

  2. 调用 Student.__init__(self, name, score) 初始化对象,这时候 "alice"85 会传给 namescore

所以等价于:

obj = Student.__new__(Student)     # 创建空对象
Student.__init__(obj, "alice", 85) # 初始化

2、登录装饰器

在 Web 开发中常用于检查用户是否已登录,未登录则跳转或拒绝访问。

from functools import wraps

# 模拟登录状态
current_user = None  # 全局变量表示当前用户

def login_required(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        if current_user is None:
            print("错误:请先登录!")
            return None
        return func(*args, **kwargs)
    return wrapper

@login_required
def view_profile():
    print(f"正在查看 {current_user} 的个人资料")

# 测试
view_profile()  # 错误:请先登录!

current_user = "Alice"
view_profile()  # 正在查看 Alice 的个人资料

3、lru_cache 缓存装饰器

作用:使用“最近最少使用”(LRU)算法缓存函数的返回结果,避免重复计算,显著提升性能,尤其适用于递归或耗时计算。

模块:functools.lru_cache

斐波那契数列优化

from functools import lru_cache


@lru_cache(maxsize=128)  # 最多缓存 128 个不同参数的结果
def fib(n):
    print(f"Calculating fib({n})")
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)


print(fib(10))  # 后续对 fib(10)、fib(9)... 会使用缓存
print(fib(10))
print(fib(20))
print(fib(30))  # 后续对 fib(20)、fib(19)... 会使用缓存

结果

from functools import lru_cache


@lru_cache(maxsize=128)  # 最多缓存 128 个不同参数的结果
def fib(n):
    print(f"Calculating fib({n})")
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)


print(fib(5))  # 后续对 fib(5)使用缓存
print(fib(5))
print(fib(10))

由结果可知:第二次调用fib(5)的时候已经有缓存,fib(10) 到fib(5) 处就使用缓存

image

 

  

 

Auto Copied
posted @ 2023-05-30 16:01  凡人半睁眼  阅读(92)  评论(0)    收藏  举报