Python裝飾器範例講解(三)

2023-02-16 15:00:58

Python裝飾器範例講解(三)

本文多參考《流暢的python》,在此基礎上增加了一些範例便於理解

姊妹篇

Python裝飾器範例講解(一),讓你簡單的會用

Python裝飾器範例講解(二),主要講了一個萬能公式(原理)

本文其實反而是最最基礎的部分,當然也回答了好幾個關鍵的問題,也有一些是重複的地方

  • 理解裝飾器必須理解函數、閉包等概念
  • 閉包後面單獨講,函數在本文是重點,從函數講起

函數:一等物件

  • 在Python中,函數是一等物件,需要滿足以下條件:
    • 在執行時建立
    • 能賦值給變數或資料結構中的元素
    • 能作為引數傳給函數
    • 能作為函數的返回結果
  • 在Python中,整數、字串和字典都是一等物件

函數名能賦值給變數

  • 範例

    def func():    
        print('hello')
    
    my_func = func   # 此處不要寫成func()  
    my_func()  # hello
    func() # hello
    
  • 這樣的使用比比皆是,比如在pytest中的一個應用

    import pytest
    
    xfail = pytest.mark.xfail  # 就是這裡
    
    
    @xfail  # 這樣看就比較簡潔了
    def test_hello():
        assert 1
    
    if __name__ == '__main__':
        pytest.main(['-sv',__file__])
    
  • 較為為典型的應用就是lambda,它是匿名的,但它同樣可以賦值給一個變數

    my_add = lambda x,y:x+y
    result = my_add(1,2)
    print(result)  # 3
    

函數能作為引數傳給函數

  • 範例

    def double(x):
        return x*2
    
    def triple(x):
        return x*3
    
    def calc(funcion_name,x):
        return funcion_name(x)
    
    print(double(2)) # 4
    print(triple(2)) # 6 
    print(calc(double,2)) # 4
    print(calc(triple,2)) # 6
    
  • 在上面的例子中你可以看到calc這個函數接收的第一個引數是函數名字

  • 呼叫的時候你傳入的是double、triple這樣的名字

  • 仔細觀察程式碼,calc的實現其實的本意就是把第一個引數當做函數名,第二個引數是第一個引數的引數。所以本質上你可以做任何事情,只要這個函數僅接收一個引數即可

    print(calc(bin,10))  # 返回的是bin(10)的結果  0b1010  
    print(calc(max,(2,5,3)))  # 執行的是max((2,5,3))  
    
  • 高階函數如map/filter/reduce/sort等,如果你接觸過,他們的引數不都是函數名嗎?

  • 我也寫過一篇文章,Python函數語言程式設計之map/filter/reduce/sorted

能作為函數的返回結果

  • 範例

    def add(x,y):
        return x+y
    
    def func():
        print('calling func')
        return add
        
    print(func()(1,2)) 
    # 輸出如下
    # calling func
    #  3
    # func() 就是 add , 跟你執行add(1,2)的效果是一樣的
    
  • 你也可以這樣

    new_add = func()
    print(new_add(1,2)) 
    # calling func
    #  3
    
  • 如果你看過前面的兩篇文章,到這裡就應該很熟悉了

可呼叫物件

  • 除了函數是可呼叫的,還有很多(其實也沒多少)都是可呼叫物件

  • 按照流暢的python的說法,有這麼多可呼叫物件

    可呼叫物件 說明
    使用者定義的函數 使用 def 語句或 lambda 表示式建立
    內建函數 使用 C 語言(CPython)實現的函數,如 len 或 time.strftime
    內建方法 使用 C 語言實現的方法,如 dict.get
    方法 在類的定義體中定義的函數
    呼叫類時會執行類的 new 方法建立一個範例,然後執行 init 方法,初始化實 例,最後把範例返回給呼叫方。因為 Python 沒有 new 運運算元,所以呼叫類相當於呼叫函數。
    類的範例 如果類定義了 call 方法,那麼它的範例可以作為函數呼叫。
    生成器函數 使用 yield 關鍵字的函數或方法。呼叫生成器函數返回的是生成器物件。

對普通的初學者而言其實就是函數和類,類的呼叫分2級,Obj()這是範例化,同時呼叫new和init。

new和init魔術方法,後面會單獨開篇講解,單例跟這個是息息相關的。

