Python裝飾器範例講解(二)

2023-02-09 18:00:21

Python裝飾器範例講解(二)

Python裝飾器範例講解(一)

你最好去看下第一篇,雖然也不是緊密的連結在一起

參考B站碼農高天的視訊,大家喜歡看視訊可以跳轉忽略本文:https://www.bilibili.com/video/BV19U4y1d79C
一鍵三連哦
本文的知識點主要是

​ 類裝飾器

​ 裝飾器的本質(up主說的萬能公式)

案例

  • 程式碼

    def count_time(func):
        def wrapper(*args,**kwargs):
            from time import time
            start_time = time()
            result = func(*args,**kwargs)
            end_time = time()
            print(f'統計花了{end_time-start_time}時間')
            return result
        return wrapper
    
  • 改造為類裝飾器(注意對比)

    • 你得知道基礎的python的物件導向的知識
    • 一些類的魔術方法如__init__和__call__
    class CountTime:
        def __init__(self,function_name):  # 類沒傳參一說,但範例化是可以傳參的,類比  def count_time(func):
            self.function_name = function_name
        def __call__(self, *args, **kwargs):  # 類範例的(),像函數的call , ==>def wrapper(*args,**kwargs):
            from time import time
            start_time = time()
            result = self.function_name(*args,**kwargs)  # 也就改了這裡,其他都一樣
            end_time = time()
            print(f'統計花了{end_time-start_time}時間')
            return result
    
  • 完整的程式碼

    def is_prime(x):
        if  x == 2 :
            return True
        elif x % 2 == 0 or x == 1 :
            return False
        for i in range(3, int(x ** 0.5) + 1, 2):
            if x % i == 0:
                return False
        return True
    
    class CountTime:
        ... # 就不重複上面了
    
    @CountTime  # 類是一個裝飾器
    def get_prime_nums(start,end):
        prime_nums = 0
        for num in range(start,end):
            if is_prime(num):
                prime_nums = prime_nums + 1
        return prime_nums
    
    
    print(get_prime_nums(2,50000))  # 效果是一樣的
    

碼農高天說

我把up主的一些話摘錄一些寫到這裡,輔助大家理解

  • 裝飾器decorator:是一個輸入是函數,輸出也是函數的函數(看講解一中的裝飾器)
  • 類裝飾器 class decorator,up主說有一定的歧義
    • 可以當做裝飾器的類(裝飾器本身)
    • 可以裝飾類的裝飾器(裝飾器要裝飾的物件)
  • 裝飾器本身既可以是函數也可以是類,裝飾的物件同樣可以是函數或者類
  • 背這些理論沒有意義,關鍵要弄懂背後的原理
  • __call__可以讓類的範例當做函數用(就是callable)

萬能公式

  • 裝飾器語法糖背後

    class CountTime:
        ...# 同上
    
    @CountTime
    def add(a,b):  # 就用碼農的demo函數
        return a+b
    
    
    print(add(1,2))
    
  • @CountTime等價於,所謂的萬能公式

    add = CountTime(add)
    
  • print(add(1,2))已經不再是使用的原始的add了,用的是新的add

    print(add(1,2)) 等價於
    
    print(CountTime(add)(1,2))
    
  • 也就是說

    # @CountTime  # 去掉裝飾器,你就是定義了一個簡單的函數
    # # add = CountTime(add) # 函數名被重定義了  相當於這樣
    def add(a,b):
        return a+b
    
    print(CountTime(add)(1,2))
    
  • 你還可以這樣

    def add(a,b):
        return a+b
    
    
    new_add = CountTime(add)
    print(new_add(1,2))
    
  • 是的,被裝飾過的函數已經不再是原來的函數了,它總是會先去執行裝飾器(CountTime(add))

  • 總結:

    • 在一個函數上做裝飾器,等價於裝飾器呼叫這個函數
    • 在類裝飾器的這個例子中,add從一個函數變成了一個類的範例(type看下即可)

改造,有引數的裝飾器

  • 我們看到過很多的裝飾是有引數的,這是怎麼實現的呢?

  • 比如你想要輸出的資訊可以調整其字首

    計時: 0.46秒
    	或者
    用時: 0.46秒
    
  • 你希望是這樣裝飾和呼叫的

    @CountTime(prefix='用時:')
    def add(a,b):
        return a+b
    
    
    print(add(1,2))
    
  • 那咋實現呢?

  • 回到萬能公式:

    @CountTime(prefix='用時:')
    def add(a,b):
        ...
    # 等價於
    add = CountTime(add)
    
    # 那麼
    @CountTime(prefix='用時:')
    def add(a,b):
        ...
        
    # 等價於
    add = CountTime(prefix='用時:')(add)
    
  • CountTime這個類能CountTime(prefix='用時:'),就是範例化做到的,所以...類的init方法要改一下,不再是傳參function_name了,而是傳你的prefix,像這樣

    class CountTimeV2:
        def __init__(self,prefix='用時:'):
            self.prefix = prefix
    
  • 但現在還不能繼續,add = CountTime(prefix='用時:')(add)中你(add)還要處理,前面是init做的,()就是callable做的,裡面的引數是add,也就是函數的名字,所以你的call也要改造,像這樣嗎?

        def __call__(self, function_name):
            from time import time
            start_time = time()
            result = function_name(*args,**kwargs)
            end_time = time()
            print(f'統計花了{end_time-start_time}時間')
            return result
    
  • 不對的,光這樣改造不夠的,因為你這個function_name(*args,**kwargs)在IDE中就會報錯,哪裡來的呢?沒有定義。

  • 回想講解一中,函數裝飾器裡層,還有一個函數,此處就可以參考

    class CountTimeV2:
        def __init__(self, prefix='用時:'):
            self.prefix = prefix
    
        def __call__(self, function_name):
            def wrapper(*args, **kwargs):  # 加了個函數 , 包裹一層
                from time import time
                start_time = time()
                result = function_name(*args, **kwargs) # 這樣就可以用引數了
                end_time = time()
                print(f'{self.prefix}{end_time - start_time}')  # 用之前的定義
                return result
    
            return wrapper
    
    
    @CountTimeV2(prefix='耗時:')  # 可以改為用時、計時等等
    def add(a, b):
        return a + b
    
    
    print(add(1, 2))
    
    

