Python装饰器如何深入理解与运用?

2026-05-22 07:551阅读0评论SEO资讯
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计3565个文字,预计阅读时间需要15分钟。

Python装饰器如何深入理解与运用?

目录+ 装饰器 + 一、基础准备 + 二、可调用对象 + 三、嵌套函数 + 3.1.1 封装 + 3.1.2 贫血DRY原则 + 3.1.3 闭包 + 四、描述器 + 4.1 简介描述器 + 4.2 使用描述器 + 五、函数装饰器 + 5.1 无参数装饰器 + 5.2 有参数装饰器

目录
  • 装饰器
    • 一、 基础准备
      • 1、 可调用对象
      • 2、 嵌套函数
        • 2.1.1 封装
        • 2.1.2 贯彻 DRY 原则
        • 2.1.3 闭包
      • 3、 描述器
        • 3.1 简介
        • 3.2 使用描述器
    • 二、 函数装饰器
      • 1、 无参装饰器
      • 2、 有参装饰器
    • 三、 类装饰器
      • 1、 查找对象属性
      • 2、 __call__实现类装饰器
      • 3、 将类装饰器添加到成员函数
      • 4、 带参数的类装饰器
    • 四、 装饰器修饰协程

装饰器 一、 基础准备 1、 可调用对象

在Python中,除了用户定义的函数,调用运算符(即 ())还可以应用到其他对象上。如果想判断对象能否调用,可以使用内置的 callable() 函数。Python 数据模型文档列出了以下 7 种可调用对象:

  1. 用户定义的函数

    使用def语句或lambda表达式创建

  2. 内置函数

    使用C语言(CPython)实现的函数,如:len()

  3. 内置方法

    使用C语言实现的方法,如:dict.get(key)

  4. 方法

    在类的定义体的函数

  5. 调用类是会运行类的__new__()方法创建一个实例,然后运行__init__()方法,初始化实例。最后把实例返回给调用方。因为Python没有new运算符,所以调用类相当于调用函数。(通常,调用类会创建那个类的实例,不过覆盖 __new__ 方法的话,也可能出现其他行为。)

  6. 类的实例

    如果类定义了__call__方法,那么它的实例可以作为函数调用

  7. 生成器函数

    使用yield关键字的函数或方法。调用生成器函数返回的是生成器对象

实例:把类的实例变成可调用对象

class Callable: def __call__(self, *args, **kwargs): return "类的实例对象被调用" call = Callable() print(call()) print(callable(call))

其相当于C语言里面的函数调用运算符的重载

2、 嵌套函数

嵌套函数是在另一个函数中定义的函数

使用场景:

  • 封装 - 数据隐藏
  • 贯彻 DRY原则
  • 闭包
2.1.1 封装

可以使用内层函数来保护它们不受函数外部变化的影响,也就是说把它们的作用域转换为局部作用域

def outer(n): count = 0 # 统计内层函数的调用次数 def inner(): print("内层函数被调用") nonlocal count count += 1 # 闭包处理 return count

在全局作用域下,尝试去访问count变量会报错,访问内层函数也会报错

实例,递归函数的高级实现

def multi(n): if not isinstance(n, int): # 如果数字不是整型 raise TypeError("n must be Integer") if n < 0: # 如果输入的数据小于0 raise ValueError("n must be zero or positive") def inner_multi(n): # 内层函数嵌套求乘积 if n <= 1: return 1 return n * inner_multi(n - 1) return inner_multi(n) print(multi(3))

当传入的数据不符合求乘积的规则时,报错

2.1.2 贯彻 DRY 原则

DRY 原则:

  • 是指在程序设计以及计算中避免重复代码,因为这样会降低灵活性、简洁性,并且有可能导致代码之间的矛盾
  • 其更多的是一种架构设计思想,在软件开发过程中的万事万物均可能重复,大到标准、框架、开发流程;中到组件、接口;小到功能、代码均纯存在自我重复。而 DRY 提倡的就是在软件开发过程中应消除所有这些自我重复

比如,函数装饰器的使用

2.1.3 闭包

闭包的介绍:

  • 在一个内部函数中,对外部作用域的变量进行引用,(并且一般外部函数的返回值为内部函数),那么内部函数就被认为是闭包