生成器後面也考慮單獨開文章說一下。

  • 範例程式碼(說明new和init)

    class Person:
        def __new__(cls, *args, **kwargs):
            print('calling new')
            cls.instance = super().__new__(cls)
            return cls.instance
        def __init__(self):
            print('calling init')
    
    wuxianfeng  = Person()
    
  • 範例輸出

    calling new
    calling init
    
  • 但此時wuxianfeng這個Person類的範例並不是可呼叫的物件

  • 如果你寫wuxianfeng(),會給你提示

    TypeError: 'Person' object is not callable
    
  • 你需要在Person類中定義一個__call__方法

    class Person:
        ...
        def __call__(self, *args, **kwargs):
            print('callable')
    
  • 此時再次執行wuxianfeng()就可以得到callable了

  • 當然如果你執行Person()()結果也是這樣的

    calling new
    calling init
    callable
    

  • python提供了一個內建的callable()函數來檢測物件是否可呼叫

    print([callable(obj) for obj in (abs, str, 13)])  # [True, True, False]
    

回到裝飾器

  • 雖然你可能已經學到裝飾器三了,但請你清空下你瞭解的裝飾器,倒也不是從0開始,帶點複習

  • 範例程式碼

    def decorate(function_name):
        def inner():
            print('calling inner')
            function_name()
        return inner
    @decorate
    def target():
        print('calling target')
    
    target()
    
  • 輸出結果

    calling inner
    calling target
    

  • 根據萬能公式,分析下執行過程

    • 當你在執行target()的時候,由於target上有個裝飾器,實際上發生的事情是target = decorate(target)

    • 前面的target 是新的(一個變數),後面的decorate(target)中的target是你之前定義的函數

    • decorate(target)就會去呼叫decorate函數傳入target引數,返回inner

    • 卡....返回了inner,是你加了裝飾器的效果,至此都沒有執行函數

    • 正是由於最終的target(),就是去呼叫了inner(),對應的語句是

      print('calling inner')
      function_name()  # 你傳入的是target就是此處的function_name
      

  • 說一些理論
    • 裝飾器只是語法糖
    • 裝飾器可以像常規的可呼叫物件那樣呼叫,其引數是另一個函數(被裝飾的函數)。
    • 裝飾器可能會處理被裝 飾的函數,然後把它返回,或者將其替換成另一個函數或可呼叫物件。
    • 裝飾器的一大特性是,能把被裝飾的函數替換成其他函數
    • 第二個特性是,裝飾器在載入模組時立即執行

  • 關於被替換

    def decorate(function_name):
        def inner():
            print('calling inner')
            function_name()
        print('這是inner的id:',id(inner))
        return inner
    @decorate
    def target():
        print('calling target')
    
    
    print('這是target的id:',id(target))
    
    
  • 範例輸出(你輸出的id跟我肯定不一樣,但2者應該是一致的,從這個角度也能看出來你執行的target不再是原來的target了)

    這是inner的id: 1804087435904
    這是target的id: 1804087435904
    

疊放裝飾器

  • 日常程式碼中還是有一些場景能看到一個函數被多個裝飾器裝飾的情況,比如pytest的allure

  • 這個執行順序就是如你所想的那般,先裝飾的先執行

  • 範例程式碼

    def decorate1(function_name):
        def inner1():
            print('calling inner1')
            function_name()
        return inner1
    
    def decorate2(function_name):
        def inner2():
            print('calling inner2')
            function_name()
        return inner2
    
    @decorate1
    @decorate2
    def target():
        print('calling target')
    
    target()  
    # 輸出
    # calling inner1
    # calling inner2
    # calling target
    
  • 但這種情況下的萬能公式是怎樣的呢???你知道不~

  • 萬能公式1

    @decorate1
    def target():
        print('calling target')
        
    # 等價於做了一件事
    target = decorate1(target)
    
  • 萬能公式2

    @decorate1
    @decorate2
    def target():
        print('calling target')
     
    # 等價於做了2件事
    # 第一件事,注意,就近原則
    target = decorate2(target)  # 前面的target是新的變數,後面的target是def的最初的、原始的函數
    # 第二件事
    target = decorate1(target)  # 前面的target又是一個新的變數,後面的target是line8的前面的target
    
    # 你也可以理解為做了一件事(合併上面2行)
    target = decorate1(decorate2(target) )  # 最近的@的先呼叫
    
  • 不信請看

    def decorate1(function_name):
        def inner1():
            print('calling inner1')
            function_name()
        return inner1
    
    def decorate2(function_name):
        def inner2():
            print('calling inner2')
            function_name()
        return inner2
    
    def target():
        print('calling target')
    
    target = decorate2(decorate1(target) )
    target()
    

  • 裝飾器就講到這裡了
  • 會用是第一步,理解簡單的過程是第二步,會寫一個裝飾器才算是基本懂了