0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python でクラスメソッドをリストなどにまとめたいときの注意点

0
Posted at

要点

  • クラスメソッドをまとめたいとなったとき、素朴にクラスメソッドのリストをクラス変数に 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品目あります
ごはん
シャケ
みそ汁
  1. より一般に、クラス変数がクラスメソッドの参照を格納していればリストでない場合も同様です (クラス直下スコープではまだクラス名を参照できない=バインド済みの状態で格納できないことに起因する問題であるため)。他方、クラスの外側でクラスメソッドを束ねるならば、属性付きで束ねると思われるのでこの問題は起きないと思います。クラスメソッド内やインスタンスメソッド内で束ねる場合も属性付きで束ねられるのでので問題は起きないと思います。

  2. クラスメソッド名を取得して getattr(cls, name) とすることもできますが、名前を経由したくない場合の話です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?