引言
在 Python 編程語(yǔ)言中势告,閉包通常指的是一個(gè)嵌套函數(shù)啦逆,即在一個(gè)函數(shù)內(nèi)部定義的另一個(gè)函數(shù)阎毅。這個(gè)嵌套的函數(shù)能夠訪問(wèn)并保留其外部函數(shù)作用域中的變量塔橡。這種結(jié)構(gòu)就構(gòu)成了一個(gè)閉包佳镜。
閉包在函數(shù)式編程語(yǔ)言中非常普遍裆馒。在 Python 中姊氓,閉包特別有用,因?yàn)樗沟媚憧梢詣?chuàng)建基于函數(shù)的裝飾器喷好,這是一種非常強(qiáng)大的功能翔横。
通過(guò)本教程,你將:
- 了解閉包的概念以及它們?cè)?Python 中的運(yùn)作方式
- 掌握閉包的典型應(yīng)用場(chǎng)景
- 探索閉包的替代方法 為了更好地理解本教程梗搅,你需要對(duì) Python 的一些基本概念有所了解禾唁,比如函數(shù)、嵌套函數(shù)些膨、裝飾器蟀俊、類(lèi)和可調(diào)用對(duì)象。
用閉包編寫(xiě)裝飾器
裝飾器是 Python 中一個(gè)非常強(qiáng)大的功能订雾,它允許你動(dòng)態(tài)地修改函數(shù)的行為肢预。在 Python 中,有兩種類(lèi)型的裝飾器:
- 基于函數(shù)的裝飾器
- 基于類(lèi)的裝飾器
基于函數(shù)的裝飾器是一個(gè)函數(shù)洼哎,它接受一個(gè)函數(shù)對(duì)象作為參數(shù)烫映,并返回另一個(gè)增加了額外功能的函數(shù)對(duì)象。這個(gè)返回的函數(shù)對(duì)象也是一個(gè)閉包噩峦。因此锭沟,在創(chuàng)建基于函數(shù)的裝飾器時(shí),你會(huì)用到閉包识补。
如你所知族淮,裝飾器可以在不修改函數(shù)內(nèi)部代碼的情況下改變函數(shù)的行為。實(shí)際上,基于函數(shù)的裝飾器就是閉包祝辣。它們的特點(diǎn)是主要用來(lái)修改你傳遞給裝飾器函數(shù)的函數(shù)行為贴妻。
這里有一個(gè)簡(jiǎn)單的裝飾器示例,它在原有函數(shù)功能的基礎(chǔ)上增加了額外的消息輸出:
>>> def decorator(function):
... def closure():
... print("Doing something before calling the function.")
... function()
... print("Doing something after calling the function.")
... return closure
...
在這個(gè)示例中蝙斜,外層函數(shù)充當(dāng)裝飾器的角色名惩。這個(gè)函數(shù)返回一個(gè)閉包對(duì)象,它通過(guò)增加額外的功能來(lái)改變被裝飾的輸入函數(shù)對(duì)象的原有行為孕荠。即便是在 decorator()
函數(shù)執(zhí)行完畢后娩鹉,閉包仍然能夠?qū)斎牒瘮?shù)產(chǎn)生影響。
以下是你如何利用裝飾器語(yǔ)法來(lái)動(dòng)態(tài)地改變一個(gè)普通 Python 函數(shù)的行為:
>>> @decorator
... def greet():
... print("Hi, Pythonista!")
...
>>> greet()
Doing something before calling the function.
Hi, Pythonista!
Doing something after calling the function.
在這個(gè)示例中稚伍,你通過(guò) @decorator
來(lái)調(diào)整 greet()
函數(shù)的行為弯予。請(qǐng)注意,現(xiàn)在調(diào)用 greet()
時(shí)个曙,你不僅得到了它的基本功能熙涤,還額外獲得了裝飾器提供的功能。
利用閉包實(shí)現(xiàn)記憶化
緩存能夠通過(guò)減少不必要的重復(fù)計(jì)算來(lái)提升算法的效率困檩。記憶化是一種防止函數(shù)對(duì)相同輸入多次執(zhí)行的常用緩存技術(shù)。
記憶化的工作原理是將特定輸入?yún)?shù)集的結(jié)果存儲(chǔ)在內(nèi)存中那槽,之后在需要時(shí)直接引用這些結(jié)果悼沿。你可以利用閉包來(lái)實(shí)現(xiàn)記憶化。
在下面的示例中骚灸,你使用了一個(gè)裝飾器——它本身也是一個(gè)閉包——來(lái)緩存一個(gè)假設(shè)的糟趾、計(jì)算成本高昂的函數(shù)的結(jié)果值:
>>> def memoize(function):
... cache = {}
... def closure(number):
... if number not in cache:
... cache[number] = function(number)
... return cache[number]
... return closure
...
在這個(gè)例子中,memoize()
函數(shù)接收一個(gè)函數(shù)對(duì)象作為參數(shù)甚牲,并返回一個(gè)新的閉包對(duì)象义郑。這個(gè)內(nèi)部函數(shù)僅對(duì)尚未處理的數(shù)字執(zhí)行輸入函數(shù)。已處理的數(shù)字及其輸入函數(shù)的結(jié)果被存儲(chǔ)在 cache
字典中丈钙,以供后續(xù)使用非驮。
現(xiàn)在,假設(shè)你有一個(gè)如下的示例函數(shù)雏赦,它模擬了一個(gè)計(jì)算成本較高的操作:
>>> from time import sleep
>>> def slow_operation(number):
... sleep(0.5)
...
該函數(shù)將代碼的執(zhí)行僅保留半秒劫笙,以模仿昂貴的操作。為此星岗,您可以使用時(shí)間模塊中的 sleep() 函數(shù)填大。
您可以使用以下代碼測(cè)量函數(shù)的執(zhí)行時(shí)間:
>>> from timeit import timeit
>>> timeit(
... "[slow_operation(number) for number in [2, 3, 4, 2, 3, 4]]",
... globals=globals(),
... number=1,
... )
3.02610950000053
在這個(gè)代碼片段中,你利用了 timeit
模塊的 timeit()
函數(shù)來(lái)測(cè)量執(zhí)行 slow_operation()
函數(shù)時(shí)俏橘,使用一系列值作為輸入的耗時(shí)允华。處理六個(gè)輸入值時(shí),代碼耗時(shí)略超過(guò)三秒。你可以通過(guò)跳過(guò)重復(fù)的輸入值靴寂,并使用記憶化技術(shù)來(lái)提高這個(gè)計(jì)算過(guò)程的效率磷蜀。
接下來(lái),按照下面的例子使用 @memoize
裝飾器來(lái)裝飾 slow_operation()
函數(shù)榨汤。然后蠕搜,執(zhí)行計(jì)時(shí)代碼:
>>> @memoize
... def slow_operation(number):
... sleep(0.5)
...
>>> timeit(
... "[slow_operation(number) for number in [2, 3, 4, 2, 3, 4]]",
... globals=globals(),
... number=1,
... )
1.5151869590008573
現(xiàn)在,由于采用了記憶化技術(shù)收壕,相同代碼的執(zhí)行時(shí)間縮短了一半妓灌。這是因?yàn)?slow_operation()
函數(shù)不會(huì)對(duì)重復(fù)的輸入值再次執(zhí)行。
利用閉包實(shí)現(xiàn)封裝
在面向?qū)ο缶幊蹋∣OP)中蜜宪,類(lèi)提供了一種將數(shù)據(jù)和行為整合到單個(gè)實(shí)體中的機(jī)制虫埂。OOP 中的一個(gè)核心需求是數(shù)據(jù)封裝,這一原則建議保護(hù)對(duì)象的數(shù)據(jù)不受外部干擾圃验,并阻止直接訪問(wèn)掉伏。
在 Python 中,實(shí)現(xiàn)嚴(yán)格的數(shù)據(jù)封裝可能比較困難澳窑,因?yàn)?Python 中并沒(méi)有私有和公共屬性的區(qū)分斧散。相反,Python 通過(guò)命名約定來(lái)表明某個(gè)類(lèi)成員是公開(kāi)的還是非公開(kāi)的摊聋。
你可以利用 Python 閉包來(lái)實(shí)現(xiàn)更嚴(yán)格的數(shù)據(jù)封裝鸡捐。閉包能夠?yàn)閿?shù)據(jù)創(chuàng)建一個(gè)私有的作用域,阻止用戶(hù)直接訪問(wèn)這些數(shù)據(jù)麻裁,從而有助于保持?jǐn)?shù)據(jù)的完整性并防止意外修改箍镜。
例如,假設(shè)你有一個(gè)如下的 Stack 類(lèi):
class Stack:
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
return self._items.pop()
該 Stack 類(lèi)將其數(shù)據(jù)存儲(chǔ)在名為 ._items 的列表對(duì)象中煎源,并實(shí)現(xiàn)常見(jiàn)的堆棧操作色迂,例如入棧和出棧。
以下是如何使用此類(lèi):
>>> from stack_v1 import Stack
>>> stack = Stack()
>>> stack.push(1)
>>> stack.push(2)
>>> stack.push(3)
>>> stack.pop()
3
>>> stack._items
[1, 2]
你的類(lèi)的基本功能已經(jīng)實(shí)現(xiàn)了手销。但是歇僧,盡管 _items
屬性被設(shè)計(jì)為非公開(kāi)的,你依然可以通過(guò)點(diǎn)表示法來(lái)訪問(wèn)它的值锋拖,就像訪問(wèn)普通屬性一樣馏慨。這種做法使得數(shù)據(jù)封裝變得困難,無(wú)法有效保護(hù)數(shù)據(jù)免受直接訪問(wèn)姑隅。
再次強(qiáng)調(diào)写隶,閉包提供了一種實(shí)現(xiàn)更嚴(yán)格數(shù)據(jù)封裝的方法。請(qǐng)看以下代碼示例:
def Stack():
_items = []
def push(item):
_items.append(item)
def pop():
return _items.pop()
def closure():
pass
closure.push = push
closure.pop = pop
return closure
在這個(gè)示例中讲仰,你通過(guò)編寫(xiě)一個(gè)函數(shù)來(lái)創(chuàng)建一個(gè)閉包對(duì)象慕趴,而不是定義一個(gè)類(lèi)。在這個(gè)函數(shù)內(nèi)部,你定義了一個(gè)局部變量 _items
冕房,它將是你閉包對(duì)象的一部分躏啰。你將使用這個(gè)變量來(lái)保存棧的數(shù)據(jù)。接著耙册,你定義了兩個(gè)內(nèi)部函數(shù)來(lái)執(zhí)行棧的操作给僵。
closure()
內(nèi)部函數(shù)作為閉包的載體。在這個(gè)函數(shù)的基礎(chǔ)上详拙,你添加了 push()
和 pop()
函數(shù)帝际。最終,你返回了最終的閉包對(duì)象饶辙。
你可以像使用 Stack
類(lèi)一樣使用 Stack()
函數(shù)蹲诀。一個(gè)重要的不同點(diǎn)是,現(xiàn)在你無(wú)法訪問(wèn) _items
屬性:
>>> from stack_v2 import Stack
>>> stack = Stack()
>>> stack.push(1)
>>> stack.push(2)
>>> stack.push(3)
>>> stack.pop()
3
>>> stack._items
Traceback (most recent call last):
...
AttributeError: 'function' object has no attribute '_items'
Stack()
函數(shù)使你能夠創(chuàng)建閉包弃揽,這些閉包的功能類(lèi)似于 Stack
類(lèi)的實(shí)例脯爪。但是,你無(wú)法直接訪問(wèn) _items
屬性矿微,這增強(qiáng)了數(shù)據(jù)的封裝性痕慢。
如果你非常講究,可以使用一種高級(jí)技巧來(lái)訪問(wèn) _items
屬性的內(nèi)容:
>>> stack.push.__closure__[0].cell_contents
[1, 2]
.__closure__
屬性會(huì)返回一個(gè)元組涌矢,其中包含了閉包中變量綁定的單元格守屉。每個(gè)單元格對(duì)象都有一個(gè)名為 cell_contents
的屬性,你可以通過(guò)它來(lái)獲取單元格中的值蒿辙。
即便有這種技巧可以訪問(wèn)閉包中的變量,但在 Python 代碼中通常不會(huì)使用它滨巴。畢竟思灌,如果你的目標(biāo)是實(shí)現(xiàn)封裝,為什么要去破壞它呢恭取?
探索閉包的替代方案
到目前為止泰偿,你已經(jīng)了解到 Python 閉包可以幫助解決一些問(wèn)題。然而蜈垮,理解閉包的內(nèi)部工作原理可能比較困難耗跛,因此使用其他工具可能會(huì)讓你的代碼更容易理解。
你可以用一個(gè)實(shí)現(xiàn)了 .__call__()
特殊方法的類(lèi)來(lái)替代閉包攒发,這樣的類(lèi)可以創(chuàng)建出可調(diào)用的實(shí)例调塌。所謂可調(diào)用實(shí)例,就是你可以像調(diào)用函數(shù)一樣去調(diào)用的對(duì)象惠猿。
以 make_root_calculator()
工廠函數(shù)為例:
>>> def make_root_calculator(root_degree, precision=2):
... def root_calculator(number):
... return round(pow(number, 1 / root_degree), precision)
... return root_calculator
...
>>> square_root = make_root_calculator(2, 4)
>>> square_root(42)
6.4807
>>> cubic_root = make_root_calculator(3)
>>> cubic_root(42)
3.48
該函數(shù)返回在其擴(kuò)展范圍內(nèi)保留 root_ Degree 和 precision 參數(shù)的閉包羔砾。您可以用以下類(lèi)替換該工廠函數(shù):
class RootCalculator:
def __init__(self, root_degree, precision=2):
self.root_degree = root_degree
self.precision = precision
def __call__(self, number):
return round(pow(number, 1 / self.root_degree), self.precision)
這個(gè)類(lèi)接收與 make_root_calculator()
相同的兩個(gè)參數(shù),并將它們?cè)O(shè)置為實(shí)例屬性。
通過(guò)實(shí)現(xiàn) .__call__()
方法姜凄,你將你的類(lèi)實(shí)例轉(zhuǎn)變?yōu)榭烧{(diào)用的對(duì)象政溃,這意味著你可以像調(diào)用普通函數(shù)一樣調(diào)用這些實(shí)例。以下展示了如何利用這個(gè)類(lèi)來(lái)創(chuàng)建類(lèi)似于根計(jì)算函數(shù)的對(duì)象:
>>> from roots import RootCalculator
>>> square_root = RootCalculator(2, 4)
>>> square_root(42)
6.4807
>>> cubic_root = RootCalculator(3)
>>> cubic_root(42)
3.48
>>> cubic_root.root_degree
3
如你所看到的态秧,RootCalculator
類(lèi)的功能與 make_root_calculator()
函數(shù)大致相同董虱。此外,你現(xiàn)在還能夠訪問(wèn)如 root_degree
這樣的配置參數(shù)申鱼。
總結(jié)
現(xiàn)在你已經(jīng)了解到愤诱,閉包通常是在 Python 中定義在另一個(gè)函數(shù)內(nèi)部的函數(shù)對(duì)象。閉包會(huì)捕獲它們封閉作用域內(nèi)定義的對(duì)象润讥,并將這些對(duì)象與內(nèi)部函數(shù)對(duì)象結(jié)合起來(lái)转锈,形成一個(gè)具有擴(kuò)展作用域的可調(diào)用對(duì)象。
你可以在多種情況下使用閉包楚殿,尤其是當(dāng)你需要在連續(xù)函數(shù)調(diào)用間保持狀態(tài)或編寫(xiě)裝飾器時(shí)撮慨。因此,掌握如何使用閉包對(duì) Python 開(kāi)發(fā)者來(lái)說(shuō)是一項(xiàng)寶貴的技能脆粥。
在本教程中砌溺,你學(xué)習(xí)了:
- 閉包是什么以及它們?cè)?Python 中的工作原理
- 實(shí)際中何時(shí)可以運(yùn)用閉包
- 可調(diào)用實(shí)例如何替代閉包 掌握了這些知識(shí)后,你可以開(kāi)始在你的代碼中創(chuàng)建和使用 Python 閉包变隔,特別是如果你對(duì)函數(shù)式編程工具感興趣的話规伐。
本文由mdnice多平臺(tái)發(fā)布