Python 面向切面編程 AOP 及裝飾器

什麼是 AOP

AOP,就是面向切面編程,簡單的說,就是動態地將代碼切入到類的指定方法、指定位置上的編程思想就是面向切面的編程。我們管切入到指定類指定方法的代碼片段稱為切面,而切入到哪些類、哪些方法則叫切入點。這樣我們就可以把幾個類共有的代碼,抽取到一個切片中,等到需要時再切入對象中去,從而改變其原有的行為。這種思想,可以使原有代碼邏輯更清晰,對原有代碼毫無入侵性,常用於像權限管理,日志記錄,事物管理等等。而 Python 中的裝飾器就是很著名的設計,常用於有切面需求的場景。類如,Django 中就大量使用裝飾器去完成一下切面需求,如權限控制,內容過濾,請求管理等等。

裝飾器

Python 裝飾器(fuctional decorators)就是用於拓展原來函數功能的一種函數,目的是在不改變原函數名或類名的情況下,給函數增加新的功能。

下面就跟我一起詳細的瞭解下裝飾器是如何工作的。首先,要明確一個概念:Python 中萬物皆對象,函數也是是對象!所以,一個函數作為對象,可以在另一個函數中定義。

看下面示例:

def a():
    def b():
        print("I'm b")
    b()
    c = b
    return c
d = a()
d()
b()
c()

輸出結果為:

I'm b
I'm b
拋出 NameError: name 'b' is not defined 錯誤
拋出 NameError: name 'c' is not defined 錯誤

從上可以看出,由於函數是對象,所以:

  • 可以賦值給變量
  • 可以在另一個函數中定義

然後,return 可以返回一個函數對象。這個函數對象是在另一個函數中定義的。由於作用域不同,所以隻有 return 返回的函數可以調用,在函數 a 中定義和賦值的函數 b 和 c 在外部作用域是無法調用的。

這意味著一個功能可以 return 另一個功能。

除瞭可以作為對象返回外,函數對象還可以作為參數傳遞給另一個函數:

def a():
    print("I'm a")
def b(func):
    print("I'm b")
    func()
b(a)

 輸出結果:

I'm b
I'm a

OK,現在,基於函數的這些特性,我們就可以創建一個裝飾器,用來在不改變原函數的情況下,實現功能。

函數裝飾器

比如,我們要在函數執行前和執行後分別執行一些別的操作,那麼根據上面函數可以作為參數傳遞,我們可以這樣實現,看下面示例:

def a():
    print("I'm a")
def b(func):
    print('在函數執行前,做一些操作')
    func()
    print("在函數執行後,做一些操作")
b(a)

輸出結果:

在函數執行前,做一些操作
I'm a
在函數執行後,做一些操作

但是這樣的話,原函數就變成瞭另一個函數,每加一個功能,就要在外面包一層新的函數,這樣原來調用的地方,就會需要全部修改,這明顯不方便,就會想,有沒有辦法可以讓函數的操作改變,但是名稱不改變,還是作為原函數呢。

看下面示例:

def a():
    print("I'm a")

def c(func):
    def b():
        print('在函數執行前,做一些操作')
        func()
        print("在函數執行後,做一些操作")
    return b

a = c(a)
a()

輸出結果:

在函數執行前,做一些操作
I'm a
在函數執行後,做一些操作

如上,我們可以將函數再包一層,將新的函數 b,作為對象返回。這樣通過函數 c,將 a 改變並重新賦值給 a,這樣就實現瞭改變函數 a,並同樣使用函數 a 來調用。

但是這樣寫起來非常不方便,因為需要重新賦值,所以在 Python 中,可以通過 @ 來實現,將函數作為參數傳遞。

看下示例:

def c(func):
    def b():
        print('在函數執行前,做一些操作')
        func()
        print("在函數執行後,做一些操作")
    return b
@c
def a():
    print("I'm a")
a()

 輸出結果:

在函數執行前,做一些操作
I'm a
在函數執行後,做一些操作

如上,通過 @c,就實現瞭將函數 a 作為參數,傳入 c,並將返回的函數重新作為函數 a。這 c 也就是一個簡單的函數裝飾器。

且如果函數是有返回值的,那麼改變後的函數也需要有返回值,如下所以:

def c(func):
    def b():
        print('在函數執行前,做一些操作')
        result = func()
        print("在函數執行後,做一些操作")
        return result
    return b
@c
def a():
    print("函數執行中。。。")
    return "I'm a"
print(a())

輸出結果:

