Python 物件當函式使及動態新增方法
繼續閱讀 《Python Tricks: The Book》,書中說到 "Objects Can Behave Like Functions", 就是把物件當成函式來呼叫,在普通物件後加個括號就能呼叫相應的__call__
函式。下面是書中的例子
class Adder: def __init__(self, n): self.n = n def __call__(self, x): return self.n + x
然後是應用類Adder
的程式碼
plus_3 = Adder(3)
plus_3(4) # 普通物件 plus_3 當成函式來用
上面plus_3
是一個普能的物件,並非一個函式,但如果把它當成函式來看待,那麼plus_3(4)
就會去尋找相應的__call__
函式。
這種行為與 Scala 的 case class 或 apply() 函式很類似。來一段 Scala 程式碼的演示
case class User(val id: Int) { def apply(name: String) { println(s"Id: ${id}, Hello ${name}") } } val user = User(1) # case class 例項的建立,其實是 new User(1) 的縮寫形式 user("Yanbin")
上面的 Scala 執行輸出
Id: 1, Hello Yanbin
這裡的物件user
後直接加括號,當函式來對待實質是呼叫相應的apply(name)
函式。
進一步延伸,對於自定義的類我們可以事先建立__call__
函式進行把物件當函式來用,但是對於別人建立的類是否能動態的新增__call__
方法呢?這一部分的內容或許與上面所介紹的無多大的關係,不過既然是觀後感,不妨列在一塊。
動態新增方法
比如說有一個沒定義__call__
方法的類
class Adder: def __init__(self, n): self.n = n
如何實現出與前面同樣的效果來
plus_3 = Adder(3) plus_3(4)
上面的plus_3(4)
會報TypeError: 'Adder' object is not callable
,因為作為一個鴨子找不到__call__
方法。
其實也很簡單,Python 的類或物件都是動態的,直接事後對Adder
加個__call__
方法就成。下面寫成 lambda 的形式
給類新增例項方法
這裡我們定義一個foo(self, x)
方法,後面的例項都使用相同的foo
方法定義。
>>> def foo(self, x): self.n = self.n + x >>> Adder.__call__ = foo >>> plus_3 = Adder(3) >>> plus_3(4) >>> plus_3.n >>> plus_3.__call__ <bound method foo of <__main__.Adder object at 0x7fac93f9e940>>
這裡注意到plus_3.__call__
是一個bound method
.
給物件新增例項方法
如果持有的是一個Adder
的物件plus_3
,不直接給Adder
類增添__call__
屬性,又該如何實現plus_3(4)
的呼叫呢?用plus_3.__class__
就行。關鍵程式碼如下
>>> plus_3.__class__.__call__ = foo >>> plus_3(4) # 同樣的效果
但是不能像下面那樣直接給plus_3
例項新增__call__
方法,比如下面的做法就有問題
>>> plus_3.__call__ = foo >>> plus_3(4) # 錯誤: TypeError: 'Adder' object is not callable >>> plus_3.__call__(4) # 錯誤: TypeError: foo() missing 1 required positional argument: 'x' >>> plus_3.__call__ <function foo at 0x7fac97d04598>
plus_3.__call__
是一個function
, 即unbound method
。
plus_3.__call__(4)
中的引數傳遞給了foo(self, x)
的第一個引數self
了,而x
沒得到引數。
因為這時候foo
並未當作一個例項方法,更準確講是bound method
, 即方法的第一個引數是例項本身,通過用self
作為形參名。
那是否能直接用plus_3.__call__
的方式來新增新的bound method
方法呢?這時候可以應用 types
模組,下面進行嘗試
>>> plus_3 = Adder(3) >>> import types >>> plus_3.__call__ = types.MethodType(foo, plus_3) >>> callable(plus_3) False # 所以仍然不能用 plus_3(4) 的方式 >>> plus_3.__call__(4) # 前面確實添加了 __call__ 方法 >>> plus_3.n 6 # 並且 self 也是湊效的 >>> plus_3.__call__ <bound method foo of <__main__.Adder object at 0x7fac93fb0438>>
上面雖然是可以新增上一個__call__
方法,它確實是一個 bound method
, 但是隻能顯式的用 __call__
來呼叫,有點特殊。再次嘗試
>>> from functools import partial >>> plus_3.__call = partial(foo, plus_3) >>> callable(plus_3) False # 仍然不是 callable >>> plus_3.__call__(4) # 這樣沒問題 >>> plus_3.__call__ functools.partial(<function foo at 0x7fac97d04598>, <__main__.Adder object at 0x7fac93fb0438>)
用types.MethodType
確實可以新增bound method
, 但是由它新增的 __call__
方法後並不能改變callable
為False
的特徵,所以無法用plus_3(4)
的方式來呼叫__call__
方法。同時證明了有 __call__
bound method 的物件也不一定能當作函式來呼叫。
一個 Python 物件的callable
為True
,直接當函式來呼叫也可能失敗,但是callable
為False
的話,當函式呼叫必定失敗。當有一個bound
__call__
方法時,callable
仍然為False
時就要深入callable
的原始碼來看判定規則了。
連結: