モダンPython上級 #3 メタクラス — いつ本当に必要か

読了 6分

#2 ディスクリプタ で属性をオブジェクト化する道具を見たなら、今回は クラス自体をオブジェクトとして扱う 道具 — メタクラスです。「上級 Python」の象徴のようによく言及されますが、実は ほとんど使わないのが正解 なところです。何がメタクラスなのか、そしてより軽い代替手段でどこまで解けるかを整理します。

出発点 — クラスもオブジェクト #

Python では クラスもオブジェクト です。

クラス = オブジェクト
class User:
    pass

print(type(User))   # <class 'type'>
print(User.__class__)   # <class 'type'>

User クラスの 型は type です。つまり Usertype のインスタンスです。すべてのクラスはある型 (メタクラス) のインスタンスとして作られます。デフォルトのメタクラスが type です。

type でクラスを動的に作る #

type でクラスを生成
def greet(self):
    print(f"hi, {self.name}")

# 同等: class User: ...
User = type("User", (object,), {
    "name": "default",
    "greet": greet,
})

u = User()
u.greet()   # hi, default

type(name, bases, namespace)クラスを動的に作る 関数です。実は class User: 構文が内部的にこれを呼び出しています。

これは めったに使わない機能 ですが、メタクラスの正体を示しています — メタクラスは結局 type のサブクラスです。

メタクラスの定義 #

メタクラス
class Meta(type):
    def __new__(mcs, name, bases, namespace):
        print(f"クラスを作成中: {name}")
        return super().__new__(mcs, name, bases, namespace)

class User(metaclass=Meta):
    pass

# 出力: クラスを作成中: User

metaclass=Meta でどのメタクラスで作るかを指定します。クラスが定義されるその瞬間Meta.__new__ が呼ばれます。

__init_subclass__ — メタクラスが要らないとき #

#1 で短く見た __init_subclass__ほぼすべてのメタクラスの仕事を解いてくれます。 メタクラスなしで。

自動登録 — __init_subclass__
class Plugin:
    registry = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        Plugin.registry.append(cls)

class JsonPlugin(Plugin): pass
class CsvPlugin(Plugin): pass

print(Plugin.registry)
# [<class 'JsonPlugin'>, <class 'CsvPlugin'>]

同じ仕事をメタクラスでやると、次のようになります。

同じ仕事 — メタクラス版
class PluginMeta(type):
    registry = []
    def __init__(cls, name, bases, namespace):
        super().__init__(name, bases, namespace)
        if name != "Plugin":
            PluginMeta.registry.append(cls)

class Plugin(metaclass=PluginMeta): pass
class JsonPlugin(Plugin): pass
class CsvPlugin(Plugin): pass

長くて、ベースクラス自体を登録しないための if name != "Plugin" のような不格好さがあります。一般的な自動登録は __init_subclass__ が正解 です。

クラスデコレータ — もう 1 つの代替 #

中級 #5 のクラスデコレータも、メタクラスがする仕事の相当部分を担います。

クラスデコレータで
def register(cls):
    Plugin.registry.append(cls)
    return cls

class Plugin:
    registry = []

@register
class JsonPlugin(Plugin): pass

@register
class CsvPlugin(Plugin): pass

明示的という長所があります。暗黙的な登録より明示的な登録 の方が良いことが多いです。

ではメタクラスはいつ? #

次の場面ではメタクラスが最も適しています。

1) type 自体の動作を変えたいとき #

isinstanceissubclass のような検査の動作はメタクラスが決定します。ABC (abc.ABCMeta) がその例です。

ABC = メタクラス
from abc import ABC, abstractmethod

class Storage(ABC):
    @abstractmethod
    def save(self, data): ...

class FileStorage(Storage):
    def save(self, data): ...

class BadStorage(Storage):
    pass

s = FileStorage()    # OK
b = BadStorage()     # ✗ TypeError: Can't instantiate abstract class

@abstractmethod を持つクラスをインスタンス化させない動作は メタクラスのレベル で起きます。こういうケースは __init_subclass__ で解くのが難しいか不可能です。

2) DSL — クラス定義そのものを新しい構文のように #

ORM モデル — ユーザー視点
class User(Model):
    id = IntegerField(primary_key=True)
    name = StringField(max_length=50)

このようなコードでメタクラスがする仕事。

  • どんなフィールドが定義されたかを収集
  • テーブル名の決定
  • _meta のようなメタデータオブジェクトの生成
  • バリデーションメソッドの自動追加

Django ORM、SQLAlchemy、Peewee などがこのパターンを使います。クラス定義を一種の DSL として 使うときにメタクラスが必要になります。

3) 多重継承で一貫した動作を保証 #

複数のベースクラスが それぞれ別のメタクラス を持つと衝突します。メタクラスのもう一段上ですべての動作を統制するとき。