在函數執行前,做一些操作
函數執行中。。。
在函數執行後,做一些操作
I'm a

如上所示:通過將返回值進行傳遞,就可以實現函數執行前後的操作。但是你會發現一個問題,就是為什麼輸出 I'm a 會在最後才打印出來?

因為 I'm a 是返回的結果,而實際上函數是 print("在函數執行後,做一些操作") 這一操作前運行的,隻是先將返回的結果給到瞭 result,然後 result 傳遞出來,最後由最下方的 print(a()) 打印瞭出來。

那如何函數 a 帶參數怎麼辦呢?很簡單,函數 a 帶參數,那麼我們返回的函數也同樣要帶參數就好啦。

看下面示例:

def c(func):
    def b(name, age):
        print('在函數執行前,做一些操作')
        result = func(name, age)
        print("在函數執行後,做一些操作")
        return result
    return b
@c
def a(name, age):
    print("函數執行中。。。")
    return "我是 {}, 今年{}歲 ".format(name, age)
print(a('Amos', 24))

輸出結果:

在函數執行前,做一些操作
函數執行中。。。
在函數執行後,做一些操作
我是 Amos, 今年24歲 

但是又有問題瞭,我寫一個裝飾器 c,需要裝飾多個不同的函數,這些函數的參數各不相同,那麼怎麼辦呢?簡單,用 *args 和 **kwargs 來表示所有參數即可。

如下示例:

def c(func):
    def b(*args, **kwargs):
        print('在函數執行前,做一些操作')
        result = func(*args, **kwargs)
        print("在函數執行後,做一些操作")
        return result
    return b
@c
def a(name, age):
    print('函數執行中。。。')
    return "我是 {}, 今年{}歲 ".format(name, age)
@c
def d(sex, height):
    print('函數執行中。。。')
    return '性別:{},身高:{}'.format(sex, height)
print(a('Amos', 24))
print(d('男', 175))

輸出結果:

在函數執行前,做一些操作
函數執行中。。。
在函數執行後,做一些操作
我是 Amos, 今年24歲 

在函數執行前,做一些操作
函數執行中。。。
在函數執行後,做一些操作
性別:男,身高:175

如上就解決瞭參數的問題,哇,這麼好用。那是不是這樣就沒有問題瞭?並不是!經過裝飾器裝飾後的函數,實際上已經變成瞭裝飾器函數 c 中定義的函數 b,所以函數的元數據則全部改變瞭!

如下示例:

def c(func):
    def b(*args, **kwargs):
        print('在函數執行前,做一些操作')
        result = func(*args, **kwargs)
        print("在函數執行後,做一些操作")
        return result
    return b
@c
def a(name, age):
    print('函數執行中。。。')
    return "我是 {}, 今年{}歲 ".format(name, age)
print(a.__name__)

輸出結果:

b

會發現函數實際上是函數 b 瞭,這就有問題瞭,那麼該怎麼解決呢,有人就會想到,可以在裝飾器函數中先把原函數的元數據保存下來,在最後再講 b 函數的元數據改為原函數的,再返回 b。這樣的確是可以的!但我們不這樣用,為什麼?

因為 Python 早就想到這個問題啦,所以給我們提供瞭一個內置的方法,來自動實現原數據的保存和替換工作。哈哈,這樣就不同我們自己動手啦!

看下面示例:

from functools import wraps
def c(func):
    @wraps(func)
    def b(*args, **kwargs):
        print('在函數執行前,做一些操作')
        result = func(*args, **kwargs)
        print("在函數執行後,做一些操作")
        return result
    return b
@c
def a(name, age):
    print('函數執行中。。。')
    return "我是 {}, 今年{}歲 ".format(name, age)
print(a.__name__)

輸出結果:

a

使用內置的 wraps 裝飾器,將原函數作為裝飾器參數,實現函數原數據的保留替換功能。

耶!裝飾器還可以帶參數啊,你看上面 wraps 裝飾器就傳入瞭參數。哈哈,是的,裝飾器還可以帶參數,那怎麼實現呢?

看下面示例:

from functools import wraps
def d(name):
    def c(func):
        @wraps(func)
        def b(*args, **kwargs):
            print('裝飾器傳入參數為:{}'.format(name))
            print('在函數執行前,做一些操作')
            result = func(*args, **kwargs)
            print("在函數執行後,做一些操作")
            return result
        return b
    return c
@d(name='我是裝飾器參數')
def a(name, age):
    print('函數執行中。。。')
    return "我是 {}, 今年{}歲 ".format(name, age)

