python装饰器装饰原理探秘

最近一直没抽出时间来写博客,这篇博客在草稿箱里面躺了好久了,一直都只有一个标题。
现在终于要开始写了。

  1. 为什么要写这个篇文章

    前段时间整天盯着python学习 群,发现好多同学对python很多内容并不是很理解,觉得有必要分享自己这段时间通过学习实践总结出来的一些东西。
    写的过程中我会将一些自己理解的内容直接用文字写出来,感觉没必要去复制粘贴一些概念性的东西,若存在有理解错误的地方,欢迎各位在留言指出一起讨论提高,SO 废话不说开写吧。

  2. 什么是装饰器

    装饰器(Decorator):个人理解装饰器无非就是一个函数,函数的功能是传入一个源函数,丢回来一个包含原函数功能的闭包来替代原函数以供调用(就是改变了函数名变量指向的对象)。
    哎呀 又扯出一个闭包概念⊙﹏⊙b汗 关于闭包下次有空再写一篇
    写到文章结尾了发现整篇没有一张图,还是上个图吧,不管这个图能不能帮你更好理解什么是装饰器。

  3. 装饰器的分类

    以实现一个非常无聊的功能(控制函数运行次数)的装饰器进行举例

    • 不带参数的装饰器

      不带参数的装饰器是真正装的饰器,通过传入一个函数返回一个闭包代替原函数,不带参数的装饰器定义方式如下:

        def my_decorator(func):
            count =0
            limit = 5
        
            def inner(*args, **kwargs):
                # do some thing
                nonlocal count 
                count  +=1
                if count  > limit:
                    print('<%s>只能被调用%s次' % (func.__name__, limit))
                    return
                else:
                    return func(*args, **kwargs)
        
            return inner
        
        
        @my_decorator
        def my_func1(n):
            print('n is %s' % n)
        
        
        def my_func2(n):
            print('func2 n is %s' % n)
        
        
        for i in range(10):
            my_func1(i)
            my_func2(i)
      

      执行结果

        n is 0
        func2 n is 0
        n is 1
        func2 n is 1
        n is 2
        func2 n is 2
        n is 3
        func2 n is 3
        n is 4
        func2 n is 4
        <my_func1>只能被调用5次
        func2 n is 5
        <my_func1>只能被调用5次
        func2 n is 6
        <my_func1>只能被调用5次
        func2 n is 7
        <my_func1>只能被调用5次
        func2 n is 8
        <my_func1>只能被调用5次
        func2 n is 9
      

      通过返回一个inner闭包 实现限制执行次数

    • 带参数的装饰器

      带参数的装饰器函数严格意义上只能算是一个伪装饰器,实际上带参数的装饰器函数是一个返回值为装饰器函数的一个函数。
      为什么要有带参数的装饰器?
      就拿前面举的例子来说: 虽然my_decorator实现的功能比较无聊,但是我还是感觉这个无聊的功能不够完美,因为函数执行次数限制在装饰器里面写死了,假如我要限制好几个不同的函数,每个函数限制不同执行次数,那么按照不带参数的装饰器的写法每个限制的执行次数我都要重新写一个装饰器,这显然非常不合理。于是我需要一个带参数的装饰器来实现这种功能。
      下面是实现该功能的函数写法:

       def my_decorator(limit):
      
           def outer(func):
               count = 0
       
               def inner(*args, **kwargs):
                   # do some thing
                   nonlocal count 
                   count  +=1
                   if count  > limit:
                       print('<%s>只能被调用%s次' % (func.__name__, limit))
                       return
                   else:
                       return func(*args, **kwargs)
       
               return inner
           return outer
       
       
       @my_decorator(2)
       def my_func1(n):
           print('n is %s' % n)
       
       
       @my_decorator(4)
       def my_func2(n):
           print('func2 n is %s' % n)
       
       
       for i in range(5):
           my_func1(i)
           my_func2(i)
      

      眼尖的同学可能注意到了这里装饰器装饰写法和不带参数的不同,不带参数时@后面直接接的装饰器函数名,此处@后面接了装饰器函数名后还有一对括号和参数。
      带了括号和参数意味着函数已经被执行了,相当于@后面跟着的是函数执行的返回值,所以my_decorator函数返回的outer函数闭包才是真正意义上的装饰器函数,
      通过my_decorator的参数limit 我们就可以每次装饰一个函数时都可以指定不同的函数执行次数上限。
      执行结果

       n is 0
       func2 n is 0
       n is 1
       func2 n is 1
       <my_func1>只能被调用2次
       func2 n is 2
       <my_func1>只能被调用2次
       func2 n is 3
       <my_func1>只能被调用2次
       <my_func2>只能被调用4次
      
  4. 关于“@”符号

    • @符号作用

      @符号是python中的一个语法糖 为了让装饰语句写起来更加简单代码更加易读。
      在函数定义前一行放置@符号后面接一个装饰器函数名代表该函数被装饰器装饰,相当于在函数定义之后立马执行了一条语句:
      func=decorator(func)
      此处func为被装饰函数,decorator为装饰器函数。

    • @ 符号给初学者带来的障碍

      @符号各种方便但是给初学者理解装饰器带来了一定障碍:初学者很容易忘掉@的实际作用,然后就会想不通装饰器到底是怎么装饰的。
      So 其实这个障碍是我写这篇文章的主要原因。

  5. 装饰器的装饰过程

    终于写到了正题 ,正题还是以限制函数运行次数的装饰器来举例,为了更好探究装饰器函数执行过程这回装饰器不再用来装饰一个函数,而是用来装饰一个class
    你没看错是装饰一个类,用来控制类的实例化,其实类的实例化过程其实就是调用了一个__call__()函数,这个以后有机会写类相关东西再详细说明。
    至于为什么这里装饰类是因为类定义过程中类内部的语句会被执行一遍,而函数定义过程中不会执行里面的语句不方便探讨装饰器各层函数执行时机。

    • 装饰器函数的执行时机

      这部分还是要上代码来分析:

        def my_decorator(limit):
            print('my_decorator is running')
        
            def outer(func):
                print('outer is running ')
                count = 0
        
                def inner(*args, **kwargs):
                    print('inner is running ')
                    nonlocal count 
                    count  +=1
                    if count  > limit:
                        print('<%s>只能被调用%s次' % (func.__name__, limit))
                        return
                    else:
                        return func(*args, **kwargs)
        
                return inner
            return outer
        
        
        print('Ready to decorate')
        
        
        @my_decorator(2)
        class A:
            print('class A defined')
        
            def __init__(self, name):
                self.name = name
                self.who_am_i()
        
            def who_am_i(self):
                print("I'm an A my name is", self.name)
        
        
        print('+++ Start loop +++')
        for i in range(3):
            A(i)
      

      以上代码装饰器还是和带参数的装饰器用同样的配方只不过在装饰器函数各层函数开始时加了一个print用料标记各个函数被执行的时机,另被装饰外装饰的不再是一个函数而是一个类。
      以控制类A的的实例化个数,当然请别在意这种蠢蠢的控制方法。
      以下是输出结果:

        Ready to decorate
        my_decorator is running
        class A defined
        outer is running 
        +++ Start loop +++
        inner is running 
        I'm an A my name is 0
        inner is running 
        I'm an A my name is 1
        inner is running 
        <A>只能被调用2次
      

      从输出结果可以很轻易看出用带参数的装饰器以@方式修饰函数或类过程中各层函数运行顺序:

      1. 装饰器函数my_decorator(伪)执行返回真正的装饰器outer(带参数装饰器专属)
      2. 定义被装饰目标A(真)
      3. 装饰器函数outer(真)执行返回inner 替代A(真)得到A(伪)
      4. A(伪)被调用
      5. inner执行
      6. inner 调用A(真)执行

      对于不带参数的装饰器与带参数的比仅仅是少了一个返回真正装饰器的过程,不再上代码了直接给出答案:

      1. 定义被装饰目标A(真)
      2. 装饰器函数my_decorator(真)执行返回inner 替代A(真)得到A(伪)
      3. A(伪)被调用
      4. inner执行
      5. inner 调用A(真)执行
    • @调用装饰器与直接显式赋值调用的区别

      虽然没有人会显式赋值调用装饰器,但还是要说一下两者区别。
      @调用装饰器与func=decorator(func)这种方式调用没有本质上的区别
      唯一的区别是带参数装饰器最外层函数执行返回真正装饰器的时机不大相同,but who cares.

  6. 装饰器拾遗

    • 多重装饰
      装饰器可以多重装饰

        call_times = dict()
      
      
        def my_decorator2(func):
        
            def inner(*args, **kwargs):
                global call_times
                call_times[func.__name__] = call_times[func.__name__] + 1 if func.__name__ in call_times else 1
                return func(*args, **kwargs)
        
            return inner
        
        
        def my_decorator(limit):
        
            def outer(func):
        
                count = 0
        
                def inner(*args, **kwargs):                        
                    nonlocal count 
                    count  +=1
                    if count  > limit:
                        print('<%s>只能被调用%s次' % (func.__name__, limit))
                        return
                    else:
                        return func(*args, **kwargs)
        
                return inner
            return outer
        
        
        @my_decorator(2)
        @my_decorator2
        class A:
        
            def __init__(self, name):
                self.name = name
                self.who_am_i()
        
            def __call__(self, *args, **kwargs):
                self.who_am_i()
        
            def who_am_i(self):
                print("I'm an A my name is", self.name)
        
        
        for i in range(3):
            A(i)
        
        print(call_times)
      

      以上用两个装饰器分别实现了限制执行次数和执行计数的功能,执行输出结果如下:

        I'm an A my name is 0            
        I'm an A my name is 1            
        <inner>只能被调用2次
        {'A': 2} 
      

      多重装饰的装饰顺序是离函数定义越近越早被执行,这个应该没必要多解释,硬要问我怎么知道的 就看那个输出结果吧最后两行吧:输出结果是"inner 只能被调用2次 "而字典的key却是"A"

    • 怎么解决 inner的尴尬
      从上面例子又引出了一个尴尬的inner 怎么消除这种尴尬呢?直接上代码:

        def my_decorator(func):
            
            def inner(*args, **kwargs):
                
                return func(*args, **kwargs)
            
            inner.__name__ = func.__name__
            return inner
      

      从此函数名不再尴尬
      好吧先写到这,继续学习去了。。。。

原文地址:https://www.cnblogs.com/RexShao/p/8379189.html