国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

Python學習之路26-函數裝飾器和閉包

sunny5541 / 2443人閱讀

摘要:初步認識裝飾器函數裝飾器用于在源代碼中標記函數,以某種方式增強函數的行為。函數裝飾器在導入模塊時立即執行,而被裝飾的函數只在明確調用時運行。只有涉及嵌套函數時才有閉包問題。如果想保留函數原本的屬性,可以使用標準庫中的裝飾器。

《流暢的Python》筆記

本篇將從最簡單的裝飾器開始,逐漸深入到閉包的概念,然后實現參數化裝飾器,最后介紹標準庫中常用的裝飾器。

1. 初步認識裝飾器

函數裝飾器用于在源代碼中“標記”函數,以某種方式增強函數的行為。裝飾器就是函數,或者說是可調用對象,它以另一個函數為參數,最后返回一個函數,但這個返回的函數并不一定是原函數。

1.1 裝飾器基礎用法

以下是裝飾器最基本的用法:

# 代碼1
#裝飾器用法
@decorate
def target(): pass

# 上述代碼等價于以下代碼
def target(): pass
target = decorate(target)

即,最終的target函數是由decorate(target)返回的函數。下面這個例子說明了這一點:

# 代碼2
def deco(func):
    def inner():
        print("running inner()")
    return inner

@deco
def target():
    print("running target()")

target()
print(target)

# 結果
running inner() # 輸出的是裝飾器內部定義的函數的調用結果
.inner at 0x000001AF32547D90>

從上面可看出,裝飾器的一大特性是能把被裝飾的函數替換成其他函數。但嚴格說來,裝飾器只是語法糖(語法糖:在編程語言中添加某種語法,但這種語法對語言的功能沒有影響,只是更方便程序員使用)。

裝飾器還可以疊加。下面是一個說明,具體例子見后面章節:

# 代碼3
@d1
@d2
def f(): pass

#上述代碼等價于以下代碼:
def f(): pass
f = d1(d2(f))
1.2 Python何時執行裝飾器

裝飾器的另一個關鍵特性是,它在被裝飾的函數定義后立即運行,這通常是在導入時,即Python加載模塊時:

# 代碼4
registry = []

def register(func):
    print("running register(%s)" % func)
    registry.append(func)
    return func

@register
def f1():
    print("running f1()")

def f2():
    print("running f2()")

if __name__ == "__main__":
    print("running in main")
    print("registry ->", registry)
    f1()
    f2()

# 結果
running register()
running in main # 進入到主程序
registry -> []
running f1()
running f2()

裝飾器register在加載模塊時就對f1()進行了注冊,所以當運行主程序時,列表registry并不為空。

函數裝飾器在導入模塊時立即執行,而被裝飾的函數只在明確調用時運行。這突出了Python程序員常說的導入時運行時之間的區別。

裝飾器在真實代碼中的使用方式與代碼4中有所不同:

裝飾器和被裝飾函數一般不在一個模塊中,通常裝飾器定義在一個模塊中,然后應用到其他模塊中的函數上;

大多數裝飾器會在內部定義一個函數,然后將其返回。

代碼4中的裝飾器原封不動地返回了傳入的函數。這種裝飾器并不是沒有用,正如代碼4中的裝飾器的名字一樣,這類裝飾器常充當了注冊器,很多Web框架就使用了這種方法。下一小節也是該類裝飾器的一個例子。

1.3 使用裝飾器改進策略模式

上一篇中我們用Python函數改進了傳統的策略模式,其中,我們定義了一個promos列表來記錄有哪些具體策略,當時的做法是用globals()函數來獲取具體的策略函數,現在我們用裝飾器來改進這一做法:

# 代碼5,對之前的代碼進行了簡略
promos = []

def promotion(promo_func): # 只充當了注冊器
    promos.append(promo_func)
    return promo_func

@promotion
def fidelity(order): pass  

@promotion
def bulk_item(order): pass

@promotion
def large_order(order): pass

def best_promo(order):
    return max(promo(order) for promo in promos)

該方案相比之前的方案,有以下三個優點:

促銷策略函數無需使用特殊名字,即不用再以_promo結尾

@promotion裝飾器突出了被裝飾函數的作用,還便于臨時禁用某個促銷策略(只需將裝飾器注釋掉)

促銷策略函數在任何地方定義都行,只要加上裝飾器即可。

2. 閉包

