今天就跟大家聊聊有關(guān)函數(shù)裝飾器怎么在Python 中使用,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。
創(chuàng)新互聯(lián)專業(yè)為企業(yè)提供邕寧網(wǎng)站建設(shè)、邕寧做網(wǎng)站、邕寧網(wǎng)站設(shè)計(jì)、邕寧網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁(yè)設(shè)計(jì)與制作、邕寧企業(yè)網(wǎng)站模板建站服務(wù),十余年邕寧做網(wǎng)站經(jīng)驗(yàn),不只是建網(wǎng)站,更提供有價(jià)值的思路和整體網(wǎng)絡(luò)服務(wù)。裝飾器的基本概念
裝飾器是一個(gè)可調(diào)用對(duì)象,它的參數(shù)是另一個(gè)函數(shù),稱為被裝飾函數(shù)。裝飾器可以修改這個(gè)函數(shù)再將其返回,也可以將其替換為另一個(gè)函數(shù)或者可調(diào)用對(duì)象。
例如:有個(gè)名為decorate
的裝飾器:
@decorate def target(): print('running target()')
上述代碼的寫法和以下寫法的效果是一樣的:
def target(): print('running target()') target = decorate(target)
但是,它們返回的target
不一定是原來(lái)的那個(gè)target
函數(shù),例如下面這個(gè)例子:
>>> def deco(func): ... def inner(): ... print('running inner()') ... return inner ... >>> @deco ... def target(): ... print('running target()') ... >>> target() running inner() >>> target <function deco.<locals>.inner at 0x0000013D88563040>
可以看到,調(diào)用target
函數(shù)執(zhí)行的是inner
函數(shù),這里的target
實(shí)際上是inner
的引用。
裝飾器的另一個(gè)關(guān)鍵特性是,它們?cè)诒谎b飾函數(shù)定義時(shí)立即執(zhí)行,這通常是發(fā)生在導(dǎo)入模塊的時(shí)候。
例如下面的這個(gè)模塊:registration.py
# 存儲(chǔ)被裝飾器 @register 裝飾的函數(shù) registry = [] # 裝飾器 def register(func): print(f"注冊(cè)函數(shù) -> {func}") # 記錄被裝飾的函數(shù) registry.append(func) return func @register def f1(): print("執(zhí)行 f1()") @register def f2(): print("執(zhí)行 f2()") def f3(): print("執(zhí)行 f3()") if __name__ == "__main__": print("執(zhí)行主函數(shù)") print("registry -> ", registry) f1() f2() f3()
現(xiàn)在我們?cè)诿钚袌?zhí)行這個(gè)腳本:
$ python registration.py 注冊(cè)函數(shù) -> <function f1 at 0x000001F6FC8320D0> 注冊(cè)函數(shù) -> <function f2 at 0x000001F6FC832160> 執(zhí)行主函數(shù) registry -> [<function f1 at 0x000001F6FC8320D0>, <function f2 at 0x000001F6FC832160>] 執(zhí)行 f1() 執(zhí)行 f2() 執(zhí)行 f3()
這里我們可以看到,在主函數(shù)執(zhí)行之前,register
已經(jīng)執(zhí)行了兩次。加載模塊后,registry
中已經(jīng)有兩個(gè)被裝飾函數(shù)的引用:f1
和f2
。不過(guò)這兩個(gè)函數(shù)以及f3
都是在腳本中明確調(diào)用后才開始執(zhí)行的。
如果只是單純的導(dǎo)入registration.py
模塊而不運(yùn)行:
>>> import registration 注冊(cè)函數(shù) -> <function f1 at 0x0000022670012280> 注冊(cè)函數(shù) -> <function f2 at 0x0000022670012310>
查看registry
中的值:
>>> registration.registry [<function f1 at 0x0000022670012280>, <function f2 at 0x0000022670012310>]
這個(gè)例子主要說(shuō)明:裝飾器在導(dǎo)入模塊時(shí)立即執(zhí)行,而被裝飾的函數(shù)只有在明確調(diào)用時(shí)才運(yùn)行。這也突出了 Python 中導(dǎo)入時(shí)和運(yùn)行時(shí)這個(gè)兩個(gè)概念的區(qū)別。
在裝飾器的實(shí)際使用中,有兩點(diǎn)和示例是不同的:
示例中裝飾器和被裝飾函數(shù)在同一個(gè)模塊中。實(shí)際使用中,裝飾器通常在一個(gè)單獨(dú)的模塊中定義,然后再應(yīng)用到其它模塊的函數(shù)上。
示例中register
裝飾器返回的函數(shù)和傳入的參數(shù)相同。實(shí)際使用中,裝飾器會(huì)在內(nèi)部定義一個(gè)新函數(shù),然后將其返回。
裝飾器內(nèi)部定義并返回新函數(shù)的做法需要靠閉包才能正常運(yùn)作。為了理解閉包,則必須先了解 Python 中的變量作用域。
我們來(lái)看下面這個(gè)例子,一個(gè)函數(shù)讀取一個(gè)局部變量a
,一個(gè)全局變量b
。
>>> def f1(a): ... print(a) ... print(b) ... >>> f1(3) 3 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 3, in f1 NameError: name 'b' is not defined
出現(xiàn)錯(cuò)誤并不奇怪。如果我們先給b
賦值,再調(diào)用f1
,那就不會(huì)出錯(cuò)了:
>>> b = 1 >>> f1(3) 3 1
現(xiàn)在,我們來(lái)看一個(gè)不尋常的例子:
>>> b = 1 >>> def f2(a): ... print(a) ... print(b) ... b = 2 ... >>> f2(3) 3 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 3, in f2 UnboundLocalError: local variable 'b' referenced before assignment
這里,f2
函數(shù)的前兩行和f1
相同,然后再給b
賦值??墒牵谫x值之前,第二個(gè)print
失敗了。這是因?yàn)镻ython 在編譯函數(shù)的定義體時(shí),發(fā)現(xiàn)在函數(shù)中有給b
賦值的語(yǔ)句,因此判斷它是局部變量。而在上述示例中,當(dāng)我們打印局部變量b
時(shí),它并沒(méi)有被綁定值,故而報(bào)錯(cuò)。
Python 不要求聲明變量,但是會(huì)把在函數(shù)定義體中賦值的變量當(dāng)成局部變量。
如果想把上述示例中的b
看成全局變量,則需要使用global
聲明:
>>> b = 1 >>> def f3(a): ... global b ... print(a) ... print(b) ... b = 2 ... >>> f3(3) 3 1 >>> b 2 >>> f3(3) 3 2
閉包是指延伸了作用域的函數(shù),其中包含了函數(shù)定義體中的引用,以及不在定義體中定義的非全局變量。
我們通過(guò)以下示例來(lái)理解這句話。
假設(shè)我們有這種需求,計(jì)算某個(gè)商品在整個(gè)歷史中的平均收盤價(jià)格(商品每天的價(jià)格會(huì)變化)。例如:
>>> avg(10) 10.0 >>> avg(11) 10.5 >>> avg(12) 11.0
那么如何獲取avg
函數(shù)?歷史收盤價(jià)格又是如何保存的?
我們可以用一個(gè)類來(lái)實(shí)現(xiàn):
class Averager: def __init__(self): self.serial = [] def __call__(self, price): self.serial.append(price) return sum(self.serial) / len(self.serial)
Averager
的實(shí)例是一個(gè)可調(diào)用對(duì)象。
>>> avg = Averager() >>> avg(10) 10.0 >>> avg(11) 10.5 >>> avg(12) 11.0
也可以使用一個(gè)函數(shù)來(lái)實(shí)現(xiàn):
>>> def make_averager(): ... serial = [] ... def averager(price): ... serial.append(price) ... return sum(serial) / len(serial) ... return averager ... >>> avg = make_averager() >>> avg(10) 10.0 >>> avg(11) 10.5 >>> avg(12) 11.0
第一種寫法很明顯的可以看到,所有歷史收盤價(jià)均保存在實(shí)例變量self.serial
中。
第二種寫法我們要好好的分析一下:serial
是make_averager
的局部變量,但是當(dāng)我們調(diào)用avg(10)
時(shí),make_averager
函數(shù)已經(jīng)返回了,它的作用域不是應(yīng)該消失了嗎?
實(shí)際上,在averager
函數(shù)中,serial
是自由變量(未在本地作用域中綁定的變量)。如下圖所示:
我們可以在averager
返回對(duì)象的__code__
屬性中查看它的局部變量和自由變量的名字。
>>> avg.__code__.co_varnames ('price',) >>> avg.__code__.co_freevars ('serial',)
自由變量serial
綁定的值存放在avg
對(duì)象的__closure__
屬性中,它是一個(gè)元組,里面的元素是cell
對(duì)象,它的cell_contents
屬性保存實(shí)際的值:
>>> avg.__closure__ (<cell at 0x000002266FF99430: list object at 0x00000226702841C0>,) >>> avg.__closure__[0].cell_contents [10, 11, 12]
綜上所述,閉包是一種函數(shù),它會(huì)保留定義函數(shù)時(shí)存在的自由變量的綁定值,這樣在我們調(diào)用這個(gè)函數(shù)時(shí),即使作用域不在了,仍然可以使用這些綁定的值。
注意:
只有嵌套在其它函數(shù)中的函數(shù)才可能需要處理不在全局作用域中的外部變量。
前面的make_averager
方法的效率并不高,我們可以只保存當(dāng)前的總值和元素個(gè)數(shù),再使用它們計(jì)算平均值。下面是我們更改后的函數(shù)體:
>>> def make_averager(): ... count = total = 0 ... def averager(price): ... count += 1 ... total += price ... return total / count ... return averager
但是這個(gè)寫法實(shí)際上是有問(wèn)題的,我們先運(yùn)行再分析:
>>> avg = make_averager() >>> avg(10) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in averager UnboundLocalError: local variable 'count' referenced before assignment
這里count
被當(dāng)成averager
的局部變量,而不是我們期望的自由變量。這是因?yàn)?code>count += 1 相當(dāng)于count = count + 1
。因此,我們?cè)?code>averager 函數(shù)體中實(shí)際包含了給count
賦值的操作,這就把count
變成局部變量。total
也有這個(gè)問(wèn)題。
為了解決這個(gè)問(wèn)題,Python3 引入了nonlocal
關(guān)鍵字,用于聲明自由變量。使用nonlocal
修改上述的例子:
>>> def make_averager(): ... count = total = 0 ... def averager(price): ... nonlocal count, total ... count += 1 ... total += price ... return total / count ... return averager ... >>> avg = make_averager() >>> avg(10) 10.0 >>> avg(11) 10.5 >>> avg(12) 11.0
如果我們把@d1
和@d2
兩個(gè)裝飾器應(yīng)用到同一個(gè)函數(shù)f()
上,實(shí)際相當(dāng)于f = d1(d2(f))
。
也就是說(shuō),下屬代碼:
@d1 @d2 def f(): pass
等同于:
def f(): pass f = d1(d2(f))
Python 會(huì)把被裝飾的參數(shù)作為第一個(gè)參數(shù)傳遞給裝飾器函數(shù),那么如何讓裝飾器接受其它的參數(shù)呢?這里我們需要定義一個(gè)裝飾器工廠函數(shù),返回真正的裝飾器函數(shù)。
以本文開頭的register
裝飾器為例,我們?yōu)樗砑右粋€(gè)active
參數(shù),如果置為False
,那就不注冊(cè)這個(gè)函數(shù)。
registry = [] def register(active=True): def decorate(func): if active: print(f"注冊(cè)函數(shù) -> {func}") # 記錄被裝飾的函數(shù) registry.append(func) return func return decorate @register() def f1(): print("執(zhí)行 f1") @register(active=False) def f2(): print("執(zhí)行 f2")
現(xiàn)在我們導(dǎo)入這個(gè)模塊:
>>> import registration 注冊(cè)函數(shù) -> <function f1 at 0x0000016D80402280>
可以看到只注冊(cè)了f1
函數(shù)。
這里我們使用嵌套函數(shù)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的裝飾器:計(jì)算被裝飾函數(shù)執(zhí)行的耗時(shí),并將函數(shù)名、參數(shù)和執(zhí)行的結(jié)果打印出來(lái)。
import time def clock(func): def clocked(*args): start_time = time.perf_counter() result = func(*args) cost = time.perf_counter() - start_time print( "[%.2f] %s(%s) -> %r" % (cost, func.__name__, list(map(repr, args)), result) ) return result return clocked
下面我們來(lái)試試這個(gè)裝飾器:
>>> @clock ... def factorial(n): ... # 計(jì)算 n 的階乘 ... return 1 if n < 2 else n * factorial(n - 1) >>> >>> factorial(6) [0.00] factorial(['1']) -> 1 [0.00] factorial(['2']) -> 2 [0.00] factorial(['3']) -> 6 [0.00] factorial(['4']) -> 24 [0.00] factorial(['5']) -> 120 [0.00] factorial(['6']) -> 720 720
具體來(lái)分析一下,這里factorial
作為func
參數(shù)傳遞給clock
函數(shù),然后clock
函數(shù)返回clocked
函數(shù),Python 解釋器會(huì)把clocked
賦值給factorial
。所以,如果我們查看factorial
的__name__
屬性,會(huì)發(fā)現(xiàn)它的值是clocked
而不是factorial
。
>>> factorial.__name__ 'clocked'
所以,factorial
保存的是clocked
的引用,每次調(diào)用factorial
實(shí)際上都是在調(diào)用clocked
函數(shù)。
我們也可以使用functools.wraps
裝飾器把func
的一些屬性復(fù)制到clocked
函數(shù)上,例如:__name__
和__doc__
:
def clock(func): @functools.wraps(func) def clocked(*args): start_time = time.perf_counter() result = func(*args) cost = time.perf_counter() - start_time print( "[%.2f] %s(%s) -> %r" % (cost, func.__name__, list(map(repr, args)), result) ) return result return clocked >>> >>> @clock ... def factorial(n): ... return 1 if n < 2 else n * factorial(n - 1) >>> >>> factorial.__name__ 'factorial'
使用functools.lru_cache
做備忘
functools.lru_cache
會(huì)把耗時(shí)的函數(shù)的結(jié)果保存起來(lái),避免傳入相同的參數(shù)時(shí)的重復(fù)計(jì)算。lru
的意思是Least Recently Used,表示緩存不會(huì)無(wú)限增長(zhǎng),一段時(shí)間不用的緩存條目會(huì)被丟棄。
lru_cache
非常適合計(jì)算第 n 個(gè)斐波那契數(shù)這樣的慢速遞歸函數(shù)。
我們來(lái)看看不使用lru_cache
時(shí)的情況:
>>> @clock ... def fibonacci(n): ... return n if n < 2 else fibonacci(n - 2) + fibonacci(n - 1) ... >>> fibonacci(6) [0.00000040] fibonacci(['0']) -> 0 [0.00000060] fibonacci(['1']) -> 1 [0.00030500] fibonacci(['2']) -> 1 [0.00000030] fibonacci(['1']) -> 1 [0.00000040] fibonacci(['0']) -> 0 [0.00000060] fibonacci(['1']) -> 1 [0.00042110] fibonacci(['2']) -> 1 [0.00074440] fibonacci(['3']) -> 2 [0.00128530] fibonacci(['4']) -> 3 [0.00000020] fibonacci(['1']) -> 1 [0.00000030] fibonacci(['0']) -> 0 [0.00000050] fibonacci(['1']) -> 1 [0.00035500] fibonacci(['2']) -> 1 [0.00055270] fibonacci(['3']) -> 2 [0.00000030] fibonacci(['0']) -> 0 [0.00000060] fibonacci(['1']) -> 1 [0.00041220] fibonacci(['2']) -> 1 [0.00000040] fibonacci(['1']) -> 1 [0.00000040] fibonacci(['0']) -> 0 [0.00000050] fibonacci(['1']) -> 1 [0.00032410] fibonacci(['2']) -> 1 [0.00061420] fibonacci(['3']) -> 2 [0.00122760] fibonacci(['4']) -> 3 [0.00206850] fibonacci(['5']) -> 5 [0.00352630] fibonacci(['6']) -> 8 8
這種方式有很多重復(fù)的計(jì)算,例如fibonacci(['1'])
執(zhí)行了 8 次,fibonacci(['2'])
執(zhí)行了 5 次等等。
現(xiàn)在我們使用functools.lru_cache
優(yōu)化一下:
>>> @functools.lru_cache ... @clock ... def fibonacci(n): ... return n if n < 2 else fibonacci(n - 2) + fibonacci(n - 1) ... >>> fibonacci(6) [0.00000060] fibonacci(['0']) -> 0 [0.00000070] fibonacci(['1']) -> 1 [0.00106320] fibonacci(['2']) -> 1 [0.00000080] fibonacci(['3']) -> 2 [0.00132790] fibonacci(['4']) -> 3 [0.00000060] fibonacci(['5']) -> 5 [0.00159670] fibonacci(['6']) -> 8 8
可以看到節(jié)省了一般的執(zhí)行時(shí)間,并且 n 的每個(gè)值只調(diào)用了一次函數(shù)。
在執(zhí)行fibonacci(30)
時(shí),如果使用未優(yōu)化的版本需要 141 秒,使用優(yōu)化后的版本只需要 0.002 秒。
除了優(yōu)化遞歸算法之外,lru_cache
在從 WEB 獲取信息的應(yīng)用中也能發(fā)揮巨大作用。
lru_cache
還有兩個(gè)可選參數(shù):
def lru_cache(maxsize=128, typed=False):
maxsize
:最多可存儲(chǔ)的調(diào)用結(jié)果的個(gè)數(shù)。緩存滿了之后,舊的結(jié)果被丟棄。為了獲取很好的性能,maxsize
應(yīng)該設(shè)置為 2 的冪。
typed
:如果置為True
,會(huì)把不同參數(shù)類型得到的結(jié)果分開保存。例如:f(3.0)
和f(3)
會(huì)被當(dāng)成不同的調(diào)用。
單分派泛函數(shù)
假設(shè)我們現(xiàn)在開發(fā)一個(gè)調(diào)試 WEB 應(yīng)用的工具:生成 HTML,顯示不同類型的 Python 對(duì)象。
我們可以這樣編寫一個(gè)函數(shù):
import html def htmlize(obj): content = html.escape(repr(obj)) return f"<pre>{content}</pre>"
現(xiàn)在我們需要做一些拓展,讓它使用特別的方式顯示某些特定類型:
str
:把字符串內(nèi)部的 替換為
<br>
,并且使用<p>
替換<pre>
;
int
:以十進(jìn)制和十六進(jìn)制顯示數(shù)字;
list
:顯示一個(gè) HTML 列表,根據(jù)各個(gè)元素的類型格式化;
最常用的方式就是寫if...elif..else
判斷:
import numbers from collections.abc import MutableSequence def htmlize(obj): if isinstance(obj, str): content = obj.replace("\n", "<br>\n") return f"<p>{content}</p>" elif isinstance(obj, numbers.Integral): content = f"{obj} ({hex(obj)})" return f"<pre>{content}</pre>" elif isinstance(obj, MutableSequence): content = "</li>\n<li>".join(htmlize(item) for item in obj) return "<ul>\n<li>" + content + "</li>\n</ul>" else: content = f"<pre>{obj}</pre>" return content
如果想添加新的類型判斷,只會(huì)將函數(shù)越寫越長(zhǎng),并且各個(gè)類型之間耦合度較高,不利于維護(hù)。
Python 3.4 新增的functools.singledispatch
裝飾器可以將整個(gè)方案拆分成多個(gè)模塊。
import numbers from collections.abc import MutableSequence from functools import singledispatch @singledispatch def htmlize(obj): content = f"<pre>{obj}</pre>" return content @htmlize.register(str) def _(text): content = text.replace("\n", "<br>\n") return f"<p>{content}</p>" @htmlize.register(numbers.Integral) def _(num): content = f"{num} ({hex(num)})" return f"<pre>{content}</pre>" @htmlize.register(MutableSequence) def _(seq): content = "</li>\n<li>".join(htmlize(item) for item in seq) return "<ul>\n<li>" + content + "</li>\n</ul>"
看完上述內(nèi)容,你們對(duì)函數(shù)裝飾器怎么在Python 中使用有進(jìn)一步的了解嗎?如果還想了解更多知識(shí)或者相關(guān)內(nèi)容,請(qǐng)關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝大家的支持。
網(wǎng)站欄目:函數(shù)裝飾器怎么在Python中使用-創(chuàng)新互聯(lián)
分享網(wǎng)址:http://vcdvsql.cn/article44/dshihe.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站導(dǎo)航、品牌網(wǎng)站制作、網(wǎng)站策劃、面包屑導(dǎo)航、網(wǎng)頁(yè)設(shè)計(jì)公司、響應(yīng)式網(wǎng)站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)
猜你還喜歡下面的內(nèi)容