最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

【翻譯】Python的.__call__()方法:創(chuàng)建可調用實例

2023-08-01 17:51 作者:月愿LW  | 我要投稿

原文鏈接:https://realpython.com/python-callable-instances/

by Leodanis Pozo Ramos 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的 object-oriented (面向對象)基本知識,包括如何定義和使用 classes 及其方法。如果對Python里的 decorators (裝飾器)以及 strategy design pattern (策略設計模式)有了解就更好了。你也應該理解state(狀態(tài))的概念。


理解Python中的可調用對象

Python中的一個 callable (可調用對象)是任何你能用一對圓括號和一系列可選參數(shù)調用的對象。在你和Python的日常交互中,就會發(fā)現(xiàn)不同的可調用對象的例子。這里羅列了一些:

  • Built-in(內置)函數(shù)和類

  • 你使用 def 關鍵字創(chuàng)建的用戶自定義 functions (函數(shù))

  • 你使用 lambda 關鍵字創(chuàng)建的匿名函數(shù)

  • 你的自定義 classes 里的 constructors (構造器)

  • Instance, classstatic 方法

  • 實現(xiàn)了 .__call__() 方法的類的實例

  • 你的函數(shù)返回的Closures(閉包)

  • 你使用 yield 關鍵字定義的 Generator 函數(shù)

  • 你使用 async 關鍵字創(chuàng)建的 Asynchronous(異步)函數(shù)和方法

所有這些不同的可調用對象都有共同點。他們實現(xiàn)了.__call__()特殊方法。為了驗證這一點,你可以使用內置的 dir() 函數(shù),這個函數(shù)接收一個對象作為參數(shù),以列表形式 returns 對象的屬性和方法:

在前兩個例子中,你將內置函數(shù) abs()all() 作為參數(shù)傳給dir()并調用,可以看到.__call__()方法都呈現(xiàn)在了輸出里。

在最后一個例子里,你定義了一個 prints 信息到屏幕上的函數(shù)。這個函數(shù)也有.__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__屬性來確認。

SampleClassclass constructor 回退到使用type.__call__()。這就是為什么你能調用SampleClass()來得到一個新實例。所以,類構造器是返回新的底層類實例的可調用對象。

在上面的例子中,你可以看出方法對象,例如sample_instance.method,也有一個把它們變成可調用對象的.__call__()特殊方法。重點就是,一個對象得有.__call__()方法才能成為可調用對象。


檢查一個對象是否可調用

如果你需要檢查一個Python對象是否可調用,那么你可以使用內置的 callable() 函數(shù),就像下面這樣:

callable()函數(shù)將一個對象作為參數(shù),如果這個對象可調用,就返回True。否則返回False。

注意:由于dir()會檢查給定對象是否有.__call__()方法,你可以使用它來檢查。雖然dir()方法在你測試代碼和案例時很有用,但在你需要快速檢查一個對象是否可調用時沒有太大幫助。相反,callable()函數(shù)是你能直接在 Boolean context (布爾上下文)中使用的 predicate (謂詞)函數(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作為第一個參數(shù),然后接收任意數(shù)量的額外參數(shù)。

這是一個具有.__call__()方法的類的實例如何運作的案例:

在這個Counter類里,你有一個.count instance attribute (實例/對象屬性)來追蹤當前計數(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ā) side effects (附加作用)。

繼續(xù)看第二個例子,考慮下面這個類,它允許你創(chuàng)建可調用對象來計算不同的冪:

在這個例子中,你的 PowerFactory 類接收一個 exponent 參數(shù),你之后將用這個參數(shù)來進行不同的冪運算。這個.__call__()方法接收一個base參數(shù)然后用之前提供的指數(shù)計算它的冪。最后,這個方法返回計算后的結果。

你的類是這樣運作的:

在這里,你使用 PowerFactory 來創(chuàng)建兩個不同的可調用實例。第一個實例計算數(shù)的平方,第二個實例計算三次方。

在這個例子中,你需要傳入在調用 square_ofcube_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__()方法是 instance initializer 。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),別的則 inherit (繼承自)父類。在許多情況下,object 類提供了這個方法:

記住 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ù)次調用之間保存了 state (狀態(tài))的可調用對象,一般稱之為stateful的可調用對象。例如,你想寫一個可調用對象,它可以從數(shù)據(jù)流中接收連續(xù)不斷的數(shù)值然后計算它們的累積平均值。在數(shù)次調用之間,這個可調用對象必須持續(xù)關注之前傳入的值。

為了解決這個問題,你可以用一個像這樣的 closure (閉包):

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中的閉包和作用域,看這里: Exploring Scopes and Closures in Python 。

這是你用帶有 .__call__() 方法的類解決上面問題的例子:

在這個例子中,你的類用一個 .data 對象屬性來保存數(shù)據(jù)。 .__call__() 方法在每次調用時接收一個新的值,然后追加到 .data 里,最后計算并返回平均值。

在這個案例里,你的代碼可讀性很強。 .data 屬性保存了多次調用的狀態(tài), .__call__() 方法計算了累積平均值??纯催@個類實際如何起作用的:

CumulativeAverager 的實例是可調用對象,在每次調用時,都保存了之前所有值并計算累積平均值。這種方式讓你的代碼更容易理解。要寫出這個類,你不需要理解Python閉包復雜的工作原理。

另一個讓人感興趣的優(yōu)點是,你可以通過 .data 屬性直接訪問到當前數(shù)據(jù)。


緩存計算過的值

另一個可調用對象的常見用例是需要一個含狀態(tài)信息的可調用對象以便能 caches (緩存)多次調用間計算過的值。當你需要優(yōu)化算法時這很有用。

比如說,你想計算給定數(shù)值的階乘,由于你打算多次運行這個計算,所以得講究效率。一種方式是把算過的值緩存下來,這樣你就不用總是算它們了。

這里有一個使用 .__call__() 實現(xiàn)了這種效果的類:

在這個類里,你使用一個 dictionary 來緩存已經(jīng)算過的階乘值。這個字典的鍵是已經(jīng)傳入過的數(shù),值是已經(jīng)計算過的階乘。

.__call__() 方法檢查了當前輸入是否在 .cache 字典里。如果是的話,就返回相關的值,不需要再計算。這就優(yōu)化了你的算法,讓它更快。

如果當前輸入的數(shù)沒有在 .cache 字典里,那這個方法就遞歸地計算階乘,緩存結果,返回最終的值。

看看這個類如何起作用:

每次調用 Factorial 的實例都會檢查緩存看看已經(jīng)計算過的值。實例只會計算還沒算過的數(shù)的階乘。注意最終所有的輸入值和階乘都會存在 .cache 字典里。


創(chuàng)建清晰便捷的APIs

編寫能產生可調用實例的類還讓你能在庫、 packages, and modules(包、模組)中設計出方便、用戶友好型的 application programming interfaces (APIs) 。

比如說,你在編寫一個新的、很炫酷的庫,用來創(chuàng)建 GUI applications (圖形化用戶接口應用程序)。你的庫有一個 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)建 class-based decorators (基于類的裝飾器)。在這種情況下,只有用.__call__() 方法,因為它能確保對象可調用。

另一個 .__call__() 的有趣用法是當你想在Python里實現(xiàn) strategy design pattern (策略設計模式)。這時,用.__call__() 來創(chuàng)建實現(xiàn)了不同策略的類就大有好處。

在接下來的章節(jié)里,你會學習如何使用 .__call__() 來創(chuàng)建基于類的裝飾器,以及在Python里實現(xiàn)策略模式。


編寫基于類的裝飾器

Python的 decorators (裝飾器)是可調用對象,把其他的可調用對象作為參數(shù),并不顯式地修改代碼,卻拓展了它們的行為。裝飾器提供了一個很好的工具來給現(xiàn)有的可調用對象增加功能。

看到別人寫、或是自己編寫基于函數(shù)的裝飾器都很常見。然而,通過充分利用 .__call__() 特殊方法,你也可以編寫基于類的裝飾器

為了說明如何做到這一點,就比如說你想創(chuàng)建一個測量自定義函數(shù)執(zhí)行時間的裝飾器。下面這段代碼展示了該怎么基于類編寫裝飾器:

ExecutionTimer 類在初始化時,接收一個函數(shù)對象作為參數(shù)。.__call__() 方法作用于這個函數(shù)對象。在這個例子中,.__call__() 使用*args and **kwargs 泛型參數(shù)來處理任何輸入的那個函數(shù)需要的參數(shù)。

接下來,你用 time.perf_counter() 來獲取輸入的函數(shù)運行前后的時間。然后你打印出函數(shù)名和毫秒級執(zhí)行時間。最后一步是返回輸入的函數(shù)調用后的結果。

注意:如果想更深入地了解Python中給代碼計時的最佳實踐,看這個 Python Timer Functions: Three Ways to Monitor Your Code。

有了這個類,你就可以立馬開始測量你的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 loop (for循環(huán))來多次運行輸入函數(shù)然后計算總的執(zhí)行時間。

接下來,你計算平均執(zhí)行時間并像往常一樣打印信息。最后,你返回輸入函數(shù)的運算結果。注意 .__call__() 返回了 timer 代表的那個函數(shù)。

有了這些改動,繼續(xù)試試 ExecutionTimer 。注意要訪問新版的 ExecutionTimer ,你需要先 reload (重載)timing.py 文件或者重啟你當前的 interactive session

你的裝飾器現(xiàn)在讓你可以運行目標函數(shù)特定次,然后計算平均運行時間,太好啦!


實現(xiàn)策略設計模式

策略設計模式允許你定義一系列類似的算法然后運行時可交替。換句話說,這個模式為特定種類的問題提供了不同的解決方案,每個解決方案綁定在一個特定對象里。然后,你可以動態(tài)地選擇合適的解決方案。

注意:策略設計模式對那些函數(shù)不是 first-class citizens (一等公民)的語言來說也很有用。比如 C++Java,使用這種模式讓你可以把函數(shù)作為參數(shù)傳給其他函數(shù)。

舉一個用 .__call__() 實現(xiàn)策略模式的例子,比如說你需要 serialize (序列化)一些數(shù)據(jù),轉成JSON 或者 YAML,取決于特定場景。這樣,你就可以用策略模式。你用一個類把序列化數(shù)據(jù)轉成JSON,另一個類把序列化數(shù)據(jù)轉成YAML。

在接下來這個例子中,你會編寫一個針對這個問題的解決方案。注意為了讓這個例子運行起來,你得先 pip install pyyaml 因為Python標準庫不支持任何處理YAML數(shù)據(jù)的工具。這是一塊 missing battery 。

注意:為了安裝pyyaml,你需要先創(chuàng)建一個Python ?virtual environment (虛擬環(huán)境),如果你還沒有的話。這樣,就可以避免在系統(tǒng)Python里擠滿一堆平常用不上的包。

這是代碼:

在這個例子中,你有 JsonSerializerYamlSerializer 類,代表了你的序列化策略。它們的 .__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)多種使用可調用實例解決真實問題的案例

有了這些知識,你就能在Python代碼里設計、實現(xiàn)可調用對象。你可以解決多種常見問題,例如保存調用狀態(tài)、緩存數(shù)據(jù)、編寫基于類的裝飾器等等。


翻譯說明:

  1. 翻譯原稿為markdown文檔,超鏈接復制到B站后會失效。

  2. 專有詞匯等一般不予翻譯,除非特別影響理解。


    【翻譯】Python的.__call__()方法:創(chuàng)建可調用實例的評論 (共 條)

    分享到微博請遵守國家法律
    东海县| 酒泉市| 屏山县| 庆城县| 南江县| 杂多县| 清水河县| 库车县| 聂拉木县| 湘阴县| 大关县| 桓台县| 乌拉特后旗| 黄浦区| 涡阳县| 桂平市| 新建县| 鹤峰县| 长宁区| 邵阳市| 乌兰察布市| 油尖旺区| 星子县| 辉县市| 喀喇| 西贡区| 马尔康县| 东港市| 宁陕县| 普兰店市| 昌江| 景泰县| 阳信县| 安陆市| 历史| 红安县| 车险| 和龙市| 东丰县| 綦江县| 广安市|