正如前文所說,多數裝飾器會在內部定義函數,并將其返回,已替換掉傳入的函數。這個機制的實現就要靠閉包,但在理解閉包之前,先來看看Python中的變量作用域。

2.1 變量作用域規則

通過下述例子來解釋局部變量和全局變量:

# 代碼6
>>> def f1(a):
...     print(a)
...     print(b)
    
>>> f1(3)
3
Traceback (most recent call last):
  -- snip --
NameError: name "b" is not defined

當代碼運行到print(a)時,Python查找變量a,發現變量a存在于局部作用域中,于是順利執行;當運行到print(b)時,python查找變量b,發現局部作用域中并沒有變量b,便接著查找全局作用域,發現也沒有變量b,最終報錯。正確的調用方式相信大家也知道,就是在調用f1(3)之前給變量b賦值。

我們再看如下代碼:

# 代碼7
>>> b = 6
>>> def f2(a):
...     print(a)
...     print(b)
...     b = 9
    
>>> f2(3)
3
Traceback (most recent call last):
  -- snip --
UnboundLocalError: local variable "b" referenced before assignment

按理說不應該報錯,并且b的值應該打印為6,但結果卻不是這樣。

事實是:變量b本來是全局變量,但由于在f2()中我們為變量b賦了值,于是Python在局部作用域中也注冊了一個名為b的變量(全局變量b依然存在,有編程基礎的同學應該知道,這叫做“覆蓋”)。當Python執行到print(b)語句時,Python先搜索局部作用域,發現其中有變量b,但是b此時還沒有被賦值(全局變量b被覆蓋,而局部變量b的賦值語句在該句后面),于是Python報錯。

如果不想代碼7報錯,則需要使用global語句,將變量b聲明為全局變量:

# 代碼8
>>> b = 6
>>> def f2(a):
...     global b
...     -- snip --
2.2 閉包的概念

現在開始真正接觸閉包。閉包指延伸了作用域的函數,它包含函數定義體中引用,但不在定義體中定義的非全局變量,即這類函數能訪問定義體之外的非全局變量。只有涉及嵌套函數時才有閉包問題。

下面用一個例子來說明閉包以及非全局變量。定義一個計算某商品一段時間內均價的函數avg,它的表現如下:

# 代碼9
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

假定商品價格每天都在變化,因此需要一個變量來保存這些值。如果用類的思想,我們可以定義一個可調用對象,把這些值存到內部屬性中,然后實現__call__方法,讓其表現得像函數;但如果按裝飾器的思想,可以定義一個如下的嵌套函數:

# 代碼10
def make_averager():
    series = []

    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total / len(series)

    return averager

然后以如下方式使用這個函數:

# 代碼11
>>> avg = make_averager()
>>> avg(10)
10.0
-- snip --

不知道大家剛接觸這個內部的averager()函數時有沒有疑惑:代碼11中,當執行avg(10)時,它是到哪里去找的變量seriesseries是函數make_averager()的局部變量,當make_averager()返回了averager()后,它的局部作用域就消失了,所以按理說series也應該跟著消失,并且上述代碼應該報錯才對。

事實上,在averager函數中,series自由變量(free variable),即未在局部作用域中綁定的變量。這里,自由變量series和內部函數averager共同組成了閉包,參考下圖:

實際上,Python在averager__code__屬性中保存了局部變量和自由變量的名稱,在__closure__屬性中保存了自由變量的值:

# 代碼12,注意這些變量的單詞含義,一目了然
>>> avg.__code__.co_varnames  # co_varnames保存局部變量的名稱
("new_value", "total")
>>> avg.__code__.co_freevars # co_freevars保存自由變量的名稱
("series",)
>>> avg.__closure__ # 單詞closure就是閉包的意思
# __closure__是一個cell對象列表,其中的元素和co_freevars元組一一對應
(,)
>>> avg.__closure__[0].cell_contents 
[10, 11, 12] # cell對象的cell_contents屬性才是真正保存自由變量的值的地方

綜上:閉包是一種函數,它會保存定義函數時存在的自由變量的綁定,這樣調用函數時,雖然外層函數的局部作用域不可用了,但仍能使用那些綁定。

注意:只有嵌套在其他函數中的函數才可能需要處理不在全局作用域中的外部變量。

2.3 nonlocal聲明

代碼10中的make_averager函數并不高效,因為如果只計算均值的話,其實不用保存每次的價格,我們可按如下方式改寫代碼10

