Python装饰器(从入门到进阶)

当我们在阅读别人的代码时,可能会经常看到在def函数时,会使用@xxxxx的语句,此外,在定义类时,总会用@staticmethod@property等语句,这便是本文要聊到的话题:装饰器(Decorator)。

装饰器是Python的一个高级话题,装饰器的构建对于工具构架者比对于应用程序员的意义会更加重要。目前,在流行的Python框架中变得越来越常见,对其基本的理解有助于认识它们的作用。我们将深入装饰器的内部工作机制,并学习自己编写新的装饰器的更多高级方法。

一、准备知识

在学习装饰器之前,我们要搞清楚以下基础知识:

  • 函数的可变长参数

    在定义函数时不确定调用的时候会传递多少个参数,那根据调用方式的不同,不确定个数的参数可能带有关键字,也可以没带关键字,分为包裹位置参数和包裹关键字参数。详情戳这里

  • 函数是一个对象

    在Python中,一切皆对象,函数也不例外。既然函数是一个对象,便可以将函数作为其他函数的参数进行传递,也可以作为其他函数的返回值进行返回。

  • 函数的闭包

    先看一段示例代码

    def fun(a):
        b = 100
        def i_fun():
            print(F'a = {a}, b = {b}')
        return i_fun
    
    ifun = fun(10)
    ifun()
    

    这里定义一个函数fun,其返回值也是一个函数对象。注意i_fun函数中的变量ab,它们的作用域只在函数fun中有效,而当我们执行ifun()时,会发现程序能正确输出ab的值,这便是所说的闭包:

    闭包 = 函数块+定义函数时的引用环境
    
    • 当内嵌函数体内引用到函数体外的变量时,将会把定义时涉及到的引用环境和函数体打包成一个整体(闭包)返回

    • 如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure)

    • 可以通过属性__closure__查看单元格对象的元组,如果执行print(ifun.__closure__), 将输出如下结果

      (<cell at 0x000002216A512400: int object at 0x00007FFD6CD32840>, <cell at 0x000002216A512370: int object at 0x00007FFD6CD33380>)
      

      两个int object便是引用环境中的变量ab

    下面给出两段关于闭包的经典错误代码

    • 示例1

      def foo():  
          a = 1  
          def bar():  
              a = a + 1  
              print(a)
          return bar
      c = foo()
      c()
      

      在执行到第四行代码时,会出现错误UnboundLocalError: local variable 'a' referenced before assignment,主要原因是:

      • 定义bar函数时,对a进行赋值,Python认为a是一个局部变量
      • 当执行到此句时,因为a已经认为是局部变量,在计算=右边的表达式时,结果不能找到局部变量a
    • 示例2

      fun_list = [lambda x:x+i for i in range(3)]
      for fun in fun_list:
          print(fun(2), end=',')
      

      按照代码的意思,其结果应该输出2,3,4,,而最终的结果却是4,4,4,

      • 主要原因是:当把函数加入fun_list列表里时,python并没有给变量i赋值,只是将变量i的引用环境进行了打包,当真正执行时,这时i的值已经变成了2,所以执行的结果都是4。可以打印每个函数的__closure__属性验证原因。
      • 解决方法:fun_list = [lambda x,i=i:x+i for i in range(3)],这里利用默认参数消除了闭包带来的影响

二、什么是装饰器

1. 理解装饰器

装饰器(Decorator),从字面上看,起到的作用便是装饰功能,可以从两个方面来理解这一概念:

  • 装饰器是什么:装饰器本身的形式是处理其他的可调用对象的可调用对象,其返回值是一个可调用对象,这里的可调用对象可以是函数,也可以是类,所以装饰器又分为函数装饰器和类装饰器。
  • 被装饰的对象是什么:被装饰的对象是函数或者是类

上面两句话描述得非常的清楚,你也一定能看懂(毕竟这些文字都是小学生应该掌握的),但是有99%的可能,你还是不清楚装饰器是什么鬼。如果你是那99%的一员,请不要走开,接下来通过编程中两个常见的应用场景,让你真正理解到什么是装饰器,为什么要用装饰器。为了方便理解,我们先以函数装饰器为例进行讲解。

场景一:函数性能测试

import time
def do_sth():
    time.sleep(2)

这里写了一个示例函数do_sth,使用time.sleep(2)模拟函数的实际代码运行。

