目次
17 章

メタクラス — いつ本当に必要か

クラスを作るクラスを扱います。type の正体、__init_subclass__ との分担、クラスデコレータで解けるケース、そして本当にメタクラスが必要な狭い領域までまとめます。

第16章 ディスクリプタと __set_name__ で属性をオブジェクト化する道具を見ましたが、本章は クラスそのものをオブジェクトとして扱う 道具 — メタクラスです。「上級 Python」の象徴のように頻繁に言及されますが、実際のコードでは使う場面をかなり絞る方が安全です。何がメタクラスなのか、そしてより軽い代替手段でどこまで解けるかを整理します。

本章の核心メッセージは「メタクラスが解いてくれる仕事の 90% は、第12章 デコレータパターン のクラスデコレータ、または第15章 マジックメソッド__init_subclass__ で解ける」です。本当にメタクラスが必要な狭い領域を識別するのが本章の目的です。

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

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__ — メタクラスが必要ないとき #

第15章 で簡単に見た __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__ の方が単純 です。

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

第12章 デコレータパターン のクラスデコレータも、メタクラスがやる仕事の相当部分をこなします。

クラスデコレータで
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 として 使うときにメタクラスが必要です。第25章 DB 連携 — SQLAlchemy 2.x + Alembic の ORM モデルがこれに当たります。

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]: 新構文以前の方式)、本フックが仕事をしていました。新しいコードは第9章 typing 本格class Stack[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

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

練習問題 #

  1. __init_subclass__ で自動登録器を作ってください。class Handler: のサブクラス定義部に route="/path" のようなキーワード引数を受け取り、Handler.routes: dict[str, type] に登録します。HomeHandlerAboutHandler の 2 つのサブクラスで動作を確認します。
  2. __init_subclass__ で必須メソッドの強制検証を作ってください。サブクラスが start / stop メソッドを持たない場合、定義時点で TypeError が発生します。
  3. メタクラス SingletonMeta でシングルトンパターンを実装してください。同じクラスの 2 回目の呼び出しが最初のインスタンスを返すか確認します。同じ仕事を __new__ だけで実装したあと、両方式の罠 (特に __init__ が毎回呼ばれるか) を比較します。

一行まとめ: クラスもオブジェクト、クラスの型が type (デフォルトメタクラス)。メタクラスは type のサブクラス。一般的なメタクラスの仕事の 90% 以上は __init_subclass__ またはクラスデコレータで解ける。本当にメタクラスが必要なのは ABC、ORM DSL、多重継承の一貫性のような狭い領域。メタクラス衝突が頻繁なのでなるべく使わない方が良い。

次の章 #

次の 第18章 非同期の深さ — イベントループ、gather/wait、async generator では、第14章 非同期入門 で見た asyncio の次の段階 — イベントループの動作、gather / wait の微妙な違い、async generator、並行性パターンを扱います。

X