Python閉包|你應(yīng)該知道的常見(jiàn)用例(下)

引言

在 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ā)布

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市匣缘,隨后出現(xiàn)的幾起案子猖闪,更是在濱河造成了極大的恐慌,老刑警劉巖肌厨,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件培慌,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡柑爸,警方通過(guò)查閱死者的電腦和手機(jī)吵护,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)表鳍,“玉大人馅而,你說(shuō)我怎么就攤上這事∑┦ィ” “怎么了瓮恭?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)厘熟。 經(jīng)常有香客問(wèn)我偎血,道長(zhǎng)诸衔,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任颇玷,我火速辦了婚禮笨农,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘帖渠。我一直安慰自己谒亦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布空郊。 她就那樣靜靜地躺著份招,像睡著了一般。 火紅的嫁衣襯著肌膚如雪狞甚。 梳的紋絲不亂的頭發(fā)上锁摔,一...
    開(kāi)封第一講書(shū)人閱讀 51,115評(píng)論 1 296
  • 那天,我揣著相機(jī)與錄音哼审,去河邊找鬼谐腰。 笑死,一個(gè)胖子當(dāng)著我的面吹牛涩盾,可吹牛的內(nèi)容都是我干的十气。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼春霍,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼砸西!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起址儒,我...
    開(kāi)封第一講書(shū)人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤芹枷,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后莲趣,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體鸳慈,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年妖爷,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片理朋。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡絮识,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嗽上,到底是詐尸還是另有隱情次舌,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布兽愤,位于F島的核電站彼念,受9級(jí)特大地震影響挪圾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜逐沙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一哲思、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吩案,春花似錦棚赔、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至残揉,卻和暖如春胧后,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背抱环。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工壳快, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人江醇。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓濒憋,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親陶夜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子凛驮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容