闭包的作用:

  1. 当闭包执行完后,仍然能够保持住当前的运行环境
  2. 闭包可以根据外部作用域的局部变量来得到不同的结果

Python装饰器的本质是返回一个函数地址,然后发生函数调用

比如

def print_(): print("你好") def inner(): print("hello") return inner # 将内层函数的地址返回 def main(print_): # 将函数作为参数传输 inner = print_() print(inner) # 其为内层函数的地址 inner() # 调用内层函数 main(print_)

函数可以当做其他函数的参数进行传入,同时如果返回值是函数,则返回该函数的内存地址

3、 描述器 3.1 简介

描述器是一个Python对象,只要其具有__get__(), __set__(), __delete__()方法中任意一个方法的对象就叫做描述器。一个对象在访问描述器时,如果该属性是一个描述器,则默认属性回调规则会被__set__(), __get__(), __delete__()方法所覆盖

作用:

  • python内部自带的staticmethod, classmethod, property, super等都是描述器,在很多Python库中也都有描述器的身影,使用描述器能让你有更高的概率写出优美的代码、更简洁的API,并且会加深对Python理解
3.2 使用描述器

class Des: def __get__(self, instance, owner): print('returned from des obj') return self.value # 访问实例属性触发 def __set__(self, instance, value): print('set in des obj') self.value = value # 设置对象属性的值时 def __delete__(self, instance): print('delete in des obj') del self.value # 删除对象时触发 class Foo: d = Des() f = Foo() f.d = 10 print(f.d) del f.d

关于描述器的更多内容,请查看官方文档,这里只要求了解

二、 函数装饰器

使用函数作为装饰器

1、 无参装饰器

语法:

@函数名 def test(): pass

实例:

def decorate(func): # 将函数传入其中 print("函数传入") print(func.__name__) def inner(*args, **kwargs): print("函数开始运行") func(*args, **kwargs) # 如果此函数有返回值,则要给内层函数添加返回值 print("函数运行结束") return inner # 将内层函数,返回给外层函数 @decorate def test(): print("你好李华") test() print(test.__name__) # 发现函数名称变为了inner """ # 如果不使用装饰器的话 test = decorate(test) # 接收内层函数 test() # 调用内层函数 """

注意:

  • __name__:属性获取函数的名称
  • __doc__:属性获取函数里面的注释

装饰器语法实现在运行时,将待装饰的函数重定向到装饰后的函数,装饰后的原函数__name__属性发生改变

那么,如何使得函数属性name的问题呢?

from functools import wraps def decorate(func): print("函数传入") @wraps(func) # 再次使用一个装饰器,使得函数名称不会改变 def inner(*args, **kwargs): print("函数开始运行") func(*args, **kwargs) print("函数运行结束") return inner @decorate def test(): print("你好李华") test() print(test.__name__) 2、 有参装饰器

from functools import wraps def decorate_outer(name): print(name) def decorate(func): print("函数传入") @wraps(func) # 再次使用一个装饰器,使得函数名称不会改变 def inner(*args, **kwargs): print("函数开始运行") func(*args, **kwargs) print("函数运行结束") return inner return decorate @decorate_outer("你好呀") def test(): print("你好李华") test()

通过使用三层函数嵌套来实现,最外层函数接收的参数为装饰器自带的参数,次外层函数接收的参数是使用装饰器的函数

当装饰器给类使用时,实现步骤也是一样:

from functools import wraps def decorate(cls): print("类传入") @wraps(cls) # 再次使用一个装饰器,使得类名称不会改变 def inner(*args, **kwargs): print("类开始创建") print(cls.__name__) c = cls(*args, **kwargs) # 创建类 print("类创建结束") return c # 将创建的类返回 return inner @decorate class A: def print_print(self): print("李华好呀") a = A() a.print_print() # 相当于 # A = decorate(A) # a = A() 三、 类装饰器 1、 查找对象属性

class A: def __init__(self, name): self.name = name def test(self): print("test") a = A("李华") print(dir(a)) # 获取对象属性或者实例属性 print(a.__dict__) # 存储对象属性(类和类的实例均为对象)的一个字典,其键为属性名,值为属性的值

