python進階20裝飾器

原創博客地址:python進階20裝飾器

Nested functions

Python允許創建嵌套函數,這意味着我們可以在函數內聲明函數並且所有的作用域和聲明週期規則也同樣適用。

 

1
2
3
4
5
6
7
>>> def outer():
...     x = 1
...     def inner():
...         print x # 1
...     inner() # 2
...
>>> outer()

這看起來稍顯複雜,但其行爲仍相當直接,易於理解。考慮一下在#1處發生了什麼——Python尋找一個名爲x的local變量,失敗了,然後在最鄰近的外層作用域裏搜尋,這個作用域是另一個函數!變量x是函數outer的local變量,但是和前文提到的一樣,inner函數擁有對外層作用域的訪問權限(最起碼有讀和修改的權限)。在#2處我們調用了inner函數。請記住inner也只是一個變量名,它也遵從Python的變量查找規則——Python首先在outer的作用域裏查找之,找到了一個名爲inner的local變量。

Closures

讓我們不從定義而是從另一個代碼示例開始。如果我們將上一個例子稍加修改會怎樣呢?

 

1
2
3
4
5
6
7
8
>>> def outer():
...     x = 1
...     def inner():
...         print x # 1
...     return inner
>>> foo = outer()
>>> foo.func_closure # doctest: +ELLIPSIS
(\<cell at 0x.... int object at 0x...>,)

從上一個例子中我們看到inner是一個由outer返回的函數,存儲於一個名爲foo的變量,我們可以通過foo()調用它。但是它能運行嗎?讓我們先來考慮一下作用域規則。
一切都依照Python的作用域規則而運行——x是outer函數了一個local變量。當inner在#1處打印x時,Python在inner中尋找一個local變量,沒有找到;然後它在外層作用域即outer函數中尋找並找到了它。
但是自此處從變量生命週期的角度來看又會如何呢?變量x是函數outer的local變量,這意味着只有當outer函數運行時它才存在。只有當outer返回後我們才能調用inner,因此依照我們關於Python如何運作的模型來看,在我們調用inner的時候x已經不復存在了, 那麼某個運行時錯誤可能會出現。
事實與我們的預想並不一致,返回的inner函數的確正常運行。Python支持一種稱爲閉包(function closures)的特性,這意味着定義於非全局作用域的inner函數在定義時記得記得它們的外層作用域長什麼樣兒。這可以通過查看inner函數的func_closure屬性來查看,它包含了外層作用域裏的變量。
請記住,每次當outer函數被調用時inner函數都被重新定義一次。目前x的值沒有改變,因此我們得到的每個inner函數和其它的inner函數擁有相同的行爲,但是如果我們將它做出一點改變呢?

 

1
2
3
4
5
6
7
8
9
10
>>> def outer(x):
...     def inner():
...         print x # 1
...     return inner
>>> print1 = outer(1)
>>> print2 = outer(2)
>>> print1()
1
>>> print2()
2

從這個例子中你可以看到closures——函數記住他們的外層作用域的事實——可以用來構建本質上有一個硬編碼參數的自定義函數。我們沒有將數字1或者2傳遞給我們的inner函數但是構建了能”記住”其應該打印數字的自定義版本。
closures獨自就是一個強有力的技術——你甚至想到在某些方面它有點類似於面向對象技術:outer是inner的構造函數,x扮演着一個類似私有成員變量的角色。它的作用有很多,如果你熟悉Python的sorted函數的key參數,你可能已經寫過一個lambda函數通過第二項而不是第一項來排序一些列list。也許你現在可以寫一個itemgetter函數,它接收一個用於檢索的索引並返回一個函數,這個函數適合傳遞給key參數。

但是讓我們不要用閉包做任何噩夢般的事情!相反,讓我們重新從頭開始來寫一個decorator!

Decorators!

一個decorator只是一個帶有一個函數作爲參數並返回一個替換函數的閉包。我們將從簡單的開始一直到寫出有用的decorators。

 

1
2
3
4
5
6
7
8
9
10
11
12
>>> def outer(some_func):  
...     def inner():  
...         print "before some_func"  
...         ret = some_func() # 1  
...         return ret + 1  
...     return inner  
>>> def foo():  
...     return 1  
>>> decorated = outer(foo) # 2  
>>> decorated()  
before some_func  
2

請仔細看我們的decorator實例。我們定義了一個接受單個參數some_func的名爲outer的函數。在outer內部我們定義了一個名爲inner的嵌套函數。inner函數打印一個字符串然後調用some_func,在#1處緩存它的返回值。some_func的值可能在每次outer被調用時不同,但是無論它是什麼我們都將調用它。最終,inner返回some_func的返回值加1,並且我們可以看到,當我們調用存儲於#2處decorated裏的返回函數時我們得到了輸出的文本和一個返回值2而不是我們期望的調用foo產生的原始值1.

我們可以說“裝飾”的變量是foo的一個裝飾版本——由foo加上一些東西構成。實際上,如果我們寫了一個有用的decorator,我們可能想用裝飾了的版本一起來替換foo,從而我們可以總是得到foo的“增添某些東西”的版本。我們可以不用學習任何新語法而做到這一點——重新將包含我們函數的變量進行賦值:

 

1
2
3
>>> foo = outer(foo)  
>>> foo # doctest: +ELLIPSIS  
<function inner at 0x...>