如果想要测试该函数的性能,我们该如何做:

  • 方法一:直接修改原函数

    import time
    def do_sth():
        start_time = time.time()
        time.sleep(2)
        print(time.time()-start_time)
    

    优点:对函数的调用者比较友好

    缺点:

    • 修改已经正常运行的源代码不是一个很好的方式
    • 如果这样的函数有1000个,你确定这样的工作你不想吐😱?
  • 方法二:在调用时添加性能测试

    import time
    def do_sth():
        time.sleep(2)
    
        
    #调用
    start_time = time.time()
    do_sth()
    print(time.time()-start_time) 
    

    优点:不需要修改原函数

    缺点:调用函数太麻烦

  • 方法三:定义一个额外测试性能的函数

    import time
    
    def do_sth():
        time.sleep(2)
        
    def do_perf(fun):
        start_time = time.time()
        result = fun()
        print(time.time()-start_time)
        return result
    
    
    do_perf(do_sth)		#调用
    

    优点:不需要修改原函数,性能测试的代码得到了复用

    缺点:对函数调用者不友好

  • 方法四:对方法三进行改进

    import time
    
    def stat_time(fun):
        def i_fun():
            start_time = time.time()
            result = fun()
            print(time.time() - start_time)
            return result
        return i_fun
        
    def do_sth():
        time.sleep(2)
    do_sth = stat_time(do_sth)
    
    
    do_sth()		#调用
    

    优点:不需要修改原函数,性能测试的代码得到了复用,对函数调用者友好。

    缺点:非得说缺点吗,不够优雅😜,(当然有其他的缺点,在后面会聊)

写了一堆的方法做铺垫,实际想表达的是方法四不错,但我们说它不够优雅。Python是一门优雅的编码语言,Python之禅的第一句话便是Beautiful is better than ugly(如果不知道什么是python之禅,请在Python交互界面输入import this)。

Python提供了一种语法糖@decorator,其中decorator便是我们的装饰器,它可以在不改变原函数代码的基础上,提供额外的功能,比如示例中的性能统计。我们将按装饰器的写法修改方法四的代码如下:

import time

def stat_time(fun):
    def i_fun():
        start_time = time.time()
        result = fun()
        print(time.time() - start_time)
        return result
    return i_fun
    
@stat_time
def do_sth():
    time.sleep(2)


do_sth()		#调用,此时可以等价理解为 stat_time(do_sth)()

场景二:参数有效性验证

def add2(a, b):
    print(a+b)


def some_op(a, b, c):
    print((a+b)*c)

这里有两个函数addsome_op,假设函数要求传入的参数必须是整数,我们需要使用上面装饰器的方式完成参数的有效性验证,此时该如何?

第一个问题: 如何带参数?假设我们为add2函数定义了装饰函数valid_int_2,按照上一个示例给我们的提示,执行add2(1, 2)等价于执行valid_int_2(add2)(1, 2),也就是说装饰函数valid_int_2的返回值应该是一个可以接收两个参数的函数形式,所以我们可以为add_2定义装饰函数为如下形式:

def valid_int_2(fun):
    def i_fun(a,b):
        pass
    return i_fun    

第二个问题:装饰函数的功能是什么?既然是完成参数有效性检验,则参数满足条件的执行被装饰函数,不满足条件的返回约定值或者输出提示(这里以输出提示的方式演示)

def valid_int_2(fun):
    def i_fun(a,b):
        if isinstance(a,int) and isinstance(b,int):
            return fun(a,b)
        else:
            print('参数类型错误')
    return i_fun
    
@valid_int_2
def add2(a, b):
    print(a+b)
    
add2(1,1)		#输出 2
add2('a',5)		#输出 参数类型错误

第三个问题:如何统一参数形式?现在我们解决了两个参数的问题,那如何解决任意个参数的类型检查,以及任意形式的参数类型(位置参数,关键字参数)检查?这里要用来可变长参数的知识点,对于所有的参数形式,都可以使用fun(*args, **kwargs)的形式来表示,下面写一个通用参数形式的表示

def valid_int(fun):
    def i_fun(*args, **kwargs):
        for i in args:
            if not isinstance(i, int):  raise ValueError('不是整数')
        for i in kwargs.values():
            if not isinstance(i, int):  raise ValueError('不是整数')
        return fun(*args, **kwargs)
    return i_fun

@valid_int
def add2(a, b):
    print(a+b)
    
@valid_int
def some_op(a, b, c):
    print((a+b)*c)

add2(1,1)				#正常输出  2
some_op(1,2,3)			#正常输出 9
some_op('1',1,1) 		#抛出异常: ValueError: 不是整数

第四个问题:如果想验证参数是否满足其他类型(比如字符串)要求时,是不是又需要重新写一个装饰函数呢?答案是No! 这就需要提到装饰器参数,我们在进阶部分进行讲解。

2. 回顾装饰器的概念

回顾一下装饰器的概念:装饰器本身的形式是处理其他的可调用对象(或者叫被装饰对象)的可调用对象,其返回值是一个可调用对象,这里的可调用对象可以是函数,也可以是类,所以装饰器又分为函数装饰器和类装饰器,被装饰对象目前支持函数和类。

上面的示例我们讲的是函数装饰器,可以将函数装饰器简单的理解为:函数装饰器本质上是一个函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。