前面談的是類是一個裝飾器,裝了一個函數

下面談的是函數是一個裝飾器,裝飾一個類

類的裝飾器

  • 現在有這麼一個類

    class Person:
        pass
    
    wuxianfeng = Person()
    print(wuxianfeng)  # <__main__.Person object at 0x000002361C15A460>
    
  • 你學過python可以這樣修改

    class Person:
        def __str__(self):
            return f"{self.__class__.__name__}"
    
    wuxianfeng = Person()
    print(wuxianfeng) # Person
    
  • 但如果有很多的類都要如此呢?

  • 可以寫個裝飾器,來裝飾這些類唄

  • 怎麼寫?回想剛才你學到的知識,萬能公式!

    def show_classname(): # 先不寫引數
        pass  # 先不寫內容
    
    @show_classname
    class Person:
        pass
    
    Person = show_classname(Person)
    
    wuxianfeng = Person()
    print(wuxianfeng)
    
    • 你現在要寫一個函數,名字隨意,如show_classname

    • 你肯定要裝飾在類上

      @show_classname
      class Person:
          pass
      
    • 根據萬能公式,你的Person應該變了

      Person = show_classname(Person)
      # 從上面這段程式碼,你要能分析出以下內容
          # 1. show_classname應該有個引數,傳參是個類名
          # 2. 因為可以Person = ,所以show_classname有個返回值
      
    • 對於使用者而言,應該沒有任何操作上的差異

      wuxianfeng = Person()
      # 從上面這段程式碼,你要能分析出以下內容
      # 1. Person已經被你改變了
      # 2. Person()==>show_classname(Person)(),所以show_classname這個函數的返回值還是一個類
      print(wuxianfeng)
      
      
      
    • 分析完了,函數體部分是有點不好理解的

      def show_classname(class_name):
          def __str__(self):
              return self.__class__.__name__
          class_name.__str__ = __str__
          return class_name
      
      @show_classname
      class Person:
          pass
      
      
      Person = show_classname(Person)
      
      wuxianfeng = Person()
      print(wuxianfeng)
      
    • 看著這個結果,我們來解釋下(也許你會更好理解)

      1. show_classname(Person) 返回仍然是Person
      2. 但這個時候的Person被改變了一點(你要做的不就是如此嗎?)
      3. 原來你是這樣寫的
          class Person:
              def __str__(self):
                  return f"{self.__class__.__name__}"
          看看現在的寫法
          def __str__(self):
              return self.__class__.__name__
          class_name.__str__ = __str__ 
               # 前面的class_name.__str__ 是類自己的函數(本段解釋的line 5)
               # 後面的= __str__ ,是line8的函數
               # 是的,函數可以被重新賦值,函數是一等物件,
      
    • 如果還不明白...盡力了

帶引數的類的裝飾器

碼農高天並沒有給出範例程式碼

當然如果你真懂了前面的"改造,有引數的裝飾器",也很簡單

  • 直接上程式碼

    def show_classname(info='類名:'):
        def wrapper(class_name):
            def __str__(self):
                return info+ self.__class__.__name__
            class_name.__str__ = __str__
            return class_name
        return wrapper
    
    @show_classname('類的名字是:')  #
    class Person:
        pass
    
    wuxianfeng = Person()
    print(wuxianfeng)
    
    
    
    
  • 預設值就是='類名:',怎麼用呢

    @show_classname()
    class Human:
        pass
    qianyuli = Human()
    print(qianyuli)
    
  • 注意不能這樣

    @show_classname
    class Human:
        pass
    qianyuli = Human()
    print(qianyuli)
    
  • 提示錯誤

    Traceback (most recent call last):
      File "demo.py", line 21, in <module>
        qianyuli = Human()
    TypeError: wrapper() missing 1 required positional argument: 'class_name'
    
  • 提個問題,為何會報錯?

  • 如果你無法解釋的通,你應該還沒理解。

  • 答案其實還是萬能公式。

    @show_classname
    class Human:
        pass
    # 1. 
    等價於(萬能公式來了)
    Human = show_classname(Human)
    
    # 2. 
    show_classname(Human) 執行這個的時候其實你在做
    def show_classname(info='類名:'):
        ...
    Human這個東西傳給了info
    # 你要不信,你改為下面這樣就知道了;信的話就過
                def show_classname(info='類名:'):
                    print('info是啥?',info.name)
                class Human:
                    name = '女媧'
                    
    # 3. 
    show_classname(Human)這個的返回是wrapper
    但wrapper這個函數是有個引數的,看你的定義def  wrapper(class_name):
        
    # 4. 
    定義的時候是感知不到問題的,下面的報錯行
    qianyuli = Human()
    
    其實你是在
    Human()=>show_classname(Human)()=>wrapper(),錯了,(看3),你需要一個class_name引數
    
    
  • 如果還不明白...盡力了