現在任何對foo()的調用都不會得到原始的foo,而是會得到我們經過裝飾的版本!領悟到了一些decorator的思想嗎?讓我們寫一個更加有用的裝飾器。
假設我們有一個提供座標對象的庫,它們可能只是由x, y兩個座標對組成。令人沮喪的是,這個座標對象並不支持算術運算,並且我們無法修改這個庫的源代碼,因此我們不能添加這些對運算的支持。我們將做大量的運算,但是我們現在只想實現加、減函數,它們可以帶兩個座標最想作爲參數並做相應的算術運算。這些函數可能很容易寫(爲了描述我將提供一個簡單的Coordinate類。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> class Coordinate(object):  
...     def __init__(self, x, y):  
...         self.x = x  
...         self.y = y  
...     def __repr__(self)  
...         return "Coord:" + str(self.__dict__)  
>>> def add(a, b):  
...     return Coordinate(a.x + b.x, a.y + b.y)  
>>> def sub(a, b):  
...      return Coordinate(a.x - b.x, a.y - b.y)  
>>> one = Coordinate(100, 200)  
>>> two = Coordinate(300, 200)  
>>> add(one, two)  
Coord:{'y': 400, 'x': 400}

但是,我們想當one和two都是{x: 0, y: 0},one和three的和爲{x: 100, y: 200},在不修改one, two, three的前提下結果有所不同(實在沒弄明白原作者此處是什麼意思^ ^)。讓我們寫一個邊界檢查decorator而不用爲每個函數添加一個對輸入參數做邊界檢查然後返回函數值!

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> def wrapper(func):  
...     def checker(a, b): # 1  
...         if a.x < 0 or a.y < 0:  
...             a = Coordinate(a.x if a.x > 0 else 0, a.y if a.y > 0 else 0)  
...         if b.x < 0 or b.y < 0:  
...             b = Coordinate(b.x if b.x > 0 else 0, b.y if b.y > 0 else 0)  
...         ret = func(a, b)  
...         if ret.x < 0 or ret.y < 0:  
...             ret = Coordinate(ret.x if ret.x > 0 else 0, ret.y if re> 0 else 0)  
...         return ret  
...     return checker  
>>> add = wrapper(add)  
>>> sub = wrapper(sub)  
>>> sub(one, two)  
Coord: {'y': 0, 'x': 0}  
>>> add(one, three)  
Coord: {'y': 200, 'x': 100}

這個裝飾器的效果和前面實例的一樣——返回一個修改過了的函數,只是在上例中對輸入參數和返回值做了一些有用的檢查和規範化,至於這樣做是否讓我們的代碼變得更加簡潔是一件可選擇的事情:將邊界檢查隔絕在它自己的函數裏,然後將其應用到通過用一個decorator包裝將我們所關心的函數上。另一個可能的方法是每次調用算數函數時對每一個輸入參數和輸出結果前對參數或者結果做邊界檢查,毫無疑問的是使用decorator至少在對一個函數進行邊界檢查的代碼量上重複更少。實際上,如果是裝飾我們自己的函數,我們可以將裝飾器應用程序寫的更明顯一點。

含參裝飾器

可以在裝飾器裏傳入一個參數,指明國籍,並在函數執行前,用自己國家的母語打一個招呼。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 小明,中國人
@say_hello("china")
def xiaoming():
    pass

# jack,美國人
@say_hello("america")
def jack():
    pass
那我們如果實現這個裝飾器,讓其可以實現 傳參 呢?

會比較複雜,需要兩層嵌套。

def say_hello(contry):
    def wrapper(func):
        def deco(*args, **kwargs):
            if contry == "china":
                print("你好!")
            elif contry == "america":
                print('hello.')
            else:
                return

            # 真正執行函數的地方
            func(*args, **kwargs)
        return deco
    return wrapper

來執行一下  
xiaoming()
print("------------")
jack()

看看輸出結果。

 

1
2
3
你好!
------------
hello.

用偏函數與類實現裝飾器

絕大多數裝飾器都是基於函數和閉包實現的,但這並非製造裝飾器的唯一方式。
事實上,Python 對某個對象是否能通過裝飾器( @decorator)形式使用只有一個要求:decorator 必須是一個“可被調用(callable)的對象。
對於這個 callable 對象,我們最熟悉的就是函數了。
除函數之外,類也可以是 callable 對象,只要實現了call 函數(上面幾個例子已經接觸過了)。
還有容易被人忽略的偏函數其實也是 callable 對象。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import time
import functools

class DelayFunc:
    def __init__(self,  duration, func):
        self.duration = duration
        self.func = func

    def __call__(self, *args, **kwargs):
        print(f'Wait for {self.duration} seconds...')
        time.sleep(self.duration)
        return self.func(*args, **kwargs)

    def eager_call(self, *args, **kwargs):
        print('Call without delay')
        return self.func(*args, **kwargs)

def delay(duration):
    """
    裝飾器:推遲某個函數的執行。
    同時提供 .eager_call 方法立即執行
    """
    # 此處爲了避免定義額外函數,
    # 直接使用 functools.partial 幫助構造 DelayFunc 實例
    return functools.partial(DelayFunc, duration)
我們的業務函數很簡單,就是相加

@delay(duration=2)
def add(a, b):
    return a+b

來看一下執行過程

 

1
2
3
4
5
6
7
8
9
>>> add    # 可見 add 變成了 Delay 的實例
<__main__.DelayFunc object at 0x107bd0be0>
>>>
>>> add(3,5)  # 直接調用實例,進入 __call__
Wait for 2 seconds...
8
>>>
>>> add.func # 實現實例方法
<function add at 0x107bef1e0>

參考

【翻譯】12步理解Python Decorators:https://harveyqing.gitbooks.io/python-read-and-write/content/python_advance/python_decorator_in_12_steps.html
裝飾器進階用法詳解:python.iswbm.com/en/latest/c03/c03_01.html
python中的閉包:https://blog.csdn.net/weixin_44141532/article/details/87116038

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章