# 代碼13
def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        count += 1
        total += new_value
        return total / count

    return averager

但此時直接運行代碼11的話,則會報代碼7中的錯誤:UnboundLocalError

問題在于:由于count是不可變類型,在執行count += 1時,該語句等價于count = count + 1,而這就成了賦值語句,count不再是自由變量,而變成了averager的局部變量。total也是一樣的情況。而在之前的代碼10中沒有這個問題,因為series是個可變類型,我們只是調用series.append,以及把它傳給了sumlen,它并沒有變為局部變量。

對于不可變類型來說,只能讀取,不能更新,否則會隱式創建局部變量。為了解決這個問題,Python3引入了nonlocal聲明。它的作用是把變量顯式標記為自由變量:

# 代碼14
def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        nonlocal count, total
        -- snip --
3. 裝飾器

了解了閉包后,現在開始正式使用嵌套函數來實現裝飾器。首先來認識標準庫中三個重要的裝飾器。

3.1 標準庫中的裝飾器 3.1.1 functools.wraps裝飾器

來看一個簡單的裝飾器:

# 代碼15
def deco(func):
    def test():
        func()
    return test

@deco
def Test():
    """This is a test"""
    print("This is a test")

print(Test.__name__)
print(Test.__doc__)

# 結果
test
None

我們想讓裝飾器來自動幫我們做一些額外的操作,但像改變函數屬性這樣的操作并不一定是我們想要的:從上面可以看出,Test現在指向了內部函數testTest自身的屬性被遮蓋。如果想保留函數原本的屬性,可以使用標準庫中的functools.wraps裝飾器。下面以一個更復雜的裝飾器為例,它會在每次調用被裝飾函數時計時,并將經過的時間,傳入的參數和調用的結果打印出來:

# 代碼16
# clockdeco.py
import time, functools

def clock(func): # 兩層嵌套
    @functools.wraps(func)  # 綁定屬性
    def clocked(*args, **kwargs):
        t0 = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - t0
        name = func.__name__
        arg_lst = [] # 參數列表
        if args:
            arg_lst.append(", ".join(repr(arg) for arg in args))
        if kwargs:
            pairs = ["%s=%r" % (k, w) for k, w in sorted(kwargs.items())]
            arg_lst.append(", ".join(pairs))
        arg_str = ", ".join(arg_lst)
        print("[%0.8fs] %s(%s) -> %r" % (elapsed, name, arg_str, result))
        return result
    return clocked

它的使用將和下一個裝飾器一起展示。

3.1.2 functools.lru_cache裝飾器

functools.lru_cache實現了備忘(memoization)功能,這是一項優化技術,他把耗時的函數的結果保存起來,避免傳入相同參數時重復計算。以斐波那契函數為例,我們知道以遞歸形式實現的斐波那契函數會出現很多重復計算,此時,就可以使用這個裝飾器。以下代碼是沒使用該裝飾器時的運行情況:

# 代碼17
from clockdeco import clock

@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)

if __name__ == "__main__":
    print(fibonacci.__name__)
    print(fibonacci.__doc__)
    print(fibonacci(6))

# 結果:
fibonacci  # fibonacci原本的屬性得到了保留
None
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00000000s] fibonacci(3) -> 2
[0.00000000s] fibonacci(4) -> 3
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00000000s] fibonacci(3) -> 2
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00049996s] fibonacci(2) -> 1
[0.00049996s] fibonacci(3) -> 2
[0.00049996s] fibonacci(4) -> 3
[0.00049996s] fibonacci(5) -> 5
[0.00049996s] fibonacci(6) -> 8
8

可以看出,fibonacci(1)調用了8次,下面我們用functools.lru_cache來改進上述代碼:

# 代碼18
import functools
from clockdeco import clock

@functools.lru_cache()  # 注意此處有個括號!該裝飾器就收參數!不能省!
@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)

if __name__ == "__main__":
    print(fibonacci(6))
    
# 結果:
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00000000s] fibonacci(2) -> 1
[0.00000000s] fibonacci(3) -> 2
[0.00000000s] fibonacci(4) -> 3
[0.00000000s] fibonacci(5) -> 5
[0.00000000s] fibonacci(6) -> 8
8

functools.lru_cache裝飾器可以接受參數,并且此代碼還疊放了裝飾器。

lru_cache有兩個參數:functools.lru_cache(maxsize=128, typed=False)

