要点
- クラスメソッドをまとめたいとなったとき、素朴にクラスメソッドのリストをクラス変数に
funcs = [func0, func1]などとセットして 1 そこから取り出したクラスメソッドをコールすると、TypeError: 'classmethod' object is not callable となりコールできません。 - これはクラスメソッドのコール時に属性アクセス
cls.func()されていないためなので、自力で「ディスクリプタプロトコル__get__でオーナークラスをバインド」してfunc.__get__(None, cls)()とコールする必要があります 2。あるいはクラス変数にせずに「バインドされたクラスメソッド群を返すクラスメソッド」などにすればよりシンプルに解決します。- 下記の私のケースの後者では、クラスメソッドの組がデータメンバに依存するので、「バインドされたクラスメソッド群を返すインスタンスメソッド」にしています。
- そもそもクラスメソッドとはクラスメソッドクラスのインスタンスオブジェクトであり、属性アクセス
cls.func時に内部でディスクリプタプロトコルのコールfunc.__get__(None, cls)を介してコーラブルオブジェクトを返します。これを介さないfuncはクラスメソッドオブジェクトであるため、func()できません。
参考文献
3. データモデル — Python 3.14.4 ドキュメント
内容
クラスメソッドをリストなどにまとめたいことがあると思います。例えば、「朝定食 A タイプ」なら「ごはん、シャケ、みそ汁」の品目生成をコールしたいといった場合です。
以下がそのようなスクリプトで、asagohan_map に各タイプにおける品目生成をまとめてあります (この場合リストでなくリストをもつ辞書ですが、どのみち以降の記述は同様です)。
ただこのとき、asagohan_map からクラスメソッドを取り出してそのままコールする (コメントアウト行) のではエラーになり、明示的にディスクリプタプロトコル __get__ でオーナークラスをバインドする必要があります (コメントアウト行の下の行)。
解決策1. 明示的にバインドする
from enum import Enum
class Asagohan:
AsagohanType = Enum('AsagohanType', ['A', 'B'])
gohan = classmethod(lambda cls: print('ごはん'))
shake = classmethod(lambda cls: print('シャケ'))
tamag = classmethod(lambda cls: print('卵焼き'))
umebo = classmethod(lambda cls: print('梅干し'))
misos = classmethod(lambda cls: print('みそ汁'))
asagohan_map = {
AsagohanType.A: [gohan, shake, misos],
AsagohanType.B: [gohan, tamag, umebo, misos],
}
def __init__(self, type_):
self.type_ = type(self).AsagohanType[type_]
def call(self):
item_funcs = type(self).asagohan_map[self.type_]
print(f'{len(item_funcs)}品目あります')
for f in item_funcs:
# f() # -> TypeError: 'classmethod' object is not callable
f.__get__(None, type(self))()
a = Asagohan('A')
a.call()
3品目あります
ごはん
シャケ
みそ汁
他方、以下のように「バインドされたクラスメソッド群を返すインスタンスメソッド」を用意すれば、自力でディスクリプタプロトコルでバインドすることを回避できます。
解決策2. クラスメソッドまたはインスタンスメソッド内でバインド状態で束ねてから返す
from enum import Enum
class Asagohan:
AsagohanType = Enum('AsagohanType', ['A', 'B'])
gohan = classmethod(lambda cls: print('ごはん'))
shake = classmethod(lambda cls: print('シャケ'))
tamag = classmethod(lambda cls: print('卵焼き'))
umebo = classmethod(lambda cls: print('梅干し'))
misos = classmethod(lambda cls: print('みそ汁'))
def _get_item_funcs(self):
cls = type(self)
if self.type_ == cls.AsagohanType.A:
return [cls.gohan, cls.shake, cls.misos] # バインド済
if self.type_ == cls.AsagohanType.B:
return [cls.gohan, cls.tamag, cls.umebo, cls.misos]] # バインド済
raise NotImplementedError(self.type_)
def __init__(self, type_):
self.type_ = type(self).AsagohanType[type_]
def call(self):
item_funcs = self._get_item_funcs()
print(f'{len(item_funcs)}品目あります')
for f in item_funcs:
f()
a = Asagohan('A')
a.call()
3品目あります
ごはん
シャケ
みそ汁