一、函数装饰器入门
对应Cookbook章节 9.1、9.2、9.3 和9.8
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2023/8/4 18:44
# @Author : Maple
# @File : 01-函数装饰器入门.py
# Cookbook 9.1、9.2、9.3 和9.8
import time
from functools import wraps
def timethis(func):
'''
Decorator that reports the execution time.
'''
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, end-start)
return result
return wrapper
@timethis
def countdown(n):
'''
Counts down
'''
print('hello')
while n > 0:
n -= 1
# 多层装饰器
def decorator1(func):
@wraps(func)
def wrapper(*args, **kwargs):
print('Decorator 1')
return func(*args, **kwargs)
return wrapper
def decorator2(func):
@wraps(func)
def wrapper(*args, **kwargs):
print('Decorator 2')
return func(*args, **kwargs)
return wrapper
@decorator1
@decorator2
def add(x, y):
return x + y
# @wraps装饰func的底层 到底发生了什么?
# Part1
"""
# 1.wrapped是被装饰的函数
# 2.WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__','__annotations__')
# 3.WRAPPER_UPDATES = ('__dict__',)
# 4.返回一个 update_wrapper 的偏函数
def wraps(wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
Decorator factory to apply update_wrapper() to a wrapper function
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
"""
# Part2
"""
# 1. wrapper是装饰器被装饰后 返回的函数
# 2. updated = ('__dict__',)
def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
Update a wrapper function to look like the wrapped function
for attr in assigned:
try:
# 1. 取出`被装饰`函数的属性
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
# 2.将第一步取出来的属性值 赋值给 wrapper,也就是说wrapper的元数据实际上替换成[被装饰的函数]了
# 以此实现了被装饰函数 元数据的保留
setattr(wrapper, attr, value)
for attr in updated:
# 1.update方法实现 字典值的更新,注意是 后面更新前面的(逻辑: 交叉的key,用后面的值更新前面的; 只存在于后面的key,添加到前面的字典 )
# 2.如果wrapped 有 ('__dict__',)属性[Tips个人理解:类才有dict属性,方法没有这个属性],那么就把wrapper的dict属性也替换成wrapped的
# 3.如果wrapped 没有 dict属性, getattr去默认值{},而空字典 对wrapper.__dict__'的值 不会有任何影响
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
return wrapper
"""
if __name__ == '__main__':
# 1. 简单装饰器测试
countdown(1000000)
print('===================')
# 2.解除装饰器
# 2-1 方法之一
"""
1.适用场景: 在函数已经有装饰器的情况下,但是又要单独测试函数本身的场景
2.必要条件1: 有@wraps修饰
2.必要条件2:只有一个装饰器,多个装饰器的情况解除之后的测试结果不可预测
"""
# 通过__wrapped__直接访问被包装的函数:即countdown自身(不会走装饰器里面的逻辑)
countdown.__wrapped__(1000)
# 2-2 方法之二
print('===================')
countdown = countdown.__wrapped__
countdown(100000)
# 2-3 多个装饰器的解除测试
print('===================')
add = add.__wrapped__
result = add(2,3)
print('result = {}'.format(result)) # result = 5
"""
output:
Decorator 2 ,也就是说内层的装饰器并未解除
result = 5
"""
print(add.__module__) # __main__
print(add.__name__) # add
二、函数装饰器传参
对应cookbook章节 9.4 和 9.6
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2023/8/4 19:08
# @Author : Maple
# @File : 02-函数装饰器传参.py
import logging
from functools import wraps, partial
"""
对应cookbook
9.4 和 9.6
"""
def logged(level,name=None,message=None):
def decorater(func):
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
def wrapper(*args,**kwargs):
log.log(level,logmsg)
# print('{}:{}'.format(logname,logmsg))
return func(*args,**kwargs)
return wrapper
return decorater
# 正常调用1
@logged(logging.DEBUG)
def add(x,y):
return x + y
# 正常调用2
@logged(logging.CRITICAL,'example')
def spam():
print('Spam')
# 正常调用3
# @logged()
def sub(x,y):
return x - y
# 非正常调用,会报错
@logged
def sub(x,y):
return x - y
# 因为如上方式等价于
sub = logged(sub) # 即第一个参数是被装饰函数本身,但是logged实际需要是接收的是level,name和message三个参数
# sub()
# 如何改进?
def logged2(func = None,*, level=logging.DEBUG,name=None,message=None):
if func is None:
return partial(logged2,level = level,name = name,message = message)
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
def wrapper(*args,**kwargs):
log.log(level,logmsg)
print('{}:{}'.format(logname,logmsg))
return func(*args,**kwargs)
return wrapper
"""
如下方式等价于:
sub2 = logged2(sub2) # 即第一个参数是被装饰函数本身
# 此时sub2其实已经变成wrapper
sub2()
由于第一个参数不为空,所以直接走logger2 if后面的逻辑
"""
# 1. 没有任何参数传递(注意这里并没有使用(),和装饰器的`普通用法`保持一致)
@logged2
def sub2(x,y):
return x - y
"""
执行流程
1. 由于logger2中func参数未传入,所以为None,会返回一个偏函数partial(logged2,level = level,name = name,message = message),假设是s.所以第一句执行完成后,变成
@s
def add2(x,y)
2. add2 = s(add2),此时func参数不为None,所以直接走if后面的逻辑。最终返回的add2其实已经是 wrapper 了
3. add2() --> 本质上是执行wrapper()
"""
# 2. 如果传递参数
@logged2(level=logging.CRITICAL,name='add2',message='add2被执行了')
def add2(x,y):
return x + y
if __name__ == '__main__':
print('======logger 正常有参测试==========')
result = add(1,2) #3
print(result) #Spam
spam()
# result2 = sub(3,10)
# print(result2)
print('=====logger2 sub2无参测试===========')
result2 = sub2(10,2)
# __main__:sub2
print('result2 = {}'.format(result2)) # result2 = 8
print('=====logger2 add2有参测试===========')
result3 = add2(10, 2)
#add2:add2被执行了
print('result3 = {}'.format(result3)) # result3 = 12
三、可自定义属性的装饰器
对应cookbook章节 9.5
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2023/8/16 21:17
# @Author : Maple
# @File : 05-可自定义属性的装饰器.py
import logging
from functools import partial, wraps
"""
cookbook 9.5
"""
def attach_wrapper(obj,func = None):
if func is None:
return partial(attach_wrapper,obj)
setattr(obj,func.__name__,func)
return func
def logged(level,name = None ,msg = None):
def decorate(func):
log_name = name if name else func.__module__
log = logging.getLogger(log_name)
log_msg = msg if msg else func.__name__
@wraps(func)
def wrapper(*args,**kwargs):
log.log(level, log_msg)
print('level:',level,'log_msg',log_msg)
func(*args,**kwargs)
# 通过装饰器,给wrapper设置一个属性,属性名字就是被装饰函数:即set_level,然后属性值是 该函数的引用
# 所以调用wrapper的set_level属性时,本质上就是在调用该方法。然后该方法会修改 level的值
@attach_wrapper(wrapper)
def set_level(new_level):
# 申明该变量不是函数set_level中的局部变量,而是函数之外 logged函数中的参数
nonlocal level
level = new_level
@attach_wrapper(wrapper)
def set_message(new_msg):
# 注意这里不要去改msg的值,而是要去修改log_msg
# 1. 因为方法调用set_message的时候,首先跳到这里修改局部变量的值
# 2. 然后跳到 wrapper方法,所以 如果修改msg的值, 由于[log_msg = msg if msg else func.__name__]这段代码(在wrapper上面)根本不会执行,所以msg的值也就无法改变
nonlocal log_msg
log_msg = new_msg
return wrapper
return decorate
@logged(level=logging.DEBUG,name = '罗二',msg="你也有今天")
def add(a,b):
return a + b
if __name__ == '__main__':
result = add(1,2)
print('----------------')
# 执行顺序
## 1. add此时是 wrapper
## 2. 而 set_level是wrapper中的一个实例属性,且对用的值是fun set_level
## 3. 因此以下语句会执行set_level逻辑,将level 修改为logging.INFO)
add.set_level(logging.INFO)
## 4. 执行 wrapper中的逻辑:log.log(level, log_msg).....,注意此时level已经被修改成logging.INFO
result = add(1, 2)
print('----------------')
add.set_message('好汉不提当年勇')
result = add(1, 2)
四、强制参数类型检测
对应cookbook章节 9.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2023/8/5 0:53
# @Author : Maple
# @File : 03-强制参数类型检测.py
from inspect import signature
from functools import wraps
"""
对应cookbook 9.7
"""
def typeassert(*ty_args,**ty_kwargs):
def decorate(func):
# 如果是优化执行模式,不进行参数类型检查
if not __debug__:
return func
sig = signature(func)
# 自定义的参数类型限制条件,Sample:OrderedDict([('x', <class 'int'>), ('z', <class 'int'>)])
bound_types = sig.bind_partial(*ty_args,**ty_kwargs).arguments
print('bound_types',bound_types) # OrderedDict([('x', <class 'int'>), ('z', <class 'int'>)])
@wraps(func)
def wrapper(*args,**kwargs):
# 被装饰函数所有实参的sig 字典,bound_values.arguments:OrderedDict([('x', 1), ('y', 2), ('z', 3)])
bound_values = sig.bind(*args,**kwargs) # <BoundArguments (a=1, b=2, c=1)>
for name,value in bound_values.arguments.items():
if name in bound_types:
if not isinstance(value,bound_types[name]):
raise TypeError('参数{} Expect type {}'.format(name, bound_types[name]))
return func(*args,**kwargs)
return wrapper
return decorate
@typeassert(int,c=str)
def add(a,b,c='maple'):
print(a,b,c)
if __name__ == '__main__':
sig = signature(add)
bound_values = sig.bind(1,2,c = 1)
print(bound_values.arguments)
add(1,2,'Maple')
try:
add('I', 2, 'Maple') # TypeError: 参数a Expect type <class 'int'>
except TypeError as e:
print(e)
try:
add(1, 3, 3)
except TypeError as e:
print(e) # 参数c Expect type <class 'str'>
五、类装饰器
对应cookbook章节:9.8 和 9.9
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2023/8/5 1:55
# @Author : Maple
# @File : 04-装饰器类.py
"""
对应cookbook章节:9.8 和 9.9
"""
# 1. 将装饰器定义为类的一部分
import types
from functools import wraps
class A:
def decorator1(self,func):
@wraps(func)
def wrapper(*args,**kwargs): # 内部的wrapper方法通常不需要传入self 或者 cls
print('decorator1')
return func(*args,**kwargs)
return wrapper
@classmethod
def decorator2(cls, func):
def wrapper(*args, **kwargs):
print('decorator2')
return func(*args, *kwargs)
return wrapper
a = A()
@a.decorator1
def add(a,b):
return a + b
@A.decorator2
def sub(a,b):
return a - b
"""
@property 的getter(), setter(), deleter(),其实就是在类中定义的三个装饰器
"""
class Person:
def __init__(self,name):
self._first_name = name
# 创建一个类的实例
first_name = property()
# 通过类的实例中的getter方法,实现装饰器的功能
@first_name.getter
def first_name(self):
return self._first_name
@first_name.setter
def first_name(self,value):
if not isinstance(value,str):
raise TypeError('Expect str type')
# 2. 将类定义为装饰器
# !!!get 方法:还是没有彻底理解
class Profiled:
def __init__(self,func):
wraps(func)(self)
self.ncalls = 0
def __call__(self, *args, **kwargs):
self.ncalls += 1
print('装饰器类被调用了{}次'.format(self.ncalls))
return self.__wrapped__(*args,**kwargs)
def __get__(self, instance, cls):
if instance is None:
return self
else:
# 该方法是为实例/类动态增加方法,第一个参数应该是方法,但这里是Profiled类的一个实例, 不懂~~
## 返回结果是
print('get 方法被调用')
print('self',self)
print('instance:',instance)
print('cls:',cls)
return types.MethodType(self,instance)
@Profiled
def mul(a,b):
return a * b
class Spam:
@Profiled
def div(self,a,b):
return a / b
class Spam2:
def div(self,a,b):
return a / b
if __name__ == '__main__':
result1 = add(1,2)
print(result1)
print('=================')
result2 = sub(10,2)
print(result2)
print('=================')
p = Person('Maple')
print(p.first_name)
# p.first_name = 12
print('======将类定义为装饰器测试===========')
mul(1,2) # 装饰器类被调用了1次
mul(1,2) # 装饰器类被调用了2次
print(mul.ncalls) # 2
print('---------------------------')
"""
执行步骤:
1. 实例化Profiled,Profiled 装饰Spam,然后才实例化Spam,得到s
2. 调用s.div(10,2),进到Profiled类的get方法,返回一个Spam的bound方法
3. 调用Profiled的call 方法,执行self.__wrapped__(*args,**kwargs),其实self.__wrapped__为Spam的bound方法(Spam.div,就是被装饰的函数)
,执行spam.div()方法,得到最终结果
"""
s = Spam()
print('s',s)
s.div(10,2) # 装饰器类被调用了1次
s.div(15, 5) # 装饰器类被调用了1次
print(s.div.ncalls) # 2
# 装饰器的用法等价于:
print('======装饰器的用法等价测试===========')
s2 = Spam2()
s2 = Profiled(s2.div)
print(s2(15,3))
- 补充说明s.div(10,2)的详细执行过程
(1)前置知识-绑定方法
先来看一个例子:
import types
class MyClass:
def __init__(self, value):
self.value = value
def show_value(self, extra):
print(f'Value: {self.value}, Extra: {extra}')
# 创建 MyClass 的实例
instance = MyClass(10)
# 创建一个普通的函数
def external_function(self, extra):
print(f'External Function - Value: {self.value}, Extra: {extra}')
# 使用 types.MethodType 将 external_function 绑定到 instance 上
bound_method = types.MethodType(external_function, instance)
# 调用绑定方法
bound_method(5)
上例中bound_method就是一个绑定方法,当执行bound_method(5)时,instance会作为第一个参数传递到external_function中: 即self
(2)s.div(10,2)执行流程分析
2.1 s.div执行流程
①首先,因为div被@Profiled装饰,所以div其实是profiled的一个实例
②同时,因为Profiled类实现了__get__方法,所以本质上是一个描述器
因此当执行s.div时,会去执行Profiled类中的__get__方法,而该方法,会返回一个绑定方法:types.MethodType(self,instance)。 本质上就是将profiled实例绑定到Spam的实例s上。
假设该绑定方法就是bound_method
2.2 s.div(2,10)执行流程--> bound_method(2,10)
① 上述绑定的对象时profiled实例,因此当执行绑定方法时,会执行profiled的__call__方法
需要结合以下2点理解:1.执行绑定方法时,实际就是执行被绑定对象();2.Profield对象实现了call方法,调用对象(),其实就会调用该对象的call方法
② __call__方法中的self是profiled实例对象本身-这是又类中定义的方法决定的,然后s作为一个参数会传递给args,同时2和10作为后续的参数,也会传递给args
2.3 执行s.div本身的逻辑,比较好理解,不再赘述
简单补充说明:s.div中的3个参数: self-s实例,10,2 即来自上一步的args参数