print(a('Amos', 24))

輸出結果:

裝飾器傳入參數為:我是裝飾器參數
在函數執行前,做一些操作
函數執行中。。。
在函數執行後,做一些操作
我是 Amos, 今年24歲 

如上所示,很簡單,隻需要在原本的裝飾器之上,再包一層,相當於先接收裝飾器參數,然後返回一個不帶參數的裝飾器,然後再將函數傳入,最後返回變化後的函數。

這樣就可以實現很多功能瞭,這樣可以根據傳給裝飾器的參數不同,來分別實現不同的功能。

另外,可能會有人問, 可以在同一個函數上,使用多個裝飾器嗎?答案是:可以!

看下面示例:

from functools import wraps
def d(name):
    def c(func):
        @wraps(func)
        def b(*args, **kwargs):
            print('裝飾器傳入參數為:{}'.format(name))
            print('我是裝飾器d: 在函數執行前,做一些操作')
            result = func(*args, **kwargs)
            print("我是裝飾器d: 在函數執行後,做一些操作")
            return result
        return b
    return c
def e(name):
    def c(func):
        @wraps(func)
        def b(*args, **kwargs):
            print('裝飾器傳入參數為:{}'.format(name))
            print('我是裝飾器e: 在函數執行前,做一些操作')
            result = func(*args, **kwargs)
            print("我是裝飾器e: 在函數執行後,做一些操作")
            return result
        return b
    return c
@e(name='我是裝飾器e')
@d(name='我是裝飾器d')
def func_a(name, age):
    print('函數執行中。。。')
    return "我是 {}, 今年{}歲 ".format(name, age)

print(func_a('Amos', 24))
行後,做一些操作
我是 Amos, 今年24歲 

 輸出結果:

裝飾器傳入參數為:我是裝飾器e
我是裝飾器e: 在函數執行前,做一些操作

裝飾器傳入參數為:我是裝飾器d
我是裝飾器d: 在函數執行前,做一些操作
函數執行中。。。
我是裝飾器d: 在函數執行後,做一些操作

我是裝飾器e: 在函數執

如上所示,當兩個裝飾器同時使用時,可以想象成洋蔥,最下層的裝飾器先包裝一層,然後一直到最上層裝飾器,完成多層的包裝。然後執行時,就像切洋蔥,從最外層開始,隻執行到被裝飾函數運行時,就到瞭下一層,下一層又執行到函數運行時到下一層,一直到執行瞭被裝飾函數後,就像切到瞭洋蔥的中間,然後再往下,依次從最內層開始,依次執行到最外層。

示例:

當一個函數 a 被 bcd 三個裝飾器裝飾時,執行順序如下圖所示,多個同理。

@d
@c
@b
def a():
    pass

類裝飾器

在函數裝飾器方面,很多人搞不清楚,是因為裝飾器可以用函數實現(像上面),也可以用類實現。因為函數和類都是對象,同樣可以作為被裝飾的對象,所以根據被裝飾的對象不同,一同有下面四種情況:

  • 函數裝飾函數
  • 類裝飾函數
  • 函數裝飾類
  • 類裝飾類

下面我們依次來說明一下這四種情況的使用。

1、函數裝飾函數

from functools import wraps
def d(name):
    def c(func):
        @wraps(func)
        def b(*args, **kwargs):
            print('裝飾器傳入參數為:{}'.format(name))
            print('在函數執行前,做一些操作')
            result = func(*args, **kwargs)
            print("在函數執行後,做一些操作")
            return result
        return b
    return c
@d(name='我是裝飾器參數')
def a(name, age):
    print('函數執行中。。。')
    return "我是 {}, 今年{}歲 ".format(name, age)

print(a.__class__)
# 輸出結果:
<class 'function'>

此為最常見的裝飾器,用於裝飾函數,返回的是一個函數。

2、類裝飾函數

也就是通過類來實現裝飾器的功能而已。通過類的 __call__ 方法實現:

from functools import wraps
class D(object):
    def __init__(self, name):
        self._name = name
    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print('裝飾器傳入參數為:{}'.format(self._name))
            print('在函數執行前,做一些操作')
            result = func(*args, **kwargs)
            print("在函數執行後,做一些操作")
            return result
        return wrapper
@D(name='我是裝飾器參數')
def a(name, age):
    print('函數執行中。。。')
    return "我是 {}, 今年{}歲 ".format(name, age)
print(a.__class__)
# 輸出結果:
<class 'function'>

