【翻譯】Python的.__call__()方法:創(chuàng)建可調用實例
原文鏈接:https://realpython.com/python-callable-instances/
by
May 24, 2023
目錄:
理解Python中的可調用對象
檢查一個對象是否可調用
在Python里使用
.__call__()
創(chuàng)建可調用實例理解它們的不同:
.__init__()
vs.__call__()
將Python的
.__call__()
付諸實踐
編寫含狀態(tài)信息的可調用對象
緩存計算過的值
創(chuàng)建清晰便捷的APIs
探索
.__call__
的高級使用案例
編寫基于類的裝飾器
實現(xiàn)策略設計模式( strategy design pattern)
結語
在Python里,一個可調用對象是能使用一對圓括號和一系列可選參數(shù)調用的對象。函數(shù)、類和方法都是Python里可調用對象的常見例子。除了這些,你還可以創(chuàng)建自定義的產生可調用實例的類。為了做到這一點,你得把.__call__()
特殊方法加到你的類里。
含有.__call__()
方法的類的實例就跟函數(shù)類似,讓你能靈活便捷地給對象添加功能。作為一個Python開發(fā)者,理解如何創(chuàng)建并使用可調用對象是一項很有價值的技能。
在這個教程中,你將:
理解Python里可調用對象的概念
通過在你的類里添加
.__call__()
方法創(chuàng)建可調用實例理解
.__init__()
和.__call__()
的區(qū)別編寫一些使用可調用實例解決真實案例的代碼
為了最大程度地吸收本教程,你需要熟悉Python的
(面向對象)基本知識,包括如何定義和使用 及其方法。如果對Python里的 (裝飾器)以及 (策略設計模式)有了解就更好了。你也應該理解 (狀態(tài))的概念。理解Python中的可調用對象
Python中的一個
(可調用對象)是任何你能用一對圓括號和一系列可選參數(shù)調用的對象。在你和Python的日常交互中,就會發(fā)現(xiàn)不同的可調用對象的例子。這里羅列了一些:(內置)函數(shù)和類
你使用
關鍵字創(chuàng)建的用戶自定義 (函數(shù))你使用
關鍵字創(chuàng)建的匿名函數(shù)你的自定義
里的 (構造器), 和 方法
實現(xiàn)了
方法的類的實例你的函數(shù)返回的
(閉包)你使用
關鍵字定義的 函數(shù)你使用
關鍵字創(chuàng)建的 (異步)函數(shù)和方法
所有這些不同的可調用對象都有共同點。他們實現(xiàn)了.__call__()
特殊方法。為了驗證這一點,你可以使用內置的 函數(shù),這個函數(shù)接收一個對象作為參數(shù),以列表形式 對象的屬性和方法:
在前兩個例子中,你將內置函數(shù) dir()
并調用,可以看到.__call__()
方法都呈現(xiàn)在了輸出里。
在最后一個例子里,你定義了一個 .__call__()
。注意你可以用這個方法調用函數(shù):
注意在上面這個例子里用.__call__()
就跟直接用greet()
調用的效果是一樣的。
注意:雖然你可以直接調用
.__call__()
這樣的特殊方法,但不建議真這么干。相反,像你平常那樣調用函數(shù)就行。
現(xiàn)在,這些內部是怎么運作的呢?當你運行了類似這樣的東西callable_object(*args, **kwargs)
,Python內部把運算符翻譯成了callable_object.__call__(*args, **kwargs)
。傳給常規(guī)函數(shù)的參數(shù)就是用在.__call__()
里的參數(shù)。換句話說,不論何時你調用一個可調用對象,Python都會在幕后,用你傳進來的參數(shù)自動運行它的.__call__()
方法。
現(xiàn)在看下面這個自定義類:
在Python里,所有東西都是對象。像SampleClass
這樣的類是type
的對象,你可以通過將這個類的對象作為參數(shù)傳給type()
并調用,或是查看它的.__class__
屬性來確認。
SampleClass
的 回退到使用type.__call__()
。這就是為什么你能調用SampleClass()
來得到一個新實例。所以,類構造器是返回新的底層類實例的可調用對象。
在上面的例子中,你可以看出方法對象,例如sample_instance.method
,也有一個把它們變成可調用對象的.__call__()
特殊方法。重點就是,一個對象得有.__call__()
方法才能成為可調用對象。
檢查一個對象是否可調用
如果你需要檢查一個Python對象是否可調用,那么你可以使用內置的
函數(shù),就像下面這樣:
callable()
函數(shù)將一個對象作為參數(shù),如果這個對象可調用,就返回True
。否則返回False
。
注意:由于
dir()
會檢查給定對象是否有.__call__()
方法,你可以使用它來檢查。雖然dir()
方法在你測試代碼和案例時很有用,但在你需要快速檢查一個對象是否可調用時沒有太大幫助。相反,callable()
函數(shù)是你能直接在 (布爾上下文)中使用的 (謂詞)函數(shù)。
在上面這些例子中,sample_instance
以外的所有被測試的對象都可調用。這是可想而知的,因為SampleClass
并沒有為它的實例實現(xiàn).__call__()
方法。是的,你猜對了!通過編寫一個.__call__()
方法,就可以使自定義類的實例變成可調用的。在后續(xù)章節(jié),你會學到把類的實例變成可調用對象的基礎知識。
但是首先,一定要注意到有時callable()
會導致假陽性(即把不可調用當成可調用):
在這個例子中,callable()
返回True
。然而,自定義類的實例并不可調用,如果你試圖調用就會報錯。所以,callable()
只能保證實例所屬的類實現(xiàn)了.__call__()
方法。
在Python里使用.__call__()
創(chuàng)建可調用實例
如果你希望給定類的實例可調用,就需要在底層類實現(xiàn).__call__()
特殊方法。這個方法使你可以像調用Python里的常規(guī)函數(shù)一樣調用你的類的實例。
不像別的特殊方法,.__call__()
沒有對傳入?yún)?shù)的硬性要求。在某種意義上,它就和任何別的方法一樣,接收self
這是一個具有.__call__()
方法的類的實例如何運作的案例:
在這個Counter
類里,你有一個.count
(實例/對象屬性)來追蹤當前計數(shù)。然后你有一個.increment()
方法在每次調用時給計數(shù)加1
。最后,你加了一個.__call__()
方法。在這個例子中,.__call__()
回退到調用.increment()
,是一種執(zhí)行增長操作的快捷手段。
看看這個類實際怎么運作的:
在創(chuàng)建了Counter
的一個實例后,你調用.increment()
。這次調用把.count
屬性增加了1,你可以通過訪問屬性來確認。在后續(xù)的例子里,由于你的類里有一個.__call__()
方法,你方便地調用實例來直接增加計數(shù)。
在這個例子中,.__call__()
給計數(shù)增長提供了一個快捷手段。這種特性賦予你的類一個方便的、用戶友好型的接口。
上面這個例子中的.__call__()
方法沒有接收任何參數(shù)。這個方法也不會顯式地返回任何值。然而,在自定義類里如何編寫.__call__()
方法并沒有嚴格要求。所以,你可以讓它們接收參數(shù)、返回值,甚至像你的Counter
例子一樣引發(fā) (附加作用)。
繼續(xù)看第二個例子,考慮下面這個類,它允許你創(chuàng)建可調用對象來計算不同的冪:
在這個例子中,你的 PowerFactory
類接收一個 exponent
參數(shù),你之后將用這個參數(shù)來進行不同的冪運算。這個.__call__()
方法接收一個base
參數(shù)然后用之前提供的指數(shù)計算它的冪。最后,這個方法返回計算后的結果。
你的類是這樣運作的:
在這里,你使用 PowerFactory
來創(chuàng)建兩個不同的可調用實例。第一個實例計算數(shù)的平方,第二個實例計算三次方。
在這個例子中,你需要傳入在調用 square_of
或 cube_of
時傳入一個 base
參數(shù),因為這些調用實際上是回退到調用 .__call__()
。最后,注意從每次調用中你都獲得了冪運算的結果,這是因為 .__call__()
返回了指定冪的計算結果。
在自定義類中定義一個 .__call__()
方法讓你能把這些類的實例當普通Python函數(shù)使用。這個特性在一些情況下很方便,你將在后續(xù)的將Python的.__call__()
付諸實踐章節(jié)學到。
在進入可調用實例的一般使用案例前,你將探索.__init__()
和.__call__()
方法的不同。這兩個方法以及他們在Python類中互相關聯(lián)的角色特點會讓很多正開始學習Python的人感到困惑。
理解它們的不同:.__init__()
vs .__call__()
在Python類中區(qū)分.__init__()
和.__call__
兩者的角色對剛開始學習編程語言或其面向對象特點的開發(fā)者來說是一項很容易混淆的任務。
然而,這兩個方法還是挺不一樣的,每個都有特定的目的。
.__init__()
方法是 。Python在你調用類的構造器創(chuàng)建一個類的實例時自動調用這個方法。.__init__()
的參數(shù)和構造這個類時用的參數(shù)一樣,這些參數(shù)為類的屬性提供初始值。
同時,.__call__()
方法將實例變成可調用對象。正如你已經(jīng)學到的,Python會在你調用一個具體的類的實例時自動調用該方法。
為了闡明這兩個方法的不同,考慮下面這個案例類:
Demo
類實現(xiàn)了.__init__()
和.__call__()
。在.__init__()
中,你打印一段信息并初始化.attr
屬性。在.__call__()
中,你僅僅打印一段信息,好在這個方法以給定參數(shù)被調用時能知曉。
這個類是這樣起作用的:
正如你看到的,在你的類里,每個方法扮演的角色是不同的。.__init__()
方法在你創(chuàng)建類的實例時被調用,它的主要目的是用合理的初始值初始化實例的屬性。
你在所有的Python類里都能找到.__init__()
方法。一些類有顯式的實現(xiàn),別的則 (繼承自)父類。在許多情況下, 類提供了這個方法:
記住 object
類是所有Python類的父類。所以,即使你不在自定義類里顯式地定義.__init__()
方法,這個類也會繼承 object
類里的默認實現(xiàn)。
相反,.__call__()
方法在調用其所屬類的具體實例時會執(zhí)行,就像這個例子中的demo
。.__call__()
的目的是把你的實例變成可調用對象。換句話說,它的目的是創(chuàng)建出像普通函數(shù)一樣供你調用的對象。大多數(shù)Python類都沒實現(xiàn)這個方法。只有當你需要把自定義類的實例當函數(shù)用時才需要去實現(xiàn)它。
好了!在澄清了.__call__()
和.__init__()
的不同點后,你已經(jīng)準備好繼續(xù)探索在Python代碼中使用.__call__()
帶來的益處了。
將Python的.__call__()
付諸實踐
在一些情況下,寫一個能產生可調用實例的類是很有用的。比如說,在以下這些情景你會受益:
在調用之間保留狀態(tài)
緩存之前計算的結果
實現(xiàn)直截了當、便捷的API
雖然你可以以更普通的方式,用函數(shù)或類解決上面的問題,使用可調用對象在某些情境下也是個不錯的選擇。當你已經(jīng)有了一個類并且需要它具有函數(shù)式的行為時尤其如此。
在接下來的章節(jié)里,你將編寫一些闡明上述每個使用情景的實際案例。
編寫含狀態(tài)信息的可調用對象
有時,你可能希望寫出在數(shù)次調用之間保存了 stateful的可調用對象。例如,你想寫一個可調用對象,它可以從數(shù)據(jù)流中接收連續(xù)不斷的數(shù)值然后計算它們的累積平均值。在數(shù)次調用之間,這個可調用對象必須持續(xù)關注之前傳入的值。
(狀態(tài))的可調用對象,一般稱之為為了解決這個問題,你可以用一個像這樣的
(閉包):
在 cumulative_average()
中,你使用了 data
這個局部變量來儲存數(shù)次調用的數(shù)據(jù)。然后你定義了一個叫 average()
的內層函數(shù)。這個函數(shù)在每次調用時接收一個新值并追加到 data
里。然后這個函數(shù)計算并返回當前儲存數(shù)據(jù)的平均值。
最后, cumulative_average()
返回內層函數(shù)。實際上,返回了一個閉包,這是一個將 average()
函數(shù)和它的非局部作用域打包在一起的特殊對象。在這個案例中,閉包中包含了 data
變量。
你編寫完 cumulative_average()
之后,就可以創(chuàng)建像 stream_average
這樣的自定義閉包了。這個對象可調用,所以你可以把它當函數(shù)用,來計算數(shù)據(jù)流的累積平均值,正如你在上面的最終實例中做的那樣。
即使閉包允許你保存多次調用之間的狀態(tài),這個工具可能顯得較難理解、編寫。在這種情境下,寫一個具有 .__call__()
方法的類對你的任務更加有益,也會使你的代碼可讀性更強、更清晰。
注意:要深入了解Python中的閉包和作用域,看這里: 。
這是你用帶有 .__call__()
方法的類解決上面問題的例子:
在這個例子中,你的類用一個 .data
對象屬性來保存數(shù)據(jù)。 .__call__()
方法在每次調用時接收一個新的值,然后追加到 .data
里,最后計算并返回平均值。
在這個案例里,你的代碼可讀性很強。 .data
屬性保存了多次調用的狀態(tài), .__call__()
方法計算了累積平均值??纯催@個類實際如何起作用的:
CumulativeAverager
的實例是可調用對象,在每次調用時,都保存了之前所有值并計算累積平均值。這種方式讓你的代碼更容易理解。要寫出這個類,你不需要理解Python閉包復雜的工作原理。
另一個讓人感興趣的優(yōu)點是,你可以通過 .data
屬性直接訪問到當前數(shù)據(jù)。
緩存計算過的值
另一個可調用對象的常見用例是需要一個含狀態(tài)信息的可調用對象以便能
(緩存)多次調用間計算過的值。當你需要優(yōu)化算法時這很有用。比如說,你想計算給定數(shù)值的階乘,由于你打算多次運行這個計算,所以得講究效率。一種方式是把算過的值緩存下來,這樣你就不用總是算它們了。
這里有一個使用 .__call__()
實現(xiàn)了這種效果的類:
在這個類里,你使用一個
來緩存已經(jīng)算過的階乘值。這個字典的鍵是已經(jīng)傳入過的數(shù),值是已經(jīng)計算過的階乘。.__call__()
方法檢查了當前輸入是否在 .cache
字典里。如果是的話,就返回相關的值,不需要再計算。這就優(yōu)化了你的算法,讓它更快。
如果當前輸入的數(shù)沒有在 .cache
字典里,那這個方法就遞歸地計算階乘,緩存結果,返回最終的值。
看看這個類如何起作用:
每次調用 Factorial
的實例都會檢查緩存看看已經(jīng)計算過的值。實例只會計算還沒算過的數(shù)的階乘。注意最終所有的輸入值和階乘都會存在 .cache
字典里。
創(chuàng)建清晰便捷的APIs
編寫能產生可調用實例的類還讓你能在庫、
(包、模組)中設計出方便、用戶友好型的 。比如說,你在編寫一個新的、很炫酷的庫,用來創(chuàng)建 MainWindow
類提供了創(chuàng)建GUI程序主窗口的所有功能。
這個類將有一些方法,包括一個在屏幕上繪制出窗口的 .show()
方法。在這種情況下,你可以像這樣提供一個 .__call__()
方法:
在這個例子中,.__call__()
方法回退到調用 .show()
方法。這種實現(xiàn)讓你在調用 .show()
或實例本身時都能展示出主窗口:
在這個例子中,.__call__()
提供了一個快捷方式來在屏幕上展示程序窗口。這能提升你的用戶的使用體驗。所以,這種技巧非常有助于在Python項目中創(chuàng)建用戶友好型和易懂的接口。
另外一個能提升你API的案例是,你有個類的首要目的是提供單一的功能或行為。比如說,你想要一個 Logger
類往文件里記錄信息:
在這個例子中,Logger
的主要目的就是往你提供的日志文件里寫入信息。通過實現(xiàn) .__call__()
方法,只要像函數(shù)一樣調用對象,就能快捷地訪問這個功能。
探索.__call__
的高級使用案例
至今,你已經(jīng)學到了很多在類里使用 .__call__()
方法創(chuàng)建可調用實例的知識。在Python里,這個方法也有一些高級用法。其中之一是創(chuàng)建 (基于類的裝飾器)。在這種情況下,只有用.__call__()
方法,因為它能確保對象可調用。
另一個 .__call__()
的有趣用法是當你想在Python里實現(xiàn) (策略設計模式)。這時,用.__call__()
來創(chuàng)建實現(xiàn)了不同策略的類就大有好處。
在接下來的章節(jié)里,你會學習如何使用 .__call__()
來創(chuàng)建基于類的裝飾器,以及在Python里實現(xiàn)策略模式。
編寫基于類的裝飾器
Python的
(裝飾器)是可調用對象,把其他的可調用對象作為參數(shù),并不顯式地修改代碼,卻拓展了它們的行為。裝飾器提供了一個很好的工具來給現(xiàn)有的可調用對象增加功能。看到別人寫、或是自己編寫基于函數(shù)的裝飾器都很常見。然而,通過充分利用 .__call__()
特殊方法,你也可以編寫基于類的裝飾器。
為了說明如何做到這一點,就比如說你想創(chuàng)建一個測量自定義函數(shù)執(zhí)行時間的裝飾器。下面這段代碼展示了該怎么基于類編寫裝飾器:
ExecutionTimer
類在初始化時,接收一個函數(shù)對象作為參數(shù)。.__call__()
方法作用于這個函數(shù)對象。在這個例子中,.__call__()
使用 泛型參數(shù)來處理任何輸入的那個函數(shù)需要的參數(shù)。
接下來,你用
來獲取輸入的函數(shù)運行前后的時間。然后你打印出函數(shù)名和毫秒級執(zhí)行時間。最后一步是返回輸入的函數(shù)調用后的結果。注意:如果想更深入地了解Python中給代碼計時的最佳實踐,看這個 。
有了這個類,你就可以立馬開始測量你的Python函數(shù)的執(zhí)行時間了:
在這個代碼段里,你有一個接收列表并返回平方后的列表的函數(shù)。你想測量這個函數(shù)的執(zhí)行時間,為此使用了 ExecutionTimer
裝飾器。
一旦這個函數(shù)被裝飾了,不論何時你運行它,都會收到一則含有函數(shù)名和毫秒級執(zhí)行時間的信息。然后你收到函數(shù)的返回值。
現(xiàn)在,比如說你想加一個 repetitions
到裝飾器里。這個參數(shù)讓你能反復運行輸入函數(shù),計算平均執(zhí)行時間:
這個升級版本的 ExecutionTimer
和最初的實現(xiàn)有很大不同。這個類的初始器接收一個你提供的 repetitions
參數(shù)作為裝飾器調用的一部分。
在 .__call__()
內部,你接收輸入函數(shù)作為參數(shù)。然后創(chuàng)建一個內層函數(shù)來執(zhí)行輸入函數(shù)。在內層函數(shù)里,你使用 (for循環(huán))來多次運行輸入函數(shù)然后計算總的執(zhí)行時間。
接下來,你計算平均執(zhí)行時間并像往常一樣打印信息。最后,你返回輸入函數(shù)的運算結果。注意 .__call__()
返回了 timer
代表的那個函數(shù)。
有了這些改動,繼續(xù)試試 ExecutionTimer
。注意要訪問新版的 ExecutionTimer
,你需要先 (重載)timing.py
文件或者重啟你當前的 :
你的裝飾器現(xiàn)在讓你可以運行目標函數(shù)特定次,然后計算平均運行時間,太好啦!
實現(xiàn)策略設計模式
策略設計模式允許你定義一系列類似的算法然后運行時可交替。換句話說,這個模式為特定種類的問題提供了不同的解決方案,每個解決方案綁定在一個特定對象里。然后,你可以動態(tài)地選擇合適的解決方案。
注意:策略設計模式對那些函數(shù)不是 (一等公民)的語言來說也很有用。比如 或 ,使用這種模式讓你可以把函數(shù)作為參數(shù)傳給其他函數(shù)。
舉一個用 .__call__()
實現(xiàn)策略模式的例子,比如說你需要 (序列化)一些數(shù)據(jù),轉成 或者 ,取決于特定場景。這樣,你就可以用策略模式。你用一個類把序列化數(shù)據(jù)轉成JSON,另一個類把序列化數(shù)據(jù)轉成YAML。
在接下來這個例子中,你會編寫一個針對這個問題的解決方案。注意為了讓這個例子運行起來,你得先
因為Python標準庫不支持任何處理YAML數(shù)據(jù)的工具。這是一塊 。注意:為了安裝
pyyaml
,你需要先創(chuàng)建一個Python ? (虛擬環(huán)境),如果你還沒有的話。這樣,就可以避免在系統(tǒng)Python里擠滿一堆平常用不上的包。
這是代碼:
在這個例子中,你有 JsonSerializer
和 YamlSerializer
類,代表了你的序列化策略。它們的 .__call__()
方法使用合適的工具來把輸入數(shù)據(jù)序列化為JSON或YAML,視情況而定。
然后你有 DataSerializer
類,提供了更高級的類。你將使用這個類來序列化你的數(shù)據(jù)。首先,你需要提供一個具體的序列化類的可調用實例:
在這段代碼里,你有一個包含了樣例數(shù)據(jù)的字典。為了處理這些數(shù)據(jù),你以 JsonSerializer
為參數(shù),創(chuàng)建了一個 DataSerializer
類的實例,在這之后,你的實例能把字典轉化成JSON。
在最后那個例子里,你改變了序列化策略然后用你的數(shù)據(jù)序列化對象來把數(shù)據(jù)轉成YAML編碼。你還能想出其他有用的數(shù)據(jù)序列化對象嗎?
結語
關于Python里的可調用實例,你學到了很多,尤其是如何在自定義類里借助 .__call__()
特殊方法創(chuàng)建它們?,F(xiàn)在你知道了如何創(chuàng)建這些類,它們產生的對象讓你能像普通函數(shù)一樣調用。這使你的面向對象編程更加靈活、功能性更強。
在本篇教程中,你學到了:
理解Python里可調用對象的概念
通過在你的類里添加
.__call__()
方法創(chuàng)建可調用實例理解
.__init__()
和.__call__()
的區(qū)別實現(xiàn)多種使用可調用實例解決真實問題的案例
翻譯說明:
翻譯原稿為markdown文檔,超鏈接復制到B站后會失效。
專有詞匯等一般不予翻譯,除非特別影響理解。