- 装饰器
- 一、 基础准备
- 1、 可调用对象
- 2、 嵌套函数
- 2.1.1 封装
- 2.1.2 贯彻 DRY 原则
- 2.1.3 闭包
- 3、 描述器
- 3.1 简介
- 3.2 使用描述器
- 二、 函数装饰器
- 1、 无参装饰器
- 2、 有参装饰器
- 三、 类装饰器
- 1、 查找对象属性
- 2、
__call__
实现类装饰器 - 3、 将类装饰器添加到成员函数
- 4、 带参数的类装饰器
- 四、 装饰器修饰协程
- 一、 基础准备
在Python中,除了用户定义的函数,调用运算符(即 ()
)还可以应用到其他对象上。如果想判断对象能否调用,可以使用内置的 callable()
函数。Python 数据模型文档列出了以下 7 种可调用对象:
-
用户定义的函数
使用
def
语句或lambda
表达式创建 -
内置函数
使用C语言(CPython)实现的函数,如:
len()
-
内置方法
使用C语言实现的方法,如:
dict.get(key)
-
方法
在类的定义体的函数
-
类
调用类是会运行类的
__new__()
方法创建一个实例,然后运行__init__()
方法,初始化实例。最后把实例返回给调用方。因为Python没有new
运算符,所以调用类相当于调用函数。(通常,调用类会创建那个类的实例,不过覆盖__new__
方法的话,也可能出现其他行为。) -
类的实例
如果类定义了
__call__
方法,那么它的实例可以作为函数调用 -
生成器函数
使用
yield
关键字的函数或方法。调用生成器函数返回的是生成器对象
实例:把类的实例变成可调用对象
class Callable:
def __call__(self, *args, **kwargs):
return "类的实例对象被调用"
call = Callable()
print(call())
print(callable(call))
2、 嵌套函数其相当于C语言里面的函数调用运算符的重载
嵌套函数是在另一个函数中定义的函数
使用场景:
- 封装 - 数据隐藏
- 贯彻 DRY原则
- 闭包
可以使用内层函数来保护它们不受函数外部变化的影响,也就是说把它们的作用域转换为局部作用域
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 闭包闭包的介绍:
- 在一个内部函数中,对外部作用域的变量进行引用,(并且一般外部函数的返回值为内部函数),那么内部函数就被认为是闭包
闭包的作用:
- 当闭包执行完后,仍然能够保持住当前的运行环境
- 闭包可以根据外部作用域的局部变量来得到不同的结果
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理解
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__) # 存储对象属性(类和类的实例均为对象)的一个字典,其键为属性名,值为属性的值
属性查找顺序:
- 判断是不是系统是自动产生的属性
- 在对象实例属性的字典(
a.dict
)中查找 - 在对象属性的字典(
A.dict
)中查找 - 在对象的父类中的字典(
dict
)中查找
只有在类的字典中找到属性,python才会去看看它有没有get
等方法,对一个在实例的字典中找到的属性,python不会理会有没有get
方法,而是直接返回属性本身
基于前面的使用函数作为装饰器的理解,将类作为装饰器时需要保证以下几点
- 类的实例是可调用的
- 类需要一个地方讲被装饰的函数传入到类的实例里
第一条可以通过__call__
实现,第二条可以通过__init__
实现
__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__
方法就说明问题就解决了,那么真的是如此吗?
如果直接添加到成员函数中,会直接报错
那么,但类装饰器应用于成员函数时,类成员函数变成什么样呢?
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()
4、 带参数的类装饰器这里需要添加
__get__
魔法方法,将装饰器的self对象的指向,添加A的实例对象魔法方法具体使用,在官方文档中有详细描述:【https://docs.python.org/zh-cn/3/reference/datamodel.html】
带参数和不带参数的类装饰器有很大不同。
__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}")