callable 物件
April 26, 2022在 Python 中,函式是一級物件,可以作為值傳遞給變數、參數,或作為函式、方法的傳回值,這是個極具威力的特性,Python 標準程式庫或第三方程式庫,有許多 API 也都接受函式或傳回函式,或者是接受函式且傳回函式的高階函式,對吧?
其實這些 API,有可能在定義時的目的,是定位可以接受 callable 物件或傳回 callable 物件,或者是接受 callable 物件且傳回 callable 物件的高階 callable 物件。
函式、方法、類別?
作為一個函式,它是可以呼叫的,這不成問題,Python 有個 callable
函式,可以判斷指定的引數是否可呼叫。例如:
>>> def foo():
... print('foo')
...
>>> callable(foo)
True
在〈屬性與方法〉中談過,可以透過類別的實例取得綁定方法,綁定方法是 method
的實例,行為上可以像函式進行呼叫,傳入 callable
函式的結果會是?
>>> class Some:
... def action(self):
... print(self, 'action')
...
>>> s = Some()
>>> type(s.action)
<class 'method'>
>>> callable(s.action)
True
>>>
你有想過為什麼 Python 中,要建立類別實例,不需要使用 new
嗎?當然,你可以說語法就是如此規範,不過,其實類別在行為上,就是可以像函式進行呼叫:
>>> callable(Some)
True
>>>
__call__ 方法
作為一門動態定型語言,實現鴨子定型是很簡單的事情,既然函式、方法、類別在行為上,可以像函式進行呼叫,表示它們具有相同的行為,具體而言,它們都實現了 __call__
方法。
若類別定義有 __call__
方法 ,該類別的實例,就可以像函式一樣地呼叫:
>>> class Some:
... def __call__(self, *args):
... print(args)
...
>>> s = Some()
>>> s(1)
(1,)
>>> s(1, 2)
(1, 2)
>>> s(1, 2, 3)
(1, 2, 3)
>>>
只要實作了 __call__
方法,就是 callable 物件,這使得函式與各種類型的實例,之間的界線變得模糊,在之前的文件中,你曾經以函式的形式使用的一些 API,其實就是 callable 物件,例如 range
、enumerate
、zip
、type
等,它們其實不是 function
的實例,range
、enumerate
、zip
、type
是類別,是 type
的實例:
>>> range
<class 'range'>
>>> zip
<class 'zip'>
>>> type
<class 'type'>
>>>
類別是 type
的實例,type
定義了 __call__
方法,因此類別可以使用 range(10)
這類方式呼叫。
知道類別是個 callable 物件,range
、zip
、type
等其實是類別,並不是要你去區分 len
是函式,range
是類別這種事,而是要你不用去區分 len
是函式,range
是類別這種事,Python 可以定義 callable 物件,本意就是要讓函式與行為上可呼叫的物件之間,界線變得模糊。
因此如果有人說,range
函式,不用去糾正他,Python 就是要你搞不清楚,反正可以呼叫就是了!
有狀態的函式?
有時候會需要一個函式帶有狀態,例如,想要一個 add
函式:
>>> def add(n):
... return lambda a: n + a
...
>>> add10 = add(10)
>>> [add10(i) for i in range(5)]
[10, 11, 12, 13, 14]
>>>
add
函式傳回另一個函式,該函式帶著 n
的值,這形式了 closure,呼叫這個傳回的函式時,都會與當時傳入的 n
進行相加。
有時候攜帶的狀態需要變動,例如,你可能需要一個具有累計加總能力的函式:
>>> def running_total(base):
... total = 0
... def _running_total(a):
... nonlocal total
... total = base + total + a
... return total
... return _running_total
...
>>> partial_sum_from_10 = running_total(10)
>>> [partial_sum_from_10(i) for i in range(5)]
[10, 21, 33, 46, 60]
>>>
這種包含著內部可變動狀態的函式,也常用來模擬一些私有性,例如 JavaScript,可藉由傳回一個形成閉包的函式,來模擬物件的私有屬性。
當事情變得更為複雜之時,例如,你也許需要能直接存取內部狀態,例如運行過程取得 total
或者重置 base
,傳回函式的作法不適用之時,可以實作 callable 物件。例如:
>>> class running_total:
... def __init__(self, base):
... self.base = base
... self.total = 0
... def __call__(self, a):
... self.total = self.base + self.total + a
... return self.total
...
>>> partial_sum = running_total(10)
>>> [partial_sum(i) for i in range(5)]
[10, 21, 33, 46, 60]
>>> partial_sum.total
60
>>> partial_sum.base
10
>>>
接受/傳回 callable
Python 可以實作裝飾器(decorator),例如〈屬性與方法〉看過的 @property
,就是裝飾器,裝飾器就是 callable 物件,簡單的裝飾器可以使用函式實作,複雜的裝飾器可以使用類別實作,property
就是使用類別定義的裝飾器,而且它是個接受 callable、傳回 callable 的裝飾器。
裝飾器的使用,後續的文件會再介紹,簡單來說,在 Python 中,有些 API 可以接受函式、傳回函式,或者接受函式並傳回函式,實際上,它們可能只是接受 callable 物件、傳回 callable 物件。
有些 API 可能真的會檢查傳入的是不是函式,functools
模組有個 wraps
函式,可以將 callable 物件偽裝為函式。
例如,sorted
函式的 key
可以接受的不只是函式,只要是 callable 物件就可以,對於一組點,你也許想按照它與某個點的距離來排序:
>>> class Point:
... def __init__(self, x, y):
... self.x = x
... self.y = y
... def dist(self, p):
... return ((self.x - p.x) ** 2 + (self.y - p.y) ** 2) ** 0.5
... def __repr__(self):
... return f'Point({self.x}, {self.y})'
...
>>> points = [Point(10, 20), Point(2, 3), Point(50, 98), Point(2, 33)]
>>> pt = Point(3, 2)
>>> sorted(points, key = pt.dist)
[Point(2, 3), Point(10, 20), Point(2, 33), Point(50, 98)]
>>>
pt.dist
是綁定方法,方才談到,綁定方法是 method
的實例,行為上可以像函式進行呼叫,也就是 callable 物件,作為 sorted
的 key
引數,就可以簡單地完成基於與 pt
點的距離進行排序。
像 Point
這種記錄資料用的類別,Python 3.7 新增了 dataclasses.dataclass
裝飾器,可以用來簡化任務。