以上所示,隻是將用函數定義的裝飾器改為使用類來實現而已。還是用於裝飾函數,因為在類的 __call__ 中,最後返回的還是一個函數。

此為帶裝飾器參數的裝飾器實現方法,是通過 __call__ 方法。

若裝飾器不帶參數,則可以將 __init__ 方法去掉,但是在使用裝飾器時,需要 @D() 這樣使用,如下:

from functools import wraps
class D(object):
    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print('在函數執行前,做一些操作')
            result = func(*args, **kwargs)
            print("在函數執行後,做一些操作")
            return result
        return wrapper
@D()
def a(name, age):
    print('函數執行中。。。')
    return "我是 {}, 今年{}歲 ".format(name, age)

print(a.__class__)
# 輸出結果:
<class 'function'>

如上是比較方便簡答的,使用類定義函數裝飾器,且返回對象為函數,元數據保留。

3、函數裝飾類

下面重點來啦,我們常見的裝飾器都是用於裝飾函數的,返回的對象也是一個函數,而要裝飾類,那麼返回的對象就要是類,且類的元數據等也要保留。

不怕丟臉的說,目前我還不知道怎麼實現完美的類裝飾器,在裝飾類的時候,一般有兩種方法:

返回一個函數,實現類在創建實例的前後執行操作,並正常返回此類的實例。但是這樣經過裝飾器的類就屬於函數瞭,其無法繼承,但可以正常調用創建實例。

如下:

from functools import wraps
def d(name):
    def c(cls):
        @wraps(cls)
        def b(*args, **kwargs):
            print('裝飾器傳入參數為:{}'.format(name))
            print('在類初始化前,做一些操作')
            instance = cls(*args, **kwargs)
            print("在類初始化後,做一些操作")
            return instance
        return b
    return c
@d(name='我是裝飾器參數')
class A(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print('類初始化實例,{} {}'.format(self.name, self.age))

a = A('Amos', 24)
print(a.__class__)
print(A.__class__)

# 輸出結果:
裝飾器傳入參數為:我是裝飾器參數
在類初始化前,做一些操作
類初始化實例,Amos 24
在類初始化後,做一些操作
<class '__main__.A'>
<class 'function'>

如上所示,就是第一種方法。

4、類裝飾類

接上文,返回一個類,實現類在創建實例的前後執行操作,但類已經改變瞭,創建的實例也已經不是原本類的實例瞭。

看下面示例:

def desc(name):
    def decorator(aClass):
        class Wrapper(object):
            def __init__(self, *args, **kwargs):
                print('裝飾器傳入參數為:{}'.format(name))
                print('在類初始化前,做一些操作')
                self.wrapped = aClass(*args, **kwargs)
                print("在類初始化後,做一些操作")

            def __getattr__(self, name):
                print('Getting the {} of {}'.format(name, self.wrapped))
                return getattr(self.wrapped, name)

            def __setattr__(self, key, value):
                if key == 'wrapped':  # 這裡捕捉對wrapped的賦值
                    self.__dict__[key] = value
                else:
                    setattr(self.wrapped, key, value)

        return Wrapper
    return decorator
@desc(name='我是裝飾器參數')
class A(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print('類初始化實例,{} {}'.format(self.name, self.age))

a = A('Amos', 24)
print(a.__class__)
print(A.__class__)
print(A.__name__)

輸出結果:

裝飾器傳入參數為:我是裝飾器參數
在類初始化前,做一些操作
類初始化實例,Amos 24
在類初始化後,做一些操作
<class '__main__.desc.<locals>.decorator.<locals>.Wrapper'>
<class 'type'>
Wrapper

如上,看到瞭嗎,通過在函數中新定義類,並返回類,這樣函數還是類,但是經過裝飾器後,類 A 已經變成瞭類 Wrapper,且生成的實例 a 也是類 Wrapper 的實例,即使通過 __getattr__ 和 __setattr__ 兩個方法,使得實例a的屬性都是在由類 A 創建的實例 wrapped 的屬性,但是類的元數據無法改變。很多內置的方法也就會有問題。我個人是不推薦這種做法的!

所以,我推薦在代碼中,盡量避免類裝飾器的使用,如果要在類中做一些操作,完全可以通過修改類的魔法方法,繼承,元類等等方式來實現。如果避免不瞭,那也請謹慎處理。

到此這篇關於Python 面向切面編程 AOP 及裝飾器的文章就介紹到這瞭,更多相關Python AOP 內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: