Python笔记 - 函数、方法和类装饰器
前言
装饰器最早出现是Python2.4版本,为什么这个版本开始存在?这是因为最早标记类方法的做法是:
def foo(self):
perform method operation
foo = classmethod(foo)
当方法体过长的时候很容易让人忽略掉最后的语句classmethod(foo)
,为了解决这个问题,从2.4版本引入了装饰器。
什么是装饰器?
它允许用户在不修改原有函数或方法定义的情况下,给函数或方法添加额外的功能。 装饰器本质上是一个可调用对象(比如函数),它接受一个函数作为参数,并返回一个新的函数或可调用对象。
最简单的理解就是类比代理,比如需要记录函数运行时间,我们的做法是方法执行前获取时间,执行完毕获取时间。
侵入式的做法:
def func():
start = time.time()
time.sleep(3)
end = time.time()
print(f"耗时: {end - start:.2f}")
用装饰器的做法则为:
def log_time(fn):
def wrapper(*args, **kwargs):
start = time.time()
fn(*args, **kwargs)
end = time.time()
print(f"耗时: {end - start:.2f}")
return wrapper
@log_time
def func():
time.sleep(3)
其中log_time
这个函数就是装饰器。
装饰器的分类
装饰器的本质是一个可执行对象,因为可执行对象包括函数和类,因此装饰器分为两种:
- 函数、方法装饰器
- 类装饰器
如果细分的话,有且只有下面四种(记住这句话有助于在使用上理清思路):
- 不带参数的函数装饰器
- 带参数的函数装饰器
- 不带参数的类装饰器
- 带参数的类装饰器
如果像上面那样def
定义一个函数就是函数装饰器,如果使用class
定义一个类,这个类就是类装饰器。
Note:
- 函数指得是普通函数,方法指的是定义在类中的函数,通常第一个参数带有(self)这种。
- 其实按照PEP 3129-Class Decorators类装饰器为:装饰类的函数装饰器
按照装饰器本身的分类,可以分为函数和类;按照装饰对象,可以装饰函数和类;按照装饰对象,即PEP 3129的定义我更源称其为装饰类的函数装饰器。
也就是说按照PEP文档描述,上图序号1才是类装饰器,而不存在序号B的概念。
但我个人更愿称序号1为装饰类的函数装饰器,而序号B才是类装饰器,至于装饰类还是函数,无关紧要;下文分类也是按照A、B分为函数装饰器和类装饰器。
函数装饰器
1. 不带参数的函数装饰器
基本格式
@dec2
@dec1
def func(arg1, arg2, ...):
pass
上面的使用等价👇:
func = dec2(dec1(func))
具体的例子
# 定义不带参数的函数装饰器
def dec2(fn):
def wrapper(*args, **kwargs):
print("dec2函数执行前...")
fn(*args, **kwargs)
print("dec2函数执行后...")
return wrapper
def dec1(fn):
def wrapper(*args, **kwargs):
print("dec1函数执行前...")
fn(*args, **kwargs)
print("dec1函数执行后...")
return wrapper
# 使用装饰器
@dec2
@dec1
def func():
print("函数执行中...")
if __name__ == '__main__':
func()
输出:
理解
dec2装饰器注释代码:
# 这里的fn = dec1(func)
def dec2(fn):
# 1. wrapper是对 fn = dec1(func)的封装函数。
# 2. 他需要将 wrapper 这个包装后的函数返回,可以理解为代理后的函数。
# 3. 因此 dec2(fn) 返回的是一个装饰后的函数,本质功能还是最原始的 func 。
# 4. wrapper()里边的参数就是原函数的参数。
def wrapper(*args, **kwargs):
print("dec2函数执行前...")
fn(*args, **kwargs)
print("dec2函数执行后...")
return wrapper
dec1装饰器注释代码
# 这里的fn = func
def dec1(fn):
# 1. wrapper是对 fn = func的封装函数。
# 2. dec2(dec1(func)) 需要理解dec2的参数,即当前这个wrapper函数。
# 3. 因此对于存在两个装饰器的例子,本质是嵌套,即:封装 “封装后的函数”
def wrapper(*args, **kwargs):
print("dec1函数执行前...")
fn(*args, **kwargs)
print("dec1函数执行后...")
return wrapper
2. 带参数的函数装饰器
基本格式
@decomaker(argA, argB, ...)
def func(arg1, arg2, ...):
pass
上面的使用等价👇:
func = decomaker(argA, argB, ...)(func)
具体的例子
def attrs(**kwargs):
def decorate(fn):
# 为函数添加属性
for k, v in kwargs.items():
setattr(fn, k, v)
return fn
return decorate
@attrs(name="test", age=18)
def func():
print("函数执行中...")
if __name__ == '__main__':
func()
print(func.__dict__)
输出:
理解
@attrs(name="test", age=18)
def func():
print("函数执行中...")
这部分代码等价于:
attrs(name="test", age=18)(func)
# 而attrs(name="test", age=18) 的返回值是 decorate 装饰器
# 装饰器 decorate 的参数才是原本的函数,在装饰器内对函数添加了属性
要点总结:
- 带参数的函数装饰器返回一个装饰器
- 真正的装饰器参数为目标函数
- 封装的函数参数等同目标函数参数
加深理解例子
基本格式
@accepts(int, int)
@returns(int)
def func(a, b):
return a + b
上面的使用等价👇:
accepts(int,int)(returns(int)(func))
完整代码
def accepts(*types):
def check_accepts(f):
def wrapper(*args):
if not all(isinstance(arg, types) for arg in args):
raise TypeError("Argument %s is not of type %s" % (args, types))
return f(*args)
return wrapper
return check_accepts
def returns(return_type):
def check_returns(f):
def wrapper(*args):
result = f(*args)
if not isinstance(result, return_type):
raise TypeError("Return value is not of type %s" % return_type)
return result
return wrapper
return check_returns
@accepts(int, int)
@returns(int)
def func(a, b):
return a + b
if __name__ == '__main__':
print(func(1, 1))
理解
先不看 @accepts(int, int)。
@returns(int)
def func(a, b):
return a + b
Q1:上面的执行结果是什么?
上面的使用等价👇:
returns(int)(func)
而returns函数注释如下:
# 1. return_type 就是 @returns(int)的入参,即 int
def returns(return_type):
# 2. 带参数的函数装饰器,需要返回一个装饰器函数,即 check_returns
# 3. 装饰器函数的参数就是目标函数,即 f = func
# 4. 装饰器函数的返回值是包装函数,即 wrapper
# 5. 包装函数等价目标函数,其参数为目标返回参数,返回值等于目标函数返回值
def check_returns(f):
def wrapper(*args):
result = f(*args)
if not isinstance(result, return_type):
raise TypeError("Return value is not of type %s" % return_type)
return result
return wrapper
return check_returns
Q2:returns(int)的执行结果是什么?
返回的是新的装饰器,上面的例子即check_returns函数,这部分完全可以不用看内部实现:
def returns(return_type):
def check_returns(f):
# 其他代码
return check_returns
returns(int) 就是函数的调用,他的输入是:int 这是一个class对象,他的输出是:check_returns函数。
Q3:returns(int)(func)的执行结果是什么?
返回的是新的目标函数,上面的例子即wrapper函数,我们已经知道returns(int)返回的是check_returns函数,这里实际则是将目标函数func作为参数传递并且调用。
def check_returns(f):
def wrapper(*args):
# 其他代码
return wrapper
而这整个执行结果返回的是啥?就是新的目标函数,或者称包装函数 wrapper。
Q4:这个包装函数有什么讲究?
包装函数的入参和返回值都和目标函数保持一致。
@returns(int)
def func(a,b):
return a + b
上面的结果等价**定义一个函数(注意是定义不是调用)**👇:
def wrapper(*args):
result = f(*args)
if not isinstance(result, return_type):
raise TypeError("Return value is not of type %s" % return_type)
return result
所以,完整的调用就等价下面的形式:
@accepts(int,int)
@returns(int)
def func(a,b):
return a + b
等价
accepts(int,int)(returns(int)(func))
等价
accepts(int,int)(wrapper)
那剩下的分析,就完全一样的,只是函数装饰器accepts的目标函数是原先函数的包装函数wrapper。
accepts函数注释代码:
def accepts(*types):
# 1. 这是一个带参数的函数装饰器,因此需要返回一个装饰器,即 return check_accepts
# 2. 装饰器(check_accepts)的参数是目标函数,即 f,这里的目标函数f 是被@returns装饰后的函数 wrapper
# 3. 装饰器的返回值是包装后的函数,即 return wrapper
def check_accepts(f):
# 4. 包装函数等价函数本身,他的参数就是目标函数的参数,即 *args 为@returns装饰后的函数 wrapper参数 *args
def wrapper(*args):
if not all(isinstance(arg, types) for arg in args):
raise TypeError("Argument %s is not of type %s" % (args, types))
return f(*args)
return wrapper
return check_accepts
Q5:最后执行的返回结果是什么?
@accepts(int,int)
@returns(int)
def func(a, b):
return a + b
上面的执行结果是返回一个进行二次包装的函数,上面的代码是定义函数,而不是调用。
Q6:什么时候调用?
if __name__ == '__main__':
print(func(1, 1))
当执行这行代码的时候,实际上是调用accepts内部定义的wrapper函数,参数是(1,1)。
def wrapper(*args):
if not all(isinstance(arg, types) for arg in args):
raise TypeError("Argument %s is not of type %s" % (args, types))
return f(*args)
而这个函数的返回值是 f(*args)
,也就是原本函数返回啥,这里就返回啥。
到这里,整个分析就结束了;上面装饰器(@returns(int)和@accepts(int,int)) 的作用没有侵入原函数的情况下,对原函数的参数和返回值都进行了校验。
类装饰器
前置知识
在Python中,如果一个类 class A
没有定义 __init__
方法、__call__
方法或者 __new__
方法,那么当你尝试直接实例化这个类(即使用 A()
)时,会发生以下情况:
__new__
方法:- 实际上,每个类都有一个
__new__
方法,它是用来创建类实例的。 - 如果你没有显式地定义
__new__
方法,那么Python会使用从它的父类(通常是object
)继承的默认__new__
方法。 - 默认
__new__
方法负责分配内存并返回一个实例(通常是通过调用super().__new__(cls)
来实现的,但在没有继承关系时,直接使用object.__new__(cls)
)。
- 实际上,每个类都有一个
__init__
方法:- 如果没有定义
__init__
方法,那么实例被创建后,不会有任何初始化代码被执行。 - 这意味着实例将仅包含从其父类继承的属性和方法(如果有的话)。
- 如果没有定义
__call__
方法:__call__
方法与直接实例化类无关。它是当类的实例被当作函数调用时(例如instance()
)才会被调用的。- 因此,在
A()
的上下文中,__call__
方法不会被考虑。
当我们使用类装饰器的时候,通常意味着这个类实例是可调用对象,也就是说声明了__call__方法。
比如:
class A:
def __call__(self, *args, **kwargs):
print("__call__方法...")
if __name__ == '__main__':
a = A()
a()
类装饰器的重点在于:实例化之后再通过()调用。
1. 不带参数的类装饰器
基本格式
@Deco
class Demo:
pass
上面的使用等价👇:
Deco(Demo)
上面执行完返回的是Deco这个类实例对象。
具体的例子
class Deco:
def __init__(self, cls):
self.cls = cls
def __call__(self, *args, **kwargs):
return self.cls(*args, **kwargs)
@Deco
class Demo:
pass
if __name__ == '__main__':
print(Demo())
理解
@Deco
class Demo:
pass
上面的代码实际上是等价Deco(Demo)
,也就是说实例化Deco这个类,并且参数为Demo这个类对象。也就是执行__init__
方法,因此参数cls就是目标类Demo这个类对象。
Q1:那么正常调用Demo()获取实例对象怎么办?
因此@Deco对Demo类的装饰,导致此声明返回的是一个Deco实例对象,也就是说,Demo()实际上是instance(),其中instance是Deco类对象。
前置知识说了,要让一个类实例可以通过()调用,需要声明__call__
方法,因此在__call__
方法内部,需要把原本Demo()返回的实例对象进行返回,即:
def __call__(self, *args, **kwargs):
return self.cls(*args, **kwargs)
要点总结:
- 用类装饰器装饰后返回的是一个类装饰器对象。
- 这个类装饰器对象必须定义call方法才能作为可调用对象返回原本的类实例。
加深理解的例子
基本格式
@Deco2
@Deco1
class A:
pass
上面的使用等价👇:
Deco2(Deco1(A))
上面的代码执行完返回的是:Deco2这个类实例对象。
具备注释的完整代码
# 1. @Deco2 等价 Deco2() 由于参数即 @Deco1 装饰后返回的是 Deco1 实例对象
class Deco2:
# 2. 执行Deco2() 时候会调用 __init__ 方法,而此处的参数为 Deco1 实例对象,即可调用的对象
def __init__(self, callable_obj):
self.callable_obj = callable_obj
# 3. 当使用 Demo() 的时候,最终是调用这里的 __call__ 方法
def __call__(self, *args, **kwargs):
return self.callable_obj(*args, **kwargs)
# 1. @Deco1 等价 Deco1() 且参数为类 Demo
class Deco1:
# 2. 执行Deco1(Demo) 时候会调用 __init__ 方法,所以__init__方法的参数就是目标类 Demo
def __init__(self, cls):
self.cls = cls
# 3. 由于返回的是类实例对象,因此一定要定义__call__方法,实例对象才是可调用对象,并且返回目标类实例对象
def __call__(self, *args, **kwargs):
return self.cls(*args, **kwargs)
@Deco2
@Deco1
class Demo:
pass
if __name__ == '__main__':
print(Demo())
2. 带参数的类装饰器
基本格式
@Deco(name="Tom")
class Demo:
pass
上面的使用等价👇:
Deco(name="Tom")(Demo)
具体的例子
class Deco:
def __init__(self, name):
self.name = name
def __call__(self, cls):
setattr(cls, "name", self.name)
return cls
@Deco(name="Tom")
class Demo:
pass
if __name__ == '__main__':
print(Demo.name)
print(Demo())
理解
无论是函数装饰器还是类装饰器,带参数和不带参数的本质区别是使用了()表示立即调用。
由于上面的使用等价:
Deco(name="Tom")(Demo)
而Deco(name="Tom")
已经返回了Deco类实例对象,因此实例对象(Demo)的时候,实际调用的就是__call__
方法,因此这种情况下,目标类Demo,在__call__
方法内进行传递,而不是__init__
方法。
加深理解的例子
基本格式
@Deco2(name="Tom")
@Deco1(age=18)
class Demo:
pass
上面的使用等价👇:
Deco2(name="Tom")(Deco1(age=18)(Demo))
具备注释的完整代码
# 1. 由于上一步返回的就是目标类对象,因此这里的使用完全等价 Deco1
class Deco2:
def __init__(self, name):
self.name = name
def __call__(self, cls):
setattr(cls, "name", self.name)
return cls
# 1. @Deco1(age=18) 等价 Deco1(age=18)(Demo)
# 2. 由于 Deco1(age=18) 已经是实例化对象了,因此 Deco1(age=18)(Demo) 实际上是调用 __call__方法
# 3. 所以装饰后的结果返回的是 __call__ 方法的返回值!
class Deco1:
def __init__(self, age):
self.age = age
def __call__(self, cls):
setattr(cls, "age", self.age)
return cls
@Deco2(name="Tom")
@Deco1(age=18)
class Demo:
pass
if __name__ == '__main__':
print(Demo)
print(Demo.name)
print(Demo.age)
要点总结
- @语法糖,就可以理解当前装饰器后面加一个()表示立即调用,其参数为装饰的目标对象(类、函数或者可调用的实例对象)
- 装饰器的本质是一个可调用对象(函数,类)
- 装饰器的输入是一个可调用对象(函数,类,可调用的实例对象)
- 装饰器的输出是一个可调用对象(函数,类,可调用的实例对象)
使用场景例子
1. 不带参数的函数装饰器
@abstractmethod
用于标记一个类方法为抽象方法。
源码如下:
def abstractmethod(funcobj):
funcobj.__isabstractmethod__ = True
return funcobj
Note:不带参数的函数装饰器返回函数本身(或者包装后的函数),通常只是对原有函数做一些封装。
2. 带参数的函数装饰器
@attrs
用于为目标函数添加属性。
源码如下:
def attrs(**kwargs):
def decorate(fn):
# 为函数添加属性
for k, v in kwargs.items():
setattr(fn, k, v)
return fn
return decorate
使用:
@attrs(author="tough",version="1.0")
def add(a, b)
return a + b
Note:带参数的函数装饰器通常可以更灵活的利用参数设置一些我们期望的信息。
3. 不带参数的类装饰器
类装饰器一个经典的用法是,利用类仅做容器作用,真正的装饰器还是函数装饰器,即定义在类内部的函数。
class Decorator:
def info(self, fn):
print(fn.__name__)
return fn
decorator = Decorator()
@decorator.info
def add(a, b):
return a + b
if __name__ == '__main__':
add(1, 1)
另外常见的如:@staticmethod, @classmethod 均为不带参数的类装饰器。
4. 带参数的类装饰器
比如Flask框架中的使用:
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'This Index Page'
利用带参数的类装饰器实现元注解功能:
import types
from enum import Enum
class Annotation:
class ElementType(Enum):
class Type:
def __init__(self, name, value, label):
self.name = name
self.value = value
self.label = label
TYPE = Type('type', type, '类')
METHOD = Type('method', types.FunctionType, '函数')
def __init__(self, elementType: ElementType):
self.elementType = elementType
def __call__(self, cls):
cls.elementType = self.elementType
return cls
@Annotation(Annotation.ElementType.METHOD)
class AliaFor:
def __init__(self, cls):
self.cls = cls
def __call__(self, *args, **kwargs):
if not isinstance(self.cls, self.elementType.value.value):
raise TypeError(f"{self.cls.__name__} 不是一个{self.elementType.value.label},其类型为{type(self.cls).__name__},@AliaFor只能装饰{self.elementType.value.label}。")
return self.cls(*args, **kwargs)
@AliaFor
class A:
pass
if __name__ == '__main__':
print(A())
上面的代码功能为@Annotation(Annotation.ElementType.METHOD)标记一个类为注解类,只能作用在方法上,如果作用在类上则报错。
输出:
🔗参考链接
[1]:官方文档函数装饰器 PEP 318
[2]:官方文档类装饰器 PEP 3129
[3]:博客 # 小白了解Python中类装饰器,看这篇就够了
[4]:博客 # Python笔记 - 利用装饰器设计注解体系