目录
关于这个系列
《最值得收藏的python3语法汇总》,是我为了准备公众号“跟哥一起学python”上面视频教程而写的课件。整个课件将近200页,10w字,几乎囊括了python3所有的语法知识点。
你可以关注这个公众号“跟哥一起学python”,获取对应的视频和实例源码。
这是我和几位老程序员一起维护的个人公众号,全是原创性的干货编程类技术文章,欢迎关注。
1、概念和原理
如果你想对一个已有函数的功能进行扩展,而又不想修改这个函数,那么装饰器(decorator)就派上用场了。
比如,我们想对一个函数打点,测试其运行时间。如果不使用装饰器,我们可以这样实现:
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./14/14_1.py
import time
# 性能打点
def foo1():
t0 = time.perf_counter()
time.sleep(1)
t1 = time.perf_counter()
print(f'time spend: {t1 - t0} s')
def foo2():
t0 = time.perf_counter()
time.sleep(2)
t1 = time.perf_counter()
print(f'time spend: {t1 - t0} s')
foo1()
foo2()
输出为:
time spend: 0.9993565999999999 s
time spend: 2.0009466 s
可以看到,我们会给每个需要打点的函数加上计时打点的一段代码。这样写代码会带来两个问题:
- 打点功能本身不是该函数的核心功能,某种意义上它只是一个开发阶段的测试代码。这是对核心代码的一种污染。
- 打点功能是一个通用功能,如果我们在每个函数里面重复写这样一段代码,会显得很冗余。
这种情况下,装饰器是一个非常好的选择。代码如下:
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./14/14_2.py
import time
# 性能打点 - 装饰器方式
def perf_dot(func):
def inner():
t0 = time.perf_counter()
func()
t1 = time.perf_counter()
print(f'time spend: {t1 - t0} s')
return inner
@perf_dot
def foo1():
time.sleep(1)
@perf_dot
def foo2():
time.sleep(2)
foo1()
foo2()
输出为:
time spend: 1.000558 s
time spend: 2.000056 s
代码里面的@perf_dot,就是装饰器。我们将打点功能封装到一个函数perf_dot中,我们只需要在被测试函数头加上这个装饰器声明即可。
下面我们来看看装饰器的具体原理。
Perf_dot是一个两层嵌套的函数,在最后是return了内层函数对象,这是不是和我们前面讲的闭包很像?没错,它其实就是一个闭包(本实例没有引用外层变量,所以__closure__里面没有值)。
Perf_dot函数有一个入参func,这个入参其实就是这个装饰器要装饰的那个函数,比如foo1。
当解释器解析到@perf_dot这一句时,会将下一行的函数名作为实参传给perf_dot(foo1)。而这个函数会返回自己的内层函数对象,解释器将返回的对象赋值给foo1。
所以,当我们调用foo1()时,我们实际执行的是perf_dot的内层函数对象。我们可以打出foo1的名字:
print(foo1.__name__)
输出为:
Inner
解释器将我们要调用的函数,替换成了装饰器内层函数。所谓的功能扩展,就是在这个内层函数里面做文章。比如我们这个实例,在内层函数里面增加了打点功能,并且调用了真正的foo1函数功能。
这就是装饰器的本质,是不是挺简单?
下面我们对这个例子稍微修改一下,让函数带上参数:
import time
# 性能打点 - 装饰器方式
def perf_dot(func):
def inner(intv):
t0 = time.perf_counter()
func(intv)
t1 = time.perf_counter()
print(f'time spend: {t1 - t0} s')
return inner
@perf_dot
def foo1(intv):
time.sleep(intv)
@perf_dot
def foo2(intv):
time.sleep(intv)
foo1(1)
foo2(2)
只要装饰器内层函数的形参列表和foo的保持一致就可以。当我们调用foo1(param…)时,我们实际调用的是inner(param…)。
那么装饰器是否可以带参数呢?答案是肯定的。
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./14/14_2.py
import time
# 性能打点 - 装饰器方式
def perf_dot(name):
def outer(func):
def inner(intv):
t0 = time.perf_counter()
func(intv)
t1 = time.perf_counter()
print(f'{name} time spend: {t1 - t0} s')
return inner
return outer
@perf_dot('foo1')
def foo1(intv):
time.sleep(intv)
@perf_dot('foo2')
def foo2(intv):
time.sleep(intv)
foo1(1)
foo2(2)
输出为:
foo1 time spend: 1.0001801000000001 s
foo2 time spend: 2.0004163999999998 s
这个理解起来就稍微有点困难了。如果装饰器本身带有参数,那么装饰器函数是三层嵌套的。当执行到@perf_dot(‘foo1’)时,返回outer函数对象,它相当于不带参数的@outer。而我们传入的参数,则被封装在了闭包__closure__中。
装饰器甚至可以支持嵌套,也就是一个函数多个装饰器。比如我们希望在打点之前打印一行字符,那么我们新增一个装饰器。
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./14/14_3.py
import time
# 装饰器嵌套
def perf_dot_log(func):
def inner():
print('start dot...')
func()
print('finish dot...')
return inner
def perf_dot(func):
def inner():
t0 = time.perf_counter()
func()
t1 = time.perf_counter()
print(f'time spend: {t1 - t0} s')
return inner
@perf_dot_log
@perf_dot
def foo1():
time.sleep(1)
@perf_dot_log
@perf_dot
def foo2():
time.sleep(2)
foo1()
foo2()
输出为:
start dot...
time spend: 1.0003571 s
finish dot...
start dot...
time spend: 2.0000216999999996 s
finish dot...
我们知道,@perf_dot装饰器是将foo1替换为了它自己的内层函数对象inner。@perf_dot_log其实就是对这个inner函数再封装一层装饰器。所以foo1最终执行的是perf_dot_log的inner函数对象。它最终的调用关系是:perf_dot_log.inner -> perf_dot.inner -> foo1。
2、类装饰器
上一节我们是使用闭包函数来实现装饰器,我们也可以使用类来实现装饰器,叫做类装饰器。类装饰器需要实现一个特殊的方法__call__。
上面的打点测试的例子,我们可以采用类装饰器来实现,如下:
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./14/14_4.py
import time
# 性能打点 - 类装饰器
class PerfDot:
def __init__(self, func):
self.func = func
def __call__(self):
t0 = time.perf_counter()
self.func()
t1 = time.perf_counter()
print(f'time spend: {t1 - t0} s')
@PerfDot
def foo1():
time.sleep(1)
@PerfDot
def foo2():
time.sleep(2)
foo1()
foo2()
当执行到@PerfDot时,解释器会自动实例化一个PerfDot的实例对象,实例化入参就是foo1这个函数对象。解释器再把这个实例对象赋值给foo1。所以,我们调用foo1(),实质上是在执行PerfDot的一个实例对象,而不是foo1这个函数对象。
再来看看__call__这个特殊方法,它的作用是让一个对象成为可调用对象(callable object)。如果一个对象后面可以跟一对括号(),那么这个对象就是可被调用的,比如函数对象。你也可以使用callable(obj)方法来判断一个对象是否是可调用的。
Python比较神奇的一点是,它可以通过在类里面实现__call__方法,从而使得这个类的实例对象可以被直接调用,看起来就像在调用函数一样。
类装饰器等同于如下的实例对象调用:
inst1 = PerfDot(foo1)
inst1()
效果和@PerfDot是一样的。
类装饰器可以更方便的携带参数,你只需要在构造方法或者__call__方法中修改形参列表即可。我觉得它理解起来比函数装饰器要容易一些。
3、内置装饰器
Python3内置了一些装饰器,用于实现一些常用的功能,下面我们介绍一些常用的内置装饰器。
-
@property
将get/set/delete方法转换为属性调用。
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./14/14_5.py
# @property
class Dog:
def __init__(self, name):
self.name = name
self.__age = 0
@property
def age(self):
return self.__age
@age.setter
def age(self, age):
self.__age = age
@age.deleter
def age(self):
del self.__age
my_dog = Dog('Apple')
my_dog.age = 5
print(f'age: {my_dog.age}')
del my_dog.age
我们可以像属性引用一样调用对应的方法。当我们使用@property装饰一个方法func之后,它会自动产生两个装饰器@func.setter和@func.deleter,他们用于装饰对应的set方法和delete方法。
-
@staticmethod
将类中的方法装饰为静态方法。所谓静态方法,就是通过类直接调用的方法。它不需要创建对象,不会隐式传递self。
-
@classmethod
将类中的方法装饰为类方法。所谓类方法,就是方法入参中的self是类本身,类方法只能对类变量进行操作。
-
@abstractmethod
将类中的方法装饰为抽象方法。含抽象方法的类不能实例化,继承了含抽象方法的子类必须重写所有抽象方法,非抽象方法可以不重写。
我们通过下面的例子介绍以上3个装饰器的应用:
# author: Tiger, 关注公众号“跟哥一起学python”,ID:tiger-python
# file: ./14/14_6.py
from abc import ABCMeta, abstractmethod
# 静态方法、类方法、抽象方法
class ParentClass(metaclass=ABCMeta):
desp = 'hello'
def __init__(self, name):
self.name = name
self.age = 0
@abstractmethod
def func_abstract(self):
pass
@staticmethod
def func_static(a, b):
return a + b
@classmethod
def func_class(cls, desp):
cls.desp = desp
class ChildClass(ParentClass):
def func_abstract(self):
print('i\'m abstract method!')
inst1 = ChildClass('Apple')
inst1.func_abstract()
inst1.func_class('welcome!')
print(inst1.desp)
print(ParentClass.func_static(1, 2))
print(ChildClass.func_static(1, 2))
大家注意,这里的使用到了metaclass,它是python的元类,元类是构造类对象的类,用得比较少所以我们在前面的面向对象编程中没有讲,大家感兴趣可以自行学习。