函数装饰器已经从Python 2.5开始可用。它们主要只是一种语法糖:通过在一个函数的def语句的末尾来运行另一个函数,把最初的函数名重新绑定到结果。函数装饰器是一种关于函数的运行时声明,函数的定义需要遵守此声明。装饰器在紧挨着定义一个函数或方法的def语句之前的一行编写,并且它由@符号以及紧随其后的对于原函数的一个引用组成。

Talk is cheap show me the code!,用代码形式来理解这一句话:

@decorator				#装饰函数decorator
def fun(arg):			#被装饰函数fun
    ...

fun(66)			#调用函数

等价于

def fun(arg):
	...
fun = decorator(fun)		# 将装饰器的返回值重新绑定到原函数名
fun(66)					# 本质上相当于调用 decorator(fun)(99)

Python学习手册中描述为:

Python装饰器以两种相关的形式呈现:

  • 函数装饰器在函数定义的时候进行名称重绑定,提供一个逻辑层来管理函数和方法或随后对它们的调用。
  • 类装饰器在类定义的时候进行名称重绑定,提供一个逻辑层来管理类,或管理随后调用它们所创建的示例。

简而言之,装饰器提供了一种方法,在函数和类定义语句的末尾插入自动运行代码——对于函数装饰器,在def的末尾;对于类装饰器,在class的末尾。这样的代码可以扮演不同的角色。

关于类装饰器在进阶部分进行讲解

3. 为什么需要装饰器

为什么需要装饰器呢,如果仔细看过理解装饰器部分内容,相信不难回答。如果从纯技术的角度来看等装饰器,它不是一个严格需要的内容,因为我们可以在不顾及代码的优雅、冗余时,实现相同的功能。但,我们要说的是使用装饰器给我们带来的好处:

  • 明确的语法:使用装饰器可以让扩展意图更明确。使用@语法会比在源代码中添加特殊代码更容易识别,也更优雅简洁。
  • 代码可维护性:装饰器避免了在每个函数或类调用中重复扩展的代码。由于它们只出现一次,在类或者函数自身的定义中,它们排除了冗余性并简化了未来的代码维护。
  • 一致性:装饰器使得我们忘记使用必需的包装逻辑的可能性大大减少。这主要得益于两个优点——由于装饰是显式的并且只出现一次,出现在装饰的对象自身中,与必须包含在每次调用中的特殊代码相比较,装饰器促进了更加一致和统一的API使用。

4. 装饰器的应用

在实际工作中,你可能会成为装饰器的用户或提供者,比如在定义类的过程中,你可能经常要用到静态方法装饰器、属性装饰器以及其他一些内置的装饰器。此外,很多流行的Python工具包括了执行管理数据库或用户接口逻辑等任务的装饰器。在这样的情况中,我们不需要知道装饰器如何编码就可以完成任务。对于更为通用的场景,我们可以编写自己的任意装饰器。

装饰器的应用从大的方面可以分为两种:

  • 管理调用和实例

    装饰器通过自动把函数和类名重绑定到其他可调用对象,来实现自动运行扩展代码(或者说功能代码),以增强对函数和类的调用。函数装饰器可以理解成在被装饰函数调用之前,根据不同的需要进行相应的拦截处理,然后再调用相应的函数(对于类装饰器是实例化处理)。

  • 管理函数和类

    我们最常使用装饰器来拦截处理随后对函数和类的调用,另外,装饰器也可以用来管理函数对象或者类对象本身。换句话说,函数装饰器可以用来管理函数调用和函数对象,类装饰器可以用来管理类实例和类自身。通过返回装饰的对象自身而不是一个包装器,装饰器变成了针对函数和类的一种简单的后创建步骤。

这里列举一些常见的应用场景:

  • 在调试时执行参数验证测试

  • 自动获取和释放线程锁

  • 统计调用函数的次数以进行优化等

  • 添加跟踪调用,插入日志

  • 性能测试

  • 权限校验

示例:Flask框架的路由设置

@app.route('/')
def index():
    return 'Hello, Flask World'

app.route装饰器,实际执行的额外功能是使用add_url_rule函数关联路径和视图函数。