maxsize指定存儲多少個調用的結果,該參數最好是2的冪。當緩存滿后,根據LRU算法替換緩存中的內容,這也是為什么這個函數叫lru_cache

type如果設置為True,它將把不同參數類型下得到的結果分開保存,即把通常認為相等的浮點數和整數參數分開(比如區分1和1.0)。

lru_cache使用字典存儲結果,字典的鍵是傳入的參數,所以被lru_cache裝飾的函數的所有參數都必須是可散列的!

3.1.3 functools.singledispatch裝飾器

我們知道,C++支持函數重載,同名函數可以根據參數類型的不同而調用相應的函數。以Python代碼為例,我們希望下面這個函數表現出如下行為:

# 代碼19
def myprint(obj):
    return "Hello~~~"

# 以下是我們希望它擁有的行為:
>>> myprint(1)
Hello~~~
>>> myprint([])
Hello~~~
>>> myprint("hello") # 即,當我們傳入特定類型的參數時,函數返回特定的結果
This is a str

單憑這一個myprint還無法實現上述要求,因為Python不支持方法或函數的重載。為了實現類似的功能,一種常見的做法是將函數變為一個分派函數,使用一串if/elif/elif來判斷參數類型,再調用專門的函數(如myprint_str),但這種方式不利于代碼的擴展和維護,還顯得沒有B格。。。

為解決這個問題,從Python3.4開始,可以使用functools.singledispath裝飾器,把整體方案拆分成多個模塊,甚至可以為無法修改的類提供專門的函數。被@singledispatch裝飾的函數會變成泛函數(generic function),它會根據第一個參數的不同而調用響應的專門函數,具體用法如下:

# 代碼20
from functools import singledispatch
import numbers

@singledispatch
def myprint(obj):
    return "Hello~~~"

# 可以疊放多個register,讓同一函數支持不同類型
@myprint.register(str)
# 注冊的專門函數最好處理抽象基類,而不是具體實現,這樣代碼支持的兼容類型更廣泛
@myprint.register(numbers.Integral) 
def _(text): # 專門函數的名稱無所謂,使用 _ 可以避免起名字的麻煩
    return "Special types"

對泛函數的補充:根據參數類型的不同,以不同方式執行相同操作的一組函數。如果依據是第一個參數,則是單分派;如果依據是多個參數,則是多分派。

3.2 參數化裝飾器 3.2.1 簡單版參數化裝飾器

從上面諸多例子我們可以看到兩大類裝飾器:不帶參數的裝飾器(調用時最后沒有括號)和帶參數的裝飾器(帶括號)。Python將被裝飾的函數作為第一個參數傳給了裝飾器函數,那裝飾器函數如何接受其他參數呢?做法是:創建一個裝飾器工廠函數,在這個工廠函數內部再定義其它函數作為真正的裝飾器。工廠函數代為接受參數,這些參數作為自由變量供裝飾器使用。然后工廠函數返回裝飾器,裝飾器再應用到被裝飾函數上。

我們把1.2中代碼4@register裝飾器改為帶參數的版本,以active參數來指示裝飾器是否注冊某函數(雖然這么做有點多余)。這里只給出@register裝飾器的實現,其余代碼參考代碼4

# 代碼21
registry = set()

def register(active=True):
    def decorate(func): # 變量active對于decorate函數來說是自由變量
        print("running register(active=%s)->decorate(%s)" % (active, func))
        if active: 
            registry.add(func)
        else:
            registry.discard(func)
        return func
    return decorate

# 用法
@register(active=False) # 即使不傳參數也要作為函數調用@register()
def f():pass

# 上述用法相當于如下代碼:
# register(active=False)(f)
3.2.2 多層嵌套版參數化裝飾器

參數化裝飾器通常會把被裝飾函數替換掉,而且結構上需要多一層嵌套。下面以3.1.1中代碼16里的@clock裝飾器為例,讓它按用戶要求的格式輸出數據。為了簡便,不調用functools.wraps裝飾器:

# 代碼22
import time

DEFAULT_FMT = "[{elapsed:0.8f}s] {name}({args}) -> {result}"

def clock(fmt=DEFAULT_FMT):   # 裝飾器工廠,fmt是裝飾器的參數
    def decorate(func):       # 裝飾器
        def clocked(*_args):  # 最終的函數
            t0 = time.time()
            _result = func(*_args)
            elapsed = time.time() - t0
            name = func.__name__
            args = ", ".join(repr(arg) for arg in _args)
            result = repr(_result)
            print(fmt.format(**locals())) #locals()函數以字典形式返回clocked的局部變量
            return _result
        return clocked
    return decorate

