《最值得收藏的python3语法汇总》之装饰器

目录

关于这个系列

1、概念和原理

2、类装饰器

3、内置装饰器

@property

@staticmethod

@classmethod

@abstractmethod


 

关于这个系列

《最值得收藏的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的元类,元类是构造类对象的类,用得比较少所以我们在前面的面向对象编程中没有讲,大家感兴趣可以自行学习。

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值