属性查找顺序:

  1. 判断是不是系统是自动产生的属性
  2. 在对象实例属性的字典(a.dict)中查找
  3. 在对象属性的字典(A.dict)中查找
  4. 在对象的父类中的字典(dict)中查找

只有在类的字典中找到属性,python才会去看看它有没有get等方法,对一个在实例的字典中找到的属性,python不会理会有没有get方法,而是直接返回属性本身

基于前面的使用函数作为装饰器的理解,将类作为装饰器时需要保证以下几点

  1. 类的实例是可调用的
  2. 类需要一个地方讲被装饰的函数传入到类的实例里

第一条可以通过__call__实现,第二条可以通过__init__实现

2、 __call__实现类装饰器

class Decorate: def __init__(self, func): print("函数正在传入") self.func = func def __call__(self, *args, **kwargs): print("函数开始运行") self.func(*args, **kwargs) # 如果有返回值,则要在最后面使用return返回 print("函数运行结束") @Decorate def print_hello(): print("hello") print_hello() print(print_hello.__name__) # 发现报错 """ 相当于 print_hello = Decorate(print_hello) print_hello() """

我们发现无法输出__name__,根据约定使用装饰器不能改变函数的__name__属性,因此我们还需要改进将类作为装饰器的使用方法

解决方法,添加一个wraps(func)(self)

from functools import wraps class Decorate: def __init__(self, func): print("函数正在传入") self.func = func wraps(func)(self) # 为属性增加一个__wrapped__作为类实例属性 def __call__(self, *args, **kwargs): print("函数开始运行") self.__wrapped__(*args, **kwargs) # self.__wrapped__即为self.func print("函数运行结束") @Decorate def print_hello(): print("hello") print_hello() print(print_hello.__name__) # 返回的名字即为函数名

那么,我们以及会将此类作为装饰器正确使用了,似乎定义了__call__方法就说明问题就解决了,那么真的是如此吗?

3、 将类装饰器添加到成员函数

如果直接添加到成员函数中,会直接报错

Python装饰器如何深入理解与运用?

那么,但类装饰器应用于成员函数时,类成员函数变成什么样呢?

import types from functools import wraps class Decorate: def __init__(self, func): print("函数正在传入") self.func = func wraps(func)(self) # 为属性增加一个__wrapped__作为类实例属性 def __call__(self, *args, **kwargs): print("函数开始运行") self.__wrapped__(*args, **kwargs) # self.__wrapped__即为self.func print("函数运行结束") def __get__(self, instance, cls): """:param: instance: 即为调用装饰器的实例对象 cls: 即为类对象""" # print(instance, cls, self) if instance is None: return self else: return types.MethodType(self, instance) # 将A实例对象添加给self里面 class A: @Decorate def test(self): print('hello') a = A() a.test()

这里需要添加__get__魔法方法,将装饰器的self对象的指向,添加A的实例对象

魔法方法具体使用,在官方文档中有详细描述:

4、 带参数的类装饰器

带参数和不带参数的类装饰器有很大不同。

__init__:不再接收被装饰函数,而是接收传入参数

__call__:接收被装饰的函数,实现装饰逻辑

from functools import wraps class logger(object): def __init__(self, level='INFO'): self.level = level def __call__(self, func): # 接受函数 @wraps(func) def wrapper(*args, **kwargs): print("[{level}]: the function {func}() is running...".format(level=self.level, func=func.__name__)) func(*args, **kwargs) return wrapper # 返回函数 @logger(level='WARNING') def say(something): print("say {}!".format(something)) say("hello") print(say.__name__) 四、 装饰器修饰协程

使用异步装饰器装饰协程比较写法比较简单,调用外部函数是要使用await挂起

同时,内层函数必须转换为协程函数

import asyncio from functools import wraps from time import time def decorate(func): print("协程函数传入") @wraps(func) async def inner(*args, **kwargs): # 将装饰器转换成协程函数 print("开始运行函数") ret = await func(*args, **kwargs) print("函数运行完成") return ret return inner @decorate async def print_(a): await asyncio.sleep(2) return a async def main(): tasks = [asyncio.create_task(print_(i)) for i in range(10)] done, padding = await asyncio.wait(tasks) for i in list(done): print(i.result()) # 获取返回值 start = time() asyncio.run(main()) print(f"用时{time() - start}")