可以得到如下結論:裝飾器函數有且只有一個參數,即被裝飾器的函數;如果裝飾器要接受其他參數,請在原本的裝飾器外再套一層函數(工廠函數),由它來接受其余參數;而你最終使用的函數應該定義在裝飾器函數中,且它的參數列表應該和被裝飾的函數一致。

4. 總結

本篇首先介紹了最簡單裝飾器如何定義和使用,介紹了裝飾器在什么時候被執行,以及用最簡單的裝飾器改造了上一篇的策略模式;隨后更進一步,介紹了與閉包相關的概念,包括變量作用域,閉包和nonlocal聲明;最后介紹了更復雜的裝飾器,包括標準庫中的裝飾器的用法,以及如何定義帶參數的裝飾器。

但上述對裝飾器的描述都是基本的, 更復雜、工業級的裝飾器還需要更深入的學習。

迎大家關注我的微信公眾號"代碼港" & 個人網站 www.vpointer.net ~

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/41811.html

相關文章

  • Python中的函數裝飾器和閉包

    摘要:變量查找規則在中一個變量的查找順序是局部環境,閉包,全局,內建閉包引用了自由變量的函數。閉包的作用閉包的最大特點是可以將父函數的變量與內部函數綁定,并返回綁定變量后的函數,此時即便生成閉包的環境父函數已經釋放,閉包仍然存在。 導語:本文章記錄了本人在學習Python基礎之函數篇的重點知識及個人心得,打算入門Python的朋友們可以來一起學習并交流。 本文重點: 1、掌握裝飾器的本質、功...

    caozhijian 評論0 收藏0
  • 流暢的python讀書筆記-第七章-函數裝飾器和閉包

    摘要:函數裝飾器和閉包嚴格來說,裝飾器只是語法糖。何時執行裝飾器它們在被裝飾的函數定義之后立即運行。裝飾器突出了被裝飾的函數的作用,還便于臨時禁用某個促銷策略只需把裝飾器注釋掉。 函數裝飾器和閉包 嚴格來說,裝飾器只是語法糖。如前所示,裝飾器可以像常規的可調用對象那樣調用,其參數是另一個函數。有時,這樣做更方便,尤其是做元編程(在運行時改變程序的行為)時。 Python何時執行裝飾器 它們在...

    Hydrogen 評論0 收藏0
  • Python_裝飾器和生成器

    摘要:迭代器迭代是訪問集合元素的一種方式。迭代器是一個可以記住遍歷的位置的對象,迭代器對象從集合的第一個元素開始訪問,直到所有的元素被訪問完結束,迭代器只往前不會往后退。生成器特點保存了一套生成數值的算法。 迭代器 迭代是訪問集合元素的一種方式。迭代器是一個可以記住遍歷的位置的對象,迭代器對象從集合的第一個元素開始訪問,直到所有的元素被訪問完結束,迭代器只往前不會往后退。 可迭代對象 以直接...

    sugarmo 評論0 收藏0
  • Python學習之路8.2-對Python類的補充

    摘要:本章主要是對上一章類的補充。對于多態的補充子類可以被看成是父類的類型,但父類不能被看成是子類的類型。仍然以類為例,動物里有哺乳動物,卵生動物,有能飛的動物和不能飛的動物,這是兩種大的分類方式。一般在中,以為結尾類的都作為接口。 《Python編程:從入門到實踐》筆記。本章主要是對上一章Python類的補充。 1. 從一個類派生出所有類 上一篇文章說道Python類的定義與繼承一般是如下...

    liukai90 評論0 收藏0
  • Python閉包裝飾

    摘要:所以,有另一種說法認為閉包是由函數和與其相關的引用環境組合而成的實體。 對于已經對 閉包 或者 裝飾器有一定概念的,可以直接通過右側標題目錄直接定位到相應段落查看所需的內容。 什么是裝飾器? 裝飾器(Decorator)相對簡單,咱們先介紹它:裝飾器的功能是將被裝飾的函數當作參數傳遞給與裝飾器對應的函數(名稱相同的函數),并返回包裝后的被裝飾的函數,聽起來有點繞,沒關系,直接看示意圖,...

    justCoding 評論0 收藏0

發表評論

0條評論

最新活動
閱讀需要支付1元查看
<