一篇夯實一個知識點系列--python裝飾器

寫在前面

本系列目的:但願能夠經過一篇文章,不望鞭辟入裏,但求在工程應用中駕輕就熟。
  • 裝飾器模式是鼎鼎大名的23種設計模式之一。裝飾器模式能夠在不改變原有代碼結構的狀況下,擴展代碼功能。
  • Python將裝飾器做爲Python的一種特性,內置了對裝飾器的支持,使得Python使用者在使用裝飾器時更加方便,合理使用裝飾器,可使Python代碼極具美感。
  • 因爲設計模式是一套被反覆使用的代碼設計經驗,並非編碼必備的技能。因此在編碼過程當中,徹底放棄使用裝飾器。可是若是你不寫出pythonic風格的,沒有壞味道的代碼,那麼裝飾器是這條路上繞不過的坎兒。

乾貨兒

包含八節內容:閉包(實現裝飾器的基礎),不帶參數的函數裝飾器,帶參數的函數裝飾器,不帶參數的類裝飾器,帶參數的類裝飾器,經常使用內建裝飾器,裝飾器總結(套路總結),裝飾器經典實例(單例模式)。
  • 閉包python

    裝飾器是經過閉包實現的。閉包是一個比較複雜的話題,深了說能夠講到python對常量表和符號表的處理方式。這裏只作簡單介紹。我的認爲只要記住如下三個特性,就明白了閉包的概念。
    • 一個閉包是一個做用域。一個閉包只能訪問做用域內的local變量和做用域外的nonlocal變量。
    • 若是在做用域外有和做用域內同名的變量var,若是在做用域內先使用變量var,而後再定義變量var,那麼會拋出a變量先使用後定義的錯誤。
    • 閉包能夠將做用域"封裝"。那麼咱們能夠在閉包以外,訪問閉包內的局部變量。由於局部變量被"封裝"在了閉包內。
  • 不帶參數函數裝飾器
    假設有一個需求,咱們須要在每一個函數運行時,打印當下時刻的時間戳。那麼有如下兩種寫法:git

    • 不使用裝飾器github

      編寫一個打印時間戳的工具函數,編寫一個業務函數。傳入業務函數對象到工具函數中,實現打印時間戳並執行業務函數的需求。代碼以下:
      
      ```
      import time
      
      def f():
          print("f is running!")
      
      def f1():
          print("f1 is running!")
      
      def print_running_time(f):
          print("running time:", time.time())
          f()
      
      print_running_time(f)
      print_running_time(f1)
      
      >>> ('running time:', 1588864281.154459)
          f is running!
          ('running time:', 1588864281.154483)
          f1 is running!
      ```
    • 使用裝飾器(函數裝飾器)設計模式

      編寫一個打印時間戳的裝飾器函數,編寫一個業務函數。裝飾器函數裝飾業務函數,實現打印時間戳並執行業務函數的需求。代碼以下:
      
      ```
      import time
      
      def print_running_time(f):
          def wrapper():
              print("running time:", time.time())
              f()
          return wrapper
      
      @print_running_time
      def f():
          print("f is running!")
      
      @print_running_time
      def f1():
          print("f1 is running!")
      
      f()
      f1()
      
      >>> ('running time:', 1588864281.154459)
               f is running!
              ('running time:', 1588864281.154483)
              f1 is running!
      ```
    • 以上兩種寫法對比安全

      經過對比以上兩種寫法,咱們能夠發現最明顯的區別是代碼在運行時,第一種寫法執行的print_running_time函數,第二種寫法執行的是f函數。那麼明顯第二種寫法中抽象出的語義更加接近咱們的業務需求。在一樣需求增長的狀況下,第一種寫法須要寫更多的工具函數,而且在執行業務函數時須要進行多層嵌套,極大地增長了代碼的複雜度。第二種寫法能夠增長多個裝飾函數裝飾到業務函數上方,在多需求下依舊保持代碼的可讀性和層次感,功能的獨立性和擴展性。多線程

      • 初探裝飾器原理閉包

        裝飾器的代碼運行分爲兩步,裝飾器初始化(在運行至被裝飾函數定義處)和執行被裝飾函數(在運行至被裝飾函數調用處)
        以第二種寫法裝飾器的寫法爲例,裝飾器的原理以下:app

        • 在代碼加載過程當中,代碼從上往下執行,那麼在執行到#1代碼時,至關於執行了#2代碼。(#1和#2的代碼是等價的。@docorator_func裝飾f,就至關於執行decorator_func(f))。根據#2代碼中print_running_time可知,執行print_running_time(f)的返回值是wrapper(注意返回的是函數對象wrapper,不是wrapper()).函數

          # 1
          @print_running_time
          def f():
              print("f is running!")
          
          # 2
          print_running_time(f)
          
          # 3
          def print_running_time(f):          #3.1
              def wrapper():                  #3.2
                  print("running time:", time.time())     #3.3
                  f()                                     #3.4
              return wrapper
          
          # 4
          f()
        • 那麼源碼中#1處的三行代碼,返回值爲wrapper,即至關於經過增長@裝飾函數,f如今已經指向了wrapper對象。
        • 根據以前提到閉包的特性:閉包能夠訪問做用以外的非局部變量,能夠將做用域"封裝",在閉包以外訪問閉包內的變量。因此wrapper能夠訪問#3.1中到本身外層函數的參數f變量(被裝飾器函數對象),而且能夠封裝wrapper做用域,保存f變量。
        • 執行#4處的業務函數f(),即執行#3.2的wrapper()代碼,即執行#3.3和#3.4代碼。
        • 整個過程當中須要注意的是,在代碼運行至#1時,f做爲裝飾器參數被#3.2wrapper閉包保留,在#1執行完以後,會存在兩個f對象,#4的f對象指向wrapper,#3.4的f對象依舊是#1處的f對象。
        • 執行流程爲f()==> wrapper()==> 執行#7.1 #7.2代碼==>打印當前時刻時間戳,順利執行了原有的業務函數。
  • 帶參數的函數裝飾器工具

    如今有新的需求,根據調試和生產環境的不一樣,須要往復地開關打印時間戳的功能,那麼這時就須要爲裝飾器函數增長參數,來做爲是否打印時間戳的開關。如如下代碼所示,f()會打印當前函數的執行時間,f1()則不會打印函數的執行時間

    import time
    
     # 1
     def print_running_time(*flag):
         def outer_wrapper(f):
             def inner_wrapper():
                 if flag:
                     print("running time:", time.time())
                 f()
             return inner_wrapper
         return outer_wrapper
    
     # 2
     @print_running_time(1)
     def f():
         print("f is running!")
    
     # 3
     @print_running_time()
     def f1():
         print("f1 is running!")
     
     f()
     f1()
    
     >>> ('running time:', 1588860065.265516)
         f is running!
         f1 is running!

    帶參數裝飾器原理

    • 以前簡單裝飾器原理==>@decorator_func裝飾業務函數f<=>decorator_func(f),那麼#2處的代碼<=>print_running_time(1)(f)<=>outer_wrapper(f)<=>inner_wrapper。須要注意的是inner_wrapper做爲閉包,包含了外層兩個變量flag和f的原始值。
    • 接下來調用f(),執行inner_wrapper(),經過判斷flag真假,選擇是否打印當前時間戳,而後執行業務函數,實現需求。
  • 不帶參數類裝飾器

    • 準確來講,裝飾器的本質是將一個可調用對象做爲參數傳入另外一個可調用對象,而後經過閉包保存變量,在適當的時候執行。咱們知道,python有兩個特性

      • Python中函數和類都是一等對象(這也是裝飾器能做爲python特性的緣由之一)。
      • python中若callable(obj)爲真,那麼這個對象就是可調用的。因此類,函數,方法,實現了__call__魔術方法的類實例,都是可調用對象。
    • 根據裝飾器的本質和以上Python兩個特性能夠得出如下結論:

      • 函數和類均可以做爲裝飾器,也能夠被裝飾器裝飾。
      • 類裝飾器和函數裝飾器思路相同,__init__做爲對象初始化的第一步,能夠實現一層閉包的效果
    • 將以前簡單函數裝飾器的例子換成類裝飾器,代碼以下(爲了與以前代碼保持一致,因此類名不符合Python命名規範):

      ```
      import time
      
      class print_running_time:
          def __init__(self, f):  # 至關於閉包,經過實例屬性保存變量f實現閉包中的變量封裝
              self.f = f
      
          def __call__(self):     # 類實例能夠被調用 
              print("running time:", time.time())
              return self.f()
      
      @print_running_time
      def f():
          print("f is running!")
      
      @print_running_time
      def f1():
          print("f1 is running!")
      
      f()
      f1()
      
      >>> 輸出同簡單函數裝飾器
      ```
  • 帶參數的類裝飾器

    • 將以前簡單函數裝飾器的例子換成類裝飾器,代碼以下:

      import time
        
        class print_running_time:
            def __init__(self, *flag):  # 至關於閉包,經過實例屬性保存變量flag實現閉包中的變量封裝
                self.flag = flag
        
            def __call__(self, f):     # 類實例能夠被調用,傳入業務函數f
                def wrapper():
                    if self.flag:
                        print("running time:", time.time())
                    f()
                return wrapper
        
        @print_running_time(1)
        def f():
            print("f is running!")
        
        @print_running_time()
        def f1():
            print("f1 is running!")
        
        f()
        f1()
        
        >>> 輸出同帶參數的函數裝飾器
  • 經常使用內建裝飾器

    裝飾器是Python最重要的特性之一,Python實現了不少對裝飾器的支持
    • wraps
      wraps能夠保留被裝飾函數的__doc__。以下代碼所示,wraps裝飾器的開關會致使打印f.__doc__出現兩種結果

      • 若是註釋掉#1.1的代碼,打印結果爲#1.3
      • 若是加上#1.1的代碼,打印結果爲#2.1

        import time
        from functools import wraps
        
        # 1
        def print_running_time(f):
            @wraps(f)                   # 1.1
            def wrapper():
                '''the func wrapper'''  # 1.3
                print("running time:", time.time())
                f()
            return wrapper
        
        # 2
        @print_running_time
        def f():
            '''the func f'''        # 2.1
            print("f is running!")
        
        print(f.__doc__)
        
        >>> the func f
    • property 、setter、 deleter

      這三個是孿生兄弟,其中property用的最多,setter和deleter依附property。
      • property:將函數調用轉化爲屬性
      • setter:設置屬性
      • deleter:刪除屬性
      • 相似於JavaBean,能夠將對屬性的操做寫入函數中,限制屬性操做,保護屬性安全。代碼以下:

        class Student(object):
        
            @property
            def name(self):
                return self._name
        
            @name.setter
            def name(self, name):
                if len(name) < 2:
                    raise ValueError("無名大俠?")
                self._name = name
        
            @name.deleter
            def name(self):
                del self._name
        
        stu = Student()
        stu.name = "劉"      # name.setter
        print(stu.name)         ## property
        del stu.name           # name.deleter
        print(stu.name)         # raise AttributeError
  • 多裝飾器疊加

    多個裝飾器疊加是python中很常見的騷操做,如Flask和Django中都會用到,舉例以下:

    import sys
    
    # 1
    def f1(func):
        print('f1 start')
        def wrapper():              # 1.1
            print('f1 ' + sys._getframe().f_code.co_name + ' start')
            func()                  # 1.2
            print('f1 ' + sys._getframe().f_code.co_name + ' end')
        print('f1 end')
        return wrapper
    
    # 2 
    def f2(func):
        print('f2 start')
        def wrapper():                  # 2.1
            print('f2 ' + sys._getframe().f_code.co_name + ' start')
            func()                      # 2.2
            print('f2 ' + sys._getframe().f_code.co_name + ' end')
        print('f2 end')
        return wrapper
    
    # 3
    @f1
    @f2
    def func():                 #3.1
        print('the func')
    
    #4
    func()                      #4.1              
    
    >>> f2 start
        f2 end
        f1 start
        f1 end
        f1wrapper start
        f2wrapper start
        the func
        f2wrapper end
        f1wrapper end
    • 多裝飾器執行過程分析

      執行分爲兩步,裝飾器初始化,被裝飾函數執行。順序以下:

      1. 裝飾器初始化,根據裝飾器原理,#3處的代碼等價於f1(f2(func))
      2. 執行f2(func); >>> f2 start f2 end; return #2.1處的wrapper(#2.2處的func爲#3.1處的func)
      3. 執行f1(f2(func))==> f1(#2.1處的wrapper); >>> f1 start f1 end; return #1.1處的wrapper(#1.2處的func爲#2.1處的wrapper)
      4. 裝飾器初始化結束, 以上兩步的輸出以下:

        >>> f2 start
                f2 end
                f1 start
                f1 end
      5. 被裝飾函數執行:func() <=> #1.1處的wrapper,替換以後代碼以下

        print('f1 ' + sys._getframe().f_code.co_name + ' start')
        func()                  # 1.2
        print('f1 ' + sys._getframe().f_code.co_name + ' end')
        \#1.2處的func <=> # 2.1處的wrapper,替換以後代碼以下
        
        ```
        print('f1 ' + sys._getframe().f_code.co_name + ' start')
        print('f2 ' + sys._getframe().f_code.co_name + ' start')
        func()                      # 2.2
        print('f2 ' + sys._getframe().f_code.co_name + ' end')
        print('f1 ' + sys._getframe().f_code.co_name + ' end')
        ```
        
        \#2.2處的func即爲#4.1處的func,執行以上代碼,輸出結果以下:
         ```
          >>> f1wrapper start
              f2wrapper start
              the func
              f2wrapper end
              f1wrapper end
         ```
  • 裝飾器總結

    • 裝飾器原理:#1代碼與#2代碼等價

      # 1
         @decorator_func
         def func():
             pass
      
         # 2
         decorator_func(func)
    • 裝飾器套路

      • 不帶參數的函數裝飾器須要有兩層函數:

        • 外層函數參數爲被裝飾函數對象
        • 內層參數爲被裝飾函數的參數
      • 帶參數的函數裝飾器須要有三層函數:

        • 外層函數參數爲裝飾器函數參數(簡直是廢話,外層函數原本就是裝飾器函數)
        • 中層函數參數爲被裝飾函數對象
        • 內層參數爲被裝飾函數的參數
        • 類裝飾器同理,最外層函數能夠用__init_函數代替,中層(若是有和內層函數寫在__call__中
    • 多個裝飾器疊加

      根據業務函數和裝飾器函數的距離,由近及遠執行裝飾器函數(外層函數),而後由遠到近執行內層函數。

  • 裝飾器經典實例:單例模式

    如下均單進程可行,多線程須要加鎖

    單例模式

    # eg:1
    class Singleton:
        _singleton = None
    
        def __new__(cls):
            if cls._singleton is None:
                cls._singleton = super().__new__(cls)
            return cls._singleton
    
    ins1 = Singleton()
    ins2 = Singleton()
    print(ins1 is ins2)
    
    # eg:2
    def singleton(cls):
        ins_pool = {}
    
        def inner():
            if cls not in ins_pool:
                ins_pool[cls] = cls()
            return ins_pool[cls]
        return inner
    
    @singleton
    class Cls:
        def __init__(self):
            pass
    
    ins1 = Cls()
    ins2 = Cls()
    print(ins1 is ins2)
    
    # eg:3
    class Singleton:
    
        def __init__(self, cls):
            self.ins_pool = {}
            self.cls = cls
    
        def __call__(self):
            print(self.ins_pool)
            if self.cls not in self.ins_pool:
                self.ins_pool[self.cls] = self.cls()
            return self.ins_pool[self.cls]
    
    @Singleton
    class Cls:
        def __init__(self):
            pass
    
    ins1 = Cls()
    ins2 = Cls()
    print(ins1 is ins2)

寫在最後

但願你們能夠經過本文掌握裝飾器這個殺手級特性。歡迎關注我的博客:藥少敏的博客

相關文章
相關標籤/搜索