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

「Python有什麼好學的」這句話可不是反問句,而是問句哦。shell

主要是煎魚以爲太多的人以爲Python的語法較爲簡單,寫出來的代碼只要符合邏輯,不須要太多的學習便可,便可從一門其餘語言跳來用Python寫(固然這樣是好事,誰都但願入門簡單)。閉包

因而我便記錄一下,若是要學Python的話,到底有什麼好學的。記錄一下Python有什麼值得學的,對比其餘語言有什麼特別的地方,有什麼樣的代碼寫出來更Pythonic。一路回味,一路學習。app

什麼是修飾器,爲何叫修飾器

修飾器英文是Decorator,函數

咱們假設這樣一種場景:古老的代碼中有幾個非常複雜的函數F一、F二、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的效果。spa

這段代碼目前沒有什麼問題,接下來煎魚加需求:需求爲幾十個函數都加上這樣的先後的功能,而全部調用這些函數地方也要相應地升級。日誌

看起來這個需求比較扯,恰恰這個需求倒是較爲普遍:在調用函數的先後加上log輸出、在調用函數的先後計算調用時間、在調用函數的先後佔用和釋放資源等等。code

一種比較笨的方法就是,爲這幾十個函數逐一添加一個入口函數,針對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()

大家猜猜結果怎麼樣?

先這樣吧

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

相關文章
相關標籤/搜索