在Python中,我們可以使用def定義函式,或者透過lambda建立匿名函式,還有哪些場合會建立函式呢?類別的方法?若單純在類別上定義方法,透過類別「名稱.方法」確實可取得函式,也就是function類別的實例。

不過,如果是@classmethod標註的方法,就不是了,透過「類別實例.方法」取得的綁定方法(bound method)也不是,事實上,它們都會是method類別的實例。

嗯?它們不是可以像函式一樣地呼叫嗎?是的!不過,這類在Python中具有函式呼叫行為的物件,其實是可呼叫(callable)物件,具體而言,就是實現了__call__方法的物件。

是函式還是類別?

Python中有許多長得像函式的物件,如果命名上又特意以小寫名稱開頭,使用上,就真的跟函式沒有兩樣了。

例如,我們在迭代物件時,最常使用的range、enumerate、zip等,並不是函式,只需透過type來查詢一下,就能夠知道這些其實都是類別。

我曾想過:Python中要建立類別實例,為什麼不需要使用new之類的關鍵字來進行嗎?當然,單純認為語法就是如此的規範,這也沒錯,不過,其實類別在行為上,本來就可以像函式進行呼叫,因為類別是type的實例,而type定義了__call__方法。

如果定義了Some類別,以Some(arg)建立實例的話,實際上,相當於Some.__call__(arg),由於類別是type的實例,type定義的__call__方法第一個參數,接受的就是類別Some,其中的流程預設會依序呼叫Some的__new__與__init__方法,這就是開發者可以藉由定義__new__與__init__方法,來實現類別實例建構與初始化的原因。

進一步地,Python實現meta程式設計的方式之一,是在使用class語法定義類別時指定metaclass,被指定的對象可以是個類別,這讓用來介入類別定義的metaclass可以獨立地設計__new__、__init__等方法,便於維護或重複使用,像是abc模組就提供了可重用的ABCMeta等類別。

事實上,metaclass能夠去指定的對象,只要是可呼叫物件就可以了,Python會呼叫它的__call__方法,傳入類別名稱、父類別資訊與屬性,在簡單的情況下,傳個函式也行,因為函式也是個可呼叫物件(或許比metaclass更貼切的名稱,應該是meta_callable)。

有狀態的函式或可呼叫物件?

看來可呼叫物件是個高級玩意兒,適用於meta程式設計之類的場合?不!可呼叫物件可應用的場合可多了,例如,當需求是比「有狀態的函式」再多一些的時候,所謂有狀態的函式,就是一個構成了閉包(closure)的函式,例如:

def add(n):
return lambda a: n + a

傳回的lambda捕捉了參數n,也就是包含了呼叫add時引數的狀態,有時候這類函式攜帶的狀態可變動,例如捕捉了外包函式的區域變數,每次呼叫時都會變動該變數,這種包含著內部可變動狀態的函式,也常用來模擬一些私有性,例如,在JavaScript中,可藉由傳回一個形成閉包的函式,來模擬物件的私有屬性。

當事情變得更為複雜之時,例如,也許需要能直接存取函式捕捉的狀態,像是透過add函式傳回的函式,來取得呼叫add函式時的n值,單純使用函式去完成這項任務並非做不到,只是有時實現上麻煩,不見得容易閱讀與理解;然而,若定義一個add類別,具有以下兩個方法,問題就可以解決了,不僅容易實現與理解,客戶端的呼叫方式也不用改變:

def __init__(self, n):
self.n = n
def __call__(self, a):
return self.n + a

這就是Python中許多看來像函式的東西,其實是以類別定義的原因之一:必須能像函式一樣地呼叫,需要攜帶狀態,而且可以存取狀態。

方才談到的綁定方法是個具體的例子,雖然不鼓勵,但確實可以透過綁定方法的__self__屬性來取得綁定的實例。

接受/傳回可呼叫物件

用可呼叫物件來攜帶狀態?看來還是個高階需求啊?不!可呼叫物件可應用的場合,根本就是平易近人。在函式為一級公民的語言中,函式可以作為值傳遞給變數、參數,或作為函式、方法的傳回值,因此,Python生態圈中無論是標準或第三方程式庫,才會有那麼多的API可以接受函式、傳回函式,或者接受函式並傳回函式。

然而,別忘了,Python是個動態定型語言,那些API在定義之際,或許只是要求一隻具有可呼叫行為的鴨子,傳回一隻具有可呼叫行為的鴨子,也就是採用鴨子定型罷了,有些看來是高階函式,其實,可以更廣義地視為接受、傳回可呼叫物件的高階可呼叫物件。

Python中有許多裝飾器(decorator)就是這類物件,例如@property、@classmethod;有些API確實可能真的會檢查傳入的是不是函式,functools模組有個 @wraps裝飾器,可以將可呼叫物件偽裝為函式,其實@wraps也是個高階可呼叫物件。

高階物件聽來並不平易近人?來讓需求更普遍化一些,看看sorted函式,如果想自訂排序,我們可以指定其搭配的key或cmp參數,它們接受函式嗎?其實只要可呼叫物件就可以了!

應用的場景之一是,若有個資料類別Point,具有x、y屬性代表座標,定義了dist方法,我們可以指定另一個Point實例,計算兩者之間的距離,如果有一組Point實例points,有個Point實例pt,此時我們若想依points中各點與pt的距離排序,透過sorted函式來處理時,該怎麼寫呢?

sorted(points,key=pt.dist)就可以了,因為pt.dist傳回綁定方法,方才談到綁定方法是method類別的實例,行為上可以像函式進行呼叫,也就是可呼叫物件,作為sorted的key引數,就可以簡單地完成基於與pt點的距離進行排序。

萬物皆可呼叫

知道類別是個可呼叫物件,range、enumerate等其實是類別之後,如果此時有人說range是函式,別去糾正他們!

這是因為,Python並不是要開發者去區分len是函式、range是類別等這種事情,而是要開發者不要去區分函式或類別。Python可以定義可呼叫物件,本意就是要讓函式與行為上可呼叫的物件之間,界線變得模糊。

當這當中的界線變得模糊之後,一級函式的應用就能夠有了更大的擴展能力,只要開發者願意,可以讓任何物件實現__call__方法,而不用局限在一定要傳遞函式,這麼一來,從簡單的sorted之類應用,到複雜的meta程式設計,就都能有更大的實現彈性。

如果你想要知道更多可呼叫物件的應用,〈It's a callable!〉有著更多實際的案例;當然,具有可呼叫物件概念的語言不只有Python,而在這些語言中,如果實作時需要一個函式,我們也可以進一步思考:會不會此時其實所需要的,就是一個可呼叫物件!

專欄作者


熱門新聞

Advertisement