1. 程式人生 > >《Python有什麼好學的》之修飾器

《Python有什麼好學的》之修飾器

“Python有什麼好學的”這句話可不是反問句,而是問句哦。

主要是煎魚覺得太多的人覺得Python的語法較為簡單,寫出來的程式碼只要符合邏輯,不需要太多的學習即可,即可從一門其他語言跳來用Python寫(當然這樣是好事,誰都希望入門簡單)。

於是我便記錄一下,如果要學Python的話,到底有什麼好學的。記錄一下Python有什麼值得學的,對比其他語言有什麼特別的地方,有什麼樣的程式碼寫出來更Pythonic。一路回味,一路學習。

什麼是修飾器,為什麼叫修飾器

修飾器英文是Decorator,

我們假設這樣一種場景:古老的程式碼中有幾個很是複雜的函式F1、F2、F3…,複雜到看都不想看,反正我們就是不想改這些函式,但是我們需要改造加功能,在這個函式的前後加功能,這個時候我們很容易就實現這個需求:

def hi():
    """hi func,假裝是很複雜的函式"""
    return 'hi'

def aop(func):
    """aop func"""
    print('before func')
    print(func())
    print('after func')
    
if __name__ == '__main__':
    aop(hi)

以上是很是簡單的實現,利用Python引數可以傳函式引用的特性,就可以實現了這種類似AOP的效果。

這段程式碼目前沒有什麼問題,接下來煎魚加需求:需求為幾十個函式都加上這樣的前後的功能,而所有呼叫這些函式地方也要相應地升級。

看起來這個需求比較扯,偏偏這個需求卻是較為廣泛:在呼叫函式的前後加上log輸出、在呼叫函式的前後計算呼叫時間、在呼叫函式的前後佔用和釋放資源等等。

一種比較笨的方法就是,為這幾十個函式逐一新增一個入口函式,針對a函式新增一個a_aop函式,針對b函式新增一個b_aop函式…如此這樣。問題也很明顯:

  1. 工作量大
  2. 程式碼變得臃腫複雜
  3. 原始碼有多處呼叫了這些函式,可以會升級不完全

於是接下來有請修飾器出場,修飾器可以統一地給這些函式加這樣的功能:

def aop(func):
    """aop func"""
    def wrapper():
        """wrapper func"""
        print('before func')
        func()
        print('after func')
    return wrapper

@aop
def hi():
    """hi func"""
    print('hi')
    
@aop
def hello():
    """hello func"""
    print('hello')

if __name__ == '__main__':
    hi()
    hello()

以上aop函式就是修飾器的函式,使用該修飾器時只要在待加函式上一行加@修飾器函式名即可,如例項程式碼中就是@aop

加上了@aop後,呼叫新功能的hi函式就喝原來的呼叫一樣:就是hi()而不是aop(hi),也意味著所有呼叫這些函式的地方不需要修改就可以升級。

簡單地來說,大概修飾器就是以上的這樣子。

@是個什麼

對於新手來說,上面例子中,@就是一樣奇怪的東西:為什麼這樣子用就可以實現煎魚需求的功能了。

其實我們還可以不用@,煎魚換一種寫法:

def hi():
    """hi func"""
    print('hi')

def aop(func):
    """aop func"""
    def wrapper():
        """wrapper func"""
        print('before func')
        func()
        print('after func')
    return wrapper

if __name__ == '__main__':
    hi()

    print('')

    hi = aop(hi)
    hi()

上面的例子中的aop函式就是之前說過的修飾器函式。

如例子main函式中第一次呼叫hi函式時,由於hi函式沒叫修飾器,因此我們可以從輸出結果中看到程式只輸出了一個hi而沒有前後功能。

然後煎魚加了一個hi = aop(hi)後再呼叫hi函式,得到的輸出結果和加修飾器的一樣,換言之:

@aop 等效於hi = aop(hi)

因此,我們對於@,可以理解是,它通過閉包的方式把新函式的引用賦值給了原來函式的引用。

有點拗口。aop(hi)是新函式的引用,至於返回了引用的原因是aop函式中運用閉包返回了函式引用。而hi這個函式的引用,本來是指向舊函式的,通過hi = aop(hi)賦值後,就指向新函數了。

被調函式加引數

