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

寫在前面

本系列目的:希望可以通過一篇文章,不望鞭辟入裏,但求在工程應用中得心應手。

  • 裝飾器模式是鼎鼎大名的23種設計模式之一。裝飾器模式可以在不改變原有代碼結構的情況下,擴展代碼功能。
  • Python將裝飾器作爲Python的一種特性,內置了對裝飾器的支持,使得Python使用者在使用裝飾器時更加方便,合理使用裝飾器,可以使Python代碼極具美感。
  • 由於設計模式是一套被反覆使用的代碼設計經驗,並不是編碼必備的技能。所以在編碼過程中,完全放棄使用裝飾器。但是如果你不寫出pythonic風格的,沒有壞味道的代碼,那麼裝飾器是這條路上繞不過的坎兒。

乾貨兒

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

  • 閉包

    裝飾器是通過閉包實現的。閉包是一個比較複雜的話題,深了說可以講到python對常量表和符號表的處理方式。這裏只做簡單介紹。個人認爲只要記住以下三個特性,就明白了閉包的概念。

    • 一個閉包是一個作用域。一個閉包只能訪問作用域內的local變量和作用域外的nonlocal變量。
    • 如果在作用域外有和作用域內同名的變量var,如果在作用域內先使用變量var,然後再定義變量var,那麼會拋出a變量先使用後定義的錯誤。
    • 閉包可以將作用域"封裝"。那麼我們可以在閉包之外,訪問閉包內的局部變量。因爲局部變量被"封裝"在了閉包內。
  • 不帶參數函數裝飾器
    假設有一個需求,我們需要在每個函數運行時,打印當下時刻的時間戳。那麼有以下兩種寫法:

    • 不使用裝飾器
      編寫一個打印時間戳的工具函數,編寫一個業務函數。傳入業務函數對象到工具函數中,實現打印時間戳並執行業務函數的需求。代碼如下:

      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函數。那麼明顯第二種寫法中抽象出的語義更加接近我們的業務需求。在同樣需求增加的情況下,第一種寫法需要寫更多的工具函數,並且在執行業務函數時需要進行多層嵌套,極大地增加了代碼的複雜度。第二種寫法可以增加多個裝飾函數裝飾到業務函數上方,在多需求下依舊保持代碼的可讀性和層次感,功能的獨立性和擴展性。

      • 初探裝飾器原理

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

        • 在代碼加載過程中,代碼從上往下執行,那麼在執行到#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)
    

寫在最後

希望大家可以通過本文掌握裝飾器這個殺手級特性。歡迎關注個人博客:藥少敏的博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章