本文共计3565个文字,预计阅读时间需要15分钟。

Python装饰器如何深入理解与运用?

目录+ 装饰器 + 一、基础准备 + 二、可调用对象 + 三、嵌套函数 + 3.1.1 封装 + 3.1.2 贫血DRY原则 + 3.1.3 闭包 + 四、描述器 + 4.1 简介描述器 + 4.2 使用描述器 + 五、函数装饰器 + 5.1 无参数装饰器 + 5.2 有参数装饰器

目录
  • 装饰器
    • 一、 基础准备
      • 1、 可调用对象
      • 2、 嵌套函数
        • 2.1.1 封装
        • 2.1.2 贯彻 DRY 原则
        • 2.1.3 闭包
      • 3、 描述器
        • 3.1 简介
        • 3.2 使用描述器
    • 二、 函数装饰器
      • 1、 无参装饰器
      • 2、 有参装饰器
    • 三、 类装饰器
      • 1、 查找对象属性
      • 2、 __call__实现类装饰器
      • 3、 将类装饰器添加到成员函数
      • 4、 带参数的类装饰器
    • 四、 装饰器修饰协程

装饰器 一、 基础准备 1、 可调用对象

在Python中,除了用户定义的函数,调用运算符(即 ())还可以应用到其他对象上。如果想判断对象能否调用,可以使用内置的 callable() 函数。Python 数据模型文档列出了以下 7 种可调用对象:

  1. 用户定义的函数

    使用def语句或lambda表达式创建

  2. 内置函数

    使用C语言(CPython)实现的函数,如:len()

  3. 内置方法

    使用C语言实现的方法,如:dict.get(key)

  4. 方法

    在类的定义体的函数

  5. 调用类是会运行类的__new__()方法创建一个实例,然后运行__init__()方法,初始化实例。最后把实例返回给调用方。因为Python没有new运算符,所以调用类相当于调用函数。(通常,调用类会创建那个类的实例,不过覆盖 __new__ 方法的话,也可能出现其他行为。)

  6. 类的实例

    如果类定义了__call__方法,那么它的实例可以作为函数调用

  7. 生成器函数

    使用yield关键字的函数或方法。调用生成器函数返回的是生成器对象

实例:把类的实例变成可调用对象

class Callable: def __call__(self, *args, **kwargs): return "类的实例对象被调用" call = Callable() print(call()) print(callable(call))

其相当于C语言里面的函数调用运算符的重载

2、 嵌套函数

嵌套函数是在另一个函数中定义的函数

使用场景:

  • 封装 - 数据隐藏
  • 贯彻 DRY原则
  • 闭包
2.1.1 封装

可以使用内层函数来保护它们不受函数外部变化的影响,也就是说把它们的作用域转换为局部作用域

def outer(n): count = 0 # 统计内层函数的调用次数 def inner(): print("内层函数被调用") nonlocal count count += 1 # 闭包处理 return count

在全局作用域下,尝试去访问count变量会报错,访问内层函数也会报错

实例,递归函数的高级实现

def multi(n): if not isinstance(n, int): # 如果数字不是整型 raise TypeError("n must be Integer") if n < 0: # 如果输入的数据小于0 raise ValueError("n must be zero or positive") def inner_multi(n): # 内层函数嵌套求乘积 if n <= 1: return 1 return n * inner_multi(n - 1) return inner_multi(n) print(multi(3))

当传入的数据不符合求乘积的规则时,报错

2.1.2 贯彻 DRY 原则

DRY 原则:

  • 是指在程序设计以及计算中避免重复代码,因为这样会降低灵活性、简洁性,并且有可能导致代码之间的矛盾
  • 其更多的是一种架构设计思想,在软件开发过程中的万事万物均可能重复,大到标准、框架、开发流程;中到组件、接口;小到功能、代码均纯存在自我重复。而 DRY 提倡的就是在软件开发过程中应消除所有这些自我重复

比如,函数装饰器的使用

2.1.3 闭包

闭包的介绍:

  • 在一个内部函数中,对外部作用域的变量进行引用,(并且一般外部函数的返回值为内部函数),那么内部函数就被认为是闭包