以上的例子中,我們都假設被調函式是無參的,如hi、hello函式都是無參的,我們再看一眼煎魚剛才的寫的修飾器函式:

def aop(func):
    """aop func"""
    def wrapper():
        """wrapper func"""
        print('before func')
        func()
        print('after func')
    return wrapper

很明顯,閉包函式wrapper中,呼叫被調函式用的是func(),是無參的。同時就意味著,如果func是一個帶引數的函式,再用這個修飾器就會報錯。

@aop
def hi_with_deco(a):
    """hi func"""
    print('hi' + str(a))

if __name__ == '__main__':
    # hi()
    hi_with_deco(1)

就是引數的問題。這個時候,我們把修飾器函式改得通用一點即可,其中import了一個函式(也是修飾器函式):

from functools import wraps

def aop(func):
    """aop func"""
    @wraps(func)
    def wrap(*args, **kwargs):
        print('before')
        func(*args, **kwargs)
        print('after')

    return wrap

@aop
def hi(a, b, c):
    """hi func"""
    print('test hi: %s, %s, %s' % (a, b, c))

@aop
def hello(a, b):
    """hello func"""
    print('test hello: %s, %s' % (a, b))

if __name__ == '__main__':
    hi(1, 2, 3)
    hello('a', 'b')

這是一種很奇妙的東西,就是在寫修飾器函式的時候,還用了別的修飾器函式。那也沒什麼,畢竟修飾器函式也是函式啊,有什麼所謂。

帶引數的修飾器

思路到了這裡,煎魚不禁思考一個問題:修飾器函式也是函式,那函式也是應該能傳參的。函式傳參的話,不同的引數可以輸出不同的結果,那麼,修飾器函式傳參的話,不同的引數會怎麼樣呢?

其實很簡單,修飾器函式不同的引數,能生成不同的修飾器啊。

如,我這次用這個修飾器是把時間日誌打到test.log,而下次用修飾器的時候煎魚希望是能打到test2.log。這樣的需求,除了寫兩個修飾器函式外,還可以給修飾器加引數選項:

from functools import wraps

def aop_with_param(aop_test_str):
    def aop(func):
        """aop func"""
        @wraps(func)
        def wrap(*args, **kwargs):
            print('before ' + str(aop_test_str))
            func(*args, **kwargs)
            print('after ' + str(aop_test_str))
        return wrap
    return aop

@aop_with_param('abc')
def hi(a, b, c):
    """hi func"""
    print('test hi: %s, %s, %s' % (a, b, c))

@aop_with_param('pppppp')
def hi2(a, b, c):
    """hi func"""
    print('test hi: %s, %s, %s' % (a, b, c))

if __name__ == '__main__':
    hi(1, 2, 3)
    print('')
    hi2(2, 3, 4)

同樣的,可以加一個引數,也可以加多個引數,這裡就不說了。

修飾器類

大道同歸,邏輯複雜了之後,人們都喜歡將函式的思維層面抽象上升到物件的層面。原因往往是物件能擁有多個函式,物件往往能管理更復雜的業務邏輯。

顯然,修飾器函式也有對應的修飾器類。寫起來也沒什麼難度,和之前的生成器一樣簡單:

from functools import wraps

class aop(object):
    def __init__(self, aop_test_str):
        self.aop_test_str = aop_test_str

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print('before ' + self.aop_test_str)
            func()
            print('after ' + self.aop_test_str)

        return wrapper
        
@aop('pppppp')
def hi():
    print('hi')

看得出來,這個修飾器類也不過是多了個__call__函式,而這個__call__函式的內容和之前寫的修飾器函式一個樣!而使用這個修飾器的方法,和之前也一樣,一樣的如例子中的@aop('pppppp')

甚至,煎魚過於無聊,還試了一下繼承的修飾器類:

class sub_aop(aop):
    def __init__(self, sub_aop_str, *args, **kwargs):
        self.sub_aop_str = sub_aop_str
        super(sub_aop, self).__init__(*args, **kwargs)

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print('before ' + self.sub_aop_str)
            super(sub_aop, self).__call__(func)()
            print('after ' + self.sub_aop_str)
        return wrapper
        
@sub_aop('ssssss', 'pppppp')
def hello():
    print('hello')
    
if __name__ == '__main__':
    hello()

你們猜猜結果怎麼樣?

先這樣吧

若有錯誤之處請指出,更多地請關注造殼