Python装饰器深入理解
在本博客的前面部分其实已经介绍过Python中装饰器的基本使用了(Python迭代器、生成器、装饰器的使用,Python@函数装饰器以及super()父类继承 ),不过还有一些深入的知识点(带参数的装饰器,保存原始函数的信息,使用装饰函数装饰类)需要我们掌握。
1、带参数装饰器
不要期望装饰器可以即接收被装饰方法又接收其他关键字参数,因为装饰器在被调用过程中只能接收被调用方法(被装饰函数)作为唯一参数。那带参数的装饰器又如何实现的呢?创造一个带参数并且可以返回装饰器的函数。例如下面这个例子:
首先这个创造一个非常普通的装饰器,该装饰器的功能就是打印一句话,并把被装饰函数得到的结果返回出来。
def my_decorator(decorated):
## 该函数会被装饰器返回
def inner(*args, **kwargs):
result = decorated(*args, **kwargs)
print("已执行装饰器内容")
return result
return inner
@my_decorator
def my_fun(a, b):
return a+b
print(my_fun(1,3))
最终的执行结果是:
接下来我们将装饰器my_decorator进行改造,将其封装在另一个函数parameter_decorator中。
def parameter_decorator(value=1):
## 这是真正的装饰器
def my_decorator(decorated):
def inner(*args, **kwargs):
result = decorated(*args, **kwargs)
print("已执行装饰器内容")
print("装饰器接收到的参数是: ", value)
return result
return inner
return my_decorator ## 将my_decorator装饰器返回
@parameter_decorator(10)
def my_fun(a, b):
return a+b
print(my_fun(1,3))
最终的结果是:
我们注意到在使用装饰器的时候,我们将10作为参数,传递给了parameter_decorator函数中。此时要注意,parameter_decorator函数并不是一个装饰器,他只是一个函数,python解析器在执行到第12行时会立刻执行parameter_decorator(10), 并将返回得到my_decorator这个装饰器,不过此时这个装饰器内部已经获取到了10这个参数信息,并会执行到第七行时打印这个10。因此当解析器执行完第12行后,其实接下来的过程等价于在执行下面例子。
def my_decorator(decorated):
value = 10
def inner(*args, **kwargs):
result = decorated(*args, **kwargs)
print("已执行装饰器内容")
print("装饰器接收到的参数是: ", value)
return result
return inner
@my_decorator
def my_fun(a, b):
return a+b
所有我们可以看到其实带参数的装饰器本质上是:一个可以接收参数,执行函数体后返回一个装饰器的函数。所以我们可以看到使用的是@parameter_decorator(10)。需要注意的是第12行不能改成@parameter_decorator,因为@parameter_decorator指的是将parameter_decorator作为一个装饰器使用,也就是说会默认传递被装饰函数作为唯一参数(即value会指向my_fun),这显然不是我们所希望看的。如果你想使用parameter_decorator默认的参数(即想默认传入value=1),我们可以将第12行改为@parameter_decorator() 注意这里的括号是不能省略的,因为其本质含义都是需要先执行parameter_decorator(),而不是将parameter_decorator视为一个装饰器。
其实写到这里,我们也对装饰器的本质有了更深的认识,其实装饰器就是一个函数,只不过这个函数的第一个默认参数会被默认传递为被装饰函数,并且该函数需要返回一个函数,在Python执行过程中这个返回的函数会被赋值给被装饰函数的函数名引用。
2、保存被装饰函数帮助信息
前面我们已经提到了,我们装饰函数实质上是会将被装饰的函数进行一层封装,并返回一个新的函数,并且原本的被装饰函数的函数名指向这个新函数,这就意味着my_fun这个引用指向的已经不是再是原本我们定义的函数了。我们看下面这个例子能更加清晰的发现问题。对于函数my_fun我们直接通过help(my_fun)打印其帮助文档。
def my_fun(a, b):
"""这是一个被装饰函数的doc说明.
"""
return a+b
help(my_fun)
返回的结果是:
接下来我们使用一个装饰函数对其进行装饰,并再打印其帮助文档。
def my_decorator(decorated):
def inner(*args, **kwargs):
"""这是装饰函数返回的函数
"""
result = decorated(*args, **kwargs)
print("已执行装饰器内容")
return result
return inner
@my_decorator
def my_fun(a, b):
"""这是一个被装饰函数的doc说明.
"""
return a+b
help(my_fun)
得到的结果如下:
这个问题我们在这篇文章(Python迭代器、生成器、装饰器的使用)的最后也已经提出来了,现在我们再看其实能更清晰,被装饰后的函数的函数名my_fun已经不是指向的是我们原本定义的函数了,而是指向的my_decorator中定义的inner函数。这个结果显然给我们带来了一些麻烦,比如我们想通过help(my_fun)查看我们原本定义的函数功能,现在这种情况我们就无法查看到了,毕竟我们并不想知道这个装饰器做了那些工作(一般情况下装饰器都是为了在执行被装饰函数之前或者之后添加一些额外的辅助功能,比如注册,检查参数传递是否符合规范,将函数结果记录到指定位置等)。因此我们可以使用functools包中提供的@functools.wraps()装饰器,用来保存函数的帮助信息。代码如下
import functools
def my_decorator(decorated):
@functools.wraps(decorated)
def inner(*args, **kwargs):
"""这是装饰函数返回的函数
"""
result = decorated(*args, **kwargs)
print("已执行装饰器内容")
return result
return inner
@my_decorator
def my_fun(a, b):
"""这是一个被装饰函数的doc说明.
"""
return a+b
help(my_fun)
print(my_fun.__name__)
相比上一段代码,我们只是在inner函数之前使用了@functools.wraps(decorated)这个带参数的装饰器,对inner函数进行装饰,这个时候decorated的核心信息,就会被绑定在inner中。此时我们得到的结果就是:
已经完全解决了这篇文章(Python迭代器、生成器、装饰器的使用)的最后出现的问题。因此我们推荐在使用装饰器是,在内部一定要使用@functools.wraps(decorated),用来保存被装饰函数的i信息,否则在调代码的过程中会出现很多问题。
3、类型转换
目前我们知道装饰器最后需要返回的是一个函数,但其实Python中并没有这样规定,我们在这篇文章(Python迭代器、生成器、装饰器的使用)第三部分装饰器介绍的第一个例子中其实装饰器返回的就是一个字符串,并使用了原来的函数名指向了这个字符串(这是我们不推荐的,因为这会导致之后的编程人员并不知道这个装饰器发生了什么。因此对于装饰器来说我们最好保证返回的与原来具有一样操作行为的同样的对象,毕竟装饰器的存在是为了做辅助工作,而并不是改变原本函数工作逻辑)。但我们可以返回一个装饰器类,也就是在my_decorated函数中返回一个类,而不是一个函数,我们通过这个类的方法来执行真正的被装饰函数。例如下面这个例子。
def my_decorator(decorated):
class Task(object):
def run(self, *args, **kwargs): ## 用于执行被装饰函数
return decorated(*args, **kwargs)
def identify(self):
print("我是一个Task类")
return Task
@my_decorator
def my_fun(a, b):
return a+b
f = my_fun() ## 注意此处不能传递参数,因为此时my_fun指向的的是一个Task类
print("此时my_fun指向的是: ", my_fun)
print(f.run(1,2)) ## 在此处才传入参数,执行1+2
f.identify()
结果为:
我们在my_decorated函数中定义了一个类,这个类的run方法是真正执行my_fun函数逻辑的地方。我们将Task这个类作为my_decorated装饰器的返回值,也就是说在第16行中my_fun()其实执行的是Task()得到了一个Task实例对象,这个结果也在17行得到了验证,我们可以看到my_fun此时指向的是my_decorated中的一个类。真正执行被装饰函数的函数体是在f.run(1,2)。我们也可以使用f.identify()打印出Task类的所特有的方法。
如果大家仔细看其实可以发现,上述代码并不满足我们在本小节开头提出的原则:我们最好保证返回的与原来具有一样操作行为的同样的对象。因为如果我们要执行被装饰的代码逻辑,我们学要写成my_fun().run(1, 2),这显然与我们希望实现my_fun(1, 2)是有差距的,那我们该如何解决的,其实只要重写Task类的__call__方法并在my_decorated装饰器返回时直接返回类的对象实例即可。代码改进如下:
def my_decorator(decorated):
class Task(object):
def run(self, *args, **kwargs): ## 用于执行被装饰函数
return decorated(*args, **kwargs)
def identify(self):
print("我是一个Task类")
def __call__(self, *args, **kwds):
return self.run(*args, **kwds)
return Task() ## 直接返回一个实例
@my_decorator
def my_fun(a, b):
return a+b
print("此时my_fun指向的是: ", my_fun)
print(my_fun(1,2))
my_fun.identify()
结果如下:
我们可以看到改进后的my_fun指向的就是一个实例对象了,并且我们可以直接使用my_fun(1,2)来完成调用对__call__的调用。在第22行我们也可以使用my_fun.identify()的方式来指向Task类中的其他函数。
通过对上面的讨论我们可以进一步加深了对装饰器的认识,装饰器其实是: 一个可调用函数(具有__call__方法的类 / 函数)接收一个可调用函数 并 返回一个可调用函数。而在Python中一个普通的函数以及一个类其实是同样的一个对象,可以被用于参数传递,也可以被调用,因此都可以被视为可调用函数。
4、使用装饰函数装饰类
通过上一小结的学习,我们加深了对装饰器的理解,这一小结我们学习一下既然类可以被视为可调用函数,那是否我们的装饰器(装饰函数)可以去装饰一个类呢?答案是肯定的,比如下面这个例子就是使用了一个装饰器,装饰了一个类,并改写了类的一些方法。
import functools
import time
def my_decorator(cls):
original_init = cls.__init__ ## 保存类原始初始化函数
@functools.wraps(original_init)
def new_init(self, *args, **kwargs): ## 一个新的对象初始化函数
original_init(self, *args, **kwargs)
self.created = time.time() ## 增加为类增加一个属性
def new_repr(self):
return f"{self.name}: {self.age} : {self.created}"
cls.__init__ = new_init
cls.__repr__ = new_repr
return cls
@my_decorator
class Myclass(object):
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self): ## 打印类时返回的信息
return f"{self.name}: {self.age}"
a = Myclass("小明", 14)
print(a)
print(a.created)
返回的结果是:
我们可以看到,我们在装饰器中将类的初始化方法以及__repr__方法进行了改写,在新的初始化方法中,我们新增了一个叫做created的属性,用于记录实例对象创建的时间,这个属性在原始类定义中是没有的。注意了我们对类的方法进行修改时最好还是采用@functools.wraps()方式保存下原来的类方法的信息。