これは普通のコードではほぼありません。ライブラリ作成者に該当する領域です。

メタクラス vs その他の道具 — 決定ガイド #

やること最初の選択
サブクラスの自動登録__init_subclass__
1 クラスに動作を追加 (eq、repr など)クラスデコレータ (@dataclass など)
属性アクセスをフックディスクリプタ
インスタンスメソッド/属性を追加通常のメソッド/属性
isinstance/issubclass の動作変更メタクラス
ベースクラス定義そのものを DSL のようにメタクラス
クラス定義時点での構文バリデーションメタクラス または __init_subclass__

判断基準: __init_subclass__ またはクラスデコレータで解けるなら、ほぼ常にそちらが軽い正解。メタクラスは それらで解けないとき

__init_subclass__ の実力 #

このフックがどれだけ多くの仕事をできるかを見れば、メタクラスが減っていく理由が分かります。

自動登録 #

登録
class Handler:
    handlers = {}
    def __init_subclass__(cls, *, route=None, **kwargs):
        super().__init_subclass__(**kwargs)
        if route:
            Handler.handlers[route] = cls

class HomeHandler(Handler, route="/"): pass
class AboutHandler(Handler, route="/about"): pass

print(Handler.handlers)
# {'/': <class 'HomeHandler'>, '/about': <class 'AboutHandler'>}

サブクラス定義の箇所に route="/" のような 引数を渡せる こともあまり知られていない事実です。__init_subclass__ がその引数を受け取ります。

必須メソッドの強制 #

抽象メソッドの強制
class Service:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        for method in ("start", "stop"):
            if not hasattr(cls, method):
                raise TypeError(f"{cls.__name__}{method} が必要")

class Good(Service):
    def start(self): ...
    def stop(self): ...

class Bad(Service):    # ✗ TypeError
    def start(self): ...

abc.ABC より軽い強制 — 仕事を直接定義できます。

クラス変数のバリデーション #

フィールドのバリデーション
class Form:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        if not hasattr(cls, "name"):
            raise TypeError(f"{cls.__name__}.name が必要")
        if not isinstance(cls.name, str):
            raise TypeError("name は str")

class LoginForm(Form):
    name = "login"

__class_getitem__ — ジェネリクス構文のフック #

MyClass[int] のような表記を可能にするフック。

__class_getitem__
class Box:
    def __class_getitem__(cls, item):
        return f"Box[{item.__name__}]"

print(Box[int])   # Box[int]

list[int]dict[str, int] のようなビルトインジェネリクスはこのフックの上に作られています。ユーザークラスをジェネリックにするとき (Python 3.12 の class Foo[T]: 新構文以前のやり方) はこのフックが仕事をしていました。

type.__call__ — インスタンス生成のフロー #

User() が実行されると、次の順序で処理されます。

  1. type(User).__call__(User) が呼ばれる (メタクラスの __call__)
  2. その中で User.__new__(User) でインスタンス生成
  3. User.__init__(instance) で初期化

このフローをフックするにはメタクラスの __call__ をオーバーライドします。シングルトン、インスタンスキャッシュのような場面で登場。

シングルトン — メタクラス版
class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class Logger(metaclass=SingletonMeta):
    def __init__(self):
        self.history = []

a = Logger()
b = Logger()
print(a is b)   # True

__new__ だけでも可能な仕事ですが、__init__ が 2 回呼ばれる罠 があるためメタクラス側の方が安全な場面があります。

メタクラス衝突 #

サブクラスのメタクラスは すべての親のメタクラスのサブタイプでなければ なりません。

🚫 衝突
class MetaA(type): pass
class MetaB(type): pass

class A(metaclass=MetaA): pass
class B(metaclass=MetaB): pass

class C(A, B): pass
# TypeError: metaclass conflict

複数のライブラリのクラスを多重継承するときに出会う厄介事 — だからライブラリ作成者もメタクラスを慎重に使います。

まとめ #

今回押さえたこと。

  • クラスもオブジェクト、クラスの型は type (デフォルトのメタクラス)
  • type(name, bases, namespace) でクラスを動的に生成
  • メタクラス = type のサブクラス、class X(metaclass=Meta): で指定
  • __init_subclass__ (3.6+) が一般的なメタクラスの仕事の 90% 以上を解いてくれる
  • クラスデコレータも強力な代替
  • メタクラスは ABC、ORM の DSL、多重継承の一貫性のような狭いケース
  • __init_subclass__ は引数も受け取る — class Sub(Base, route="/x"):
  • __class_getitem__Cls[T] 表記のフック
  • メタクラスは衝突が多い道具なので、可能な限り使わない方が良い

次回 (#4 非同期の深さ) では、中級 #7 で見た asyncio の次の段階 — イベントループの動作、gather/wait の細かい違い、async generator、並行性パターンを扱います。

X