闭包的作用:

  1. 当闭包执行完后,仍然能够保持住当前的运行环境
  2. 闭包可以根据外部作用域的局部变量来得到不同的结果

Python装饰器的本质是返回一个函数地址,然后发生函数调用

比如

def print_(): print("你好") def inner(): print("hello") return inner # 将内层函数的地址返回 def main(print_): # 将函数作为参数传输 inner = print_() print(inner) # 其为内层函数的地址 inner() # 调用内层函数 main(print_)

函数可以当做其他函数的参数进行传入,同时如果返回值是函数,则返回该函数的内存地址

3、 描述器 3.1 简介

描述器是一个Python对象,只要其具有__get__(), __set__(), __delete__()方法中任意一个方法的对象就叫做描述器。一个对象在访问描述器时,如果该属性是一个描述器,则默认属性回调规则会被__set__(), __get__(), __delete__()方法所覆盖

作用:

  • python内部自带的staticmethod, classmethod, property, super等都是描述器,在很多Python库中也都有描述器的身影,使用描述器能让你有更高的概率写出优美的代码、更简洁的API,并且会加深对Python理解
3.2 使用描述器

class Des: def __get__(self, instance, owner): print('returned from des obj') return self.value # 访问实例属性触发 def __set__(self, instance, value): print('set in des obj') self.value = value # 设置对象属性的值时 def __delete__(self, instance): print('delete in des obj') del self.value # 删除对象时触发 class Foo: d = Des() f = Foo() f.d = 10 print(f.d) del f.d

关于描述器的更多内容,请查看官方文档,这里只要求了解

二、 函数装饰器

使用函数作为装饰器

1、 无参装饰器

语法:

@函数名 def test(): pass

实例:

def decorate(func): # 将函数传入其中 print("函数传入") print(func.__name__) def inner(*args, **kwargs): print("函数开始运行") func(*args, **kwargs) # 如果此函数有返回值,则要给内层函数添加返回值 print("函数运行结束") return inner # 将内层函数,返回给外层函数 @decorate def test(): print("你好李华") test() print(test.__name__) # 发现函数名称变为了inner """ # 如果不使用装饰器的话 test = decorate(test) # 接收内层函数 test() # 调用内层函数 """

注意:

  • __name__:属性获取函数的名称
  • __doc__:属性获取函数里面的注释

装饰器语法实现在运行时,将待装饰的函数重定向到装饰后的函数,装饰后的原函数__name__属性发生改变

那么,如何使得函数属性name的问题呢?

from functools import wraps def decorate(func): print("函数传入") @wraps(func) # 再次使用一个装饰器,使得函数名称不会改变 def inner(*args, **kwargs): print("函数开始运行") func(*args, **kwargs) print("函数运行结束") return inner @decorate def test(): print("你好李华") test() print(test.__name__) 2、 有参装饰器

from functools import wraps def decorate_outer(name): print(name) def decorate(func): print("函数传入") @wraps(func) # 再次使用一个装饰器,使得函数名称不会改变 def inner(*args, **kwargs): print("函数开始运行") func(*args, **kwargs) print("函数运行结束") return inner return decorate @decorate_outer("你好呀") def test(): print("你好李华") test()

通过使用三层函数嵌套来实现,最外层函数接收的参数为装饰器自带的参数,次外层函数接收的参数是使用装饰器的函数

当装饰器给类使用时,实现步骤也是一样:

from functools import wraps def decorate(cls): print("类传入") @wraps(cls) # 再次使用一个装饰器,使得类名称不会改变 def inner(*args, **kwargs): print("类开始创建") print(cls.__name__) c = cls(*args, **kwargs) # 创建类 print("类创建结束") return c # 将创建的类返回 return inner @decorate class A: def print_print(self): print("李华好呀") a = A() a.print_print() # 相当于 # A = decorate(A) # a = A() 三、 类装饰器 1、 查找对象属性

class A: def __init__(self, name): self.name = name def test(self): print("test") a = A("李华") print(dir(a)) # 获取对象属性或者实例属性 print(a.__dict__) # 存储对象属性(类和类的实例均为对象)的一个字典,其键为属性名,值为属性的值