def route(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
	def decorator(f: F) -> F:
		endpoint = options.pop("endpoint", None)
		self.add_url_rule(rule, endpoint, f, **options)
		return f

	return decorator

5. 装饰器的缺点

装饰器在不修改被装饰对象源代码的情况下可以优雅地添加辅助功能,极大地减少代码冗余,并且用户调用友好。但它并不是没有缺点:

  • 额外调用
    通过装饰添加一个包装层,在每次调用装饰对象的时候,会引发一次额外调用所需的额外性能成本,因此,装饰包装器可能会使程序变慢。实际开发中这点额外性能成本可忽略。

  • 类型修改
    当使用装饰器后,被装饰对象(函数或类)不会保持其最初的
    类型,导致其元信息消失,比如函数的docstring、__name__、参数列表等。

    import time
    import functools
    
    def stat_time(fun):
     
        def i_fun():
            start_time = time.time()
            result = fun()
            print(time.time() - start_time)
            return result
        return i_fun
        
    @stat_time
    def do_sth():
        time.sleep(2)
        
    print(do_sth.__name__)   #输出i_fun
    

    可以在定义装饰函数的时候添加一个functools.wraps装饰器进行改进:

    import time
    import functools
    
    def stat_time(fun):
        @functools.wraps(fun)
        def i_fun():
            start_time = time.time()
            result = fun()
            print(time.time() - start_time)
            return result
        return i_fun
    
    @stat_time
    def do_sth():
        time.sleep(2)
        
    print(do_sth.__name__)   #输出do_sth
    

三、进阶知识

对装饰器有一定的认识和应用后,我们再谈一谈一些进阶知识。

1. 装饰器参数

还记得参数有效性验证示例吗,当时说如果想验证参数是否满足其他类型(比如字符串,或者其它)要求时,该如何写装饰函数。这便是这里提到的装饰器参数。记得装饰器是什么吗,它是一个处理其他的可调用对象的可调用对象,其返回值是一个可调用对象。如果装饰函数本身可以带入一个参数后,然后返回的仍然是一个装饰函数不就可以达到想要的效果吗?

请看示例代码:

def com_valid(v_type):
    def valid_arg(fun):
        def i_fun(*args, **kwargs):
            for i in args:
                if not isinstance(i, v_type):  raise ValueError
            for i in kwargs.values():
                if not isinstance(i, v_type):  raise ValueError
            return fun(*args, **kwargs)
        return i_fun
    return valid_arg

@com_valid((int,float))
def do_test(*args, **kwargs):
    print('Just for test')

do_test(1, 3.1, 4)  
do_test('A')

需要注意的是:

  • com_valid((int,float))的执行结果是valid_arg函数本身,valid_arg函数才是这里真正的装饰函数。
  • 装饰器只有一个参数,就是被装饰对象的引用

2. 装饰器嵌套

有的时候,一个装饰器不够。为了支持多步骤的扩展,装饰器语法允许我们向一个装饰的函数或方法添加包装器逻辑的多个层。当使用这一功能的时候,每个装饰器必须出现在自己的一行中。这种形式的装饰器语法:

@A
@B
@C
def fun(...):
	...

其等价于

def fun(...):
	...
f = A(B(C(f)))

示例:

def A(fun):
    print('添加的第一段扩展代码A')
    return fun
def B(fun):
    print('添加的第一段扩展代码B')
    return fun
def C(fun):
    print('添加的第一段扩展代码C')
    return fun

@A
@B
@C
def fun():
    print('Just for test')
    
fun()

输出:

添加的第一段扩展代码C
添加的第一段扩展代码B
添加的第一段扩展代码A
Just for test

可以理解成:执行fun()时,相当于执行A(B(C(fun)))

3. 类装饰器

函数装饰器设计用来只增强一个特定函数或方法调用,而不是一个完整的对象接口。类装饰器更好地充当后一种角色——因为它们可以拦截实例创建调用,它们可以用来实现任意的对象接口扩展或管理任务。类装饰器在Python 2.6和Python 3.0中开始支持使用。

类装饰器的语法如下:

@decorator	# Decorate class
class C:
	...
	
	
x = C(99)	# Make an instance

等同于下面的语法:类自动地传递给装饰器,并且装饰器的结果返回来分配给类名:

class C:
	...
C = decorator(C)		# 重新将装饰器的返回值绑定到类名


x = C(99)			# 本质上调用 decorator(C)(99)

为了更好的理解类装饰器,请理解下面的示例代码:

示例1:统计类C的实例化次数

class CountIns:
    count = 0
    def __init__(self, C):
        self._C = C
        
    def __call__(self):
        CountIns.count += 1
        print(F'{CountIns.count}次')
        return self._C()
    
@CountIns
class C:
    pass

输出

1次
2次
3次

理解:C( )等价于count_ins(C)( ),其实相当于先后执行了CountIns类的__init____call__的两个方法 ,区别只在于不管调用了多少次C()__init__只执行了一次而已。

如果想统计某个函数的调用次数,该如何做呢?

class Count:
count = 0
def __init__(self, F):
  self._F = F

def __call__(self, *args,**kwargs):
  Count.count += 1
  print(F'{Count.count}次')
  return self._F(*args,**kwargs)

@Count
def fun(a,b):
print(F'{a}+{b}={a+b}')
fun(1,1)
fun(2,2)
fun(3,3)	

输出:

1次
1+1=2
2次
2+2=4
3次
3+3=6

都看到这里了,麻烦顺手点个赞呗!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JJustRight

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值