属性查找顺序:

  1. 判断是不是系统是自动产生的属性
  2. 在对象实例属性的字典(a.dict)中查找
  3. 在对象属性的字典(A.dict)中查找
  4. 在对象的父类中的字典(dict)中查找

只有在类的字典中找到属性,python才会去看看它有没有get等方法,对一个在实例的字典中找到的属性,python不会理会有没有get方法,而是直接返回属性本身

基于前面的使用函数作为装饰器的理解,将类作为装饰器时需要保证以下几点

  1. 类的实例是可调用的
  2. 类需要一个地方讲被装饰的函数传入到类的实例里

第一条可以通过__call__实现,第二条可以通过__init__实现

2、 __call__实现类装饰器

class Decorate: def __init__(self, func): print("函数正在传入") self.func = func def __call__(self, *args, **kwargs): print("函数开始运行") self.func(*args, **kwargs) # 如果有返回值,则要在最后面使用return返回 print("函数运行结束") @Decorate def print_hello(): print("hello") print_hello() print(print_hello.__name__) # 发现报错 """ 相当于 print_hello = Decorate(print_hello) print_hello() """

我们发现无法输出__name__,根据约定使用装饰器不能改变函数的__name__属性,因此我们还需要改进将类作为装饰器的使用方法

解决方法,添加一个wraps(func)(self)

from functools import wraps class Decorate: def __init__(self, func): print("函数正在传入") self.func = func wraps(func)(self) # 为属性增加一个__wrapped__作为类实例属性 def __call__(self, *args, **kwargs): print("函数开始运行") self.__wrapped__(*args, **kwargs) # self.__wrapped__即为self.func print("函数运行结束") @Decorate def print_hello(): print("hello") print_hello() print(print_hello.__name__) # 返回的名字即为函数名

那么,我们以及会将此类作为装饰器正确使用了,似乎定义了__call__方法就说明问题就解决了,那么真的是如此吗?

3、 将类装饰器添加到成员函数

如果直接添加到成员函数中,会直接报错

Python装饰器如何深入理解与运用?

那么,但类装饰器应用于成员函数时,类成员函数变成什么样呢?

import types from functools import wraps class Decorate: def __init__(self, func): print("函数正在传入") self.func = func wraps(func)(self) # 为属性增加一个__wrapped__作为类实例属性 def __call__(self, *args, **kwargs): print("函数开始运行") self.__wrapped__(*args, **kwargs) # self.__wrapped__即为self.func print("函数运行结束") def __get__(self, instance, cls): """:param: instance: 即为调用装饰器的实例对象 cls: 即为类对象""" # print(instance, cls, self) if instance is None: return self else: return types.MethodType(self, instance) # 将A实例对象添加给self里面 class A: @Decorate def test(self): print('hello') a = A() a.test()

这里需要添加__get__魔法方法,将装饰器的self对象的指向,添加A的实例对象

魔法方法具体使用,在官方文档中有详细描述:

4、 带参数的类装饰器

带参数和不带参数的类装饰器有很大不同。

__init__:不再接收被装饰函数,而是接收传入参数

__call__:接收被装饰的函数,实现装饰逻辑

from functools import wraps class logger(object): def __init__(self, level='INFO'): self.level = level def __call__(self, func): # 接受函数 @wraps(func) def wrapper(*args, **kwargs): print("[{level}]: the function {func}() is running...".format(level=self.level, func=func.__name__)) func(*args, **kwargs) return wrapper # 返回函数 @logger(level='WARNING') def say(something): print("say {}!".format(something)) say("hello") print(say.__name__) 四、 装饰器修饰协程

使用异步装饰器装饰协程比较写法比较简单,调用外部函数是要使用await挂起

同时,内层函数必须转换为协程函数

import asyncio from functools import wraps from time import time def decorate(func): print("协程函数传入") @wraps(func) async def inner(*args, **kwargs): # 将装饰器转换成协程函数 print("开始运行函数") ret = await func(*args, **kwargs) print("函数运行完成") return ret return inner @decorate async def print_(a): await asyncio.sleep(2) return a async def main(): tasks = [asyncio.create_task(print_(i)) for i in range(10)] done, padding = await asyncio.wait(tasks) for i in list(done): print(i.result()) # 获取返回值 start = time() asyncio.run(main()) print(f"用时{time() - start}")