モダンPython上級 #3 メタクラス — いつ本当に必要か
#2 ディスクリプタ で属性をオブジェクト化する道具を見たなら、今回は クラス自体をオブジェクトとして扱う 道具 — メタクラスです。「上級 Python」の象徴のようによく言及されますが、実は ほとんど使わないのが正解 なところです。何がメタクラスなのか、そしてより軽い代替手段でどこまで解けるかを整理します。
出発点 — クラスもオブジェクト #
Python では クラスもオブジェクト です。
class User:
pass
print(type(User)) # <class 'type'>
print(User.__class__) # <class 'type'>User クラスの 型は type です。つまり User は 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, defaulttype(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
# 出力: クラスを作成中: Usermetaclass=Meta でどのメタクラスで作るかを指定します。クラスが定義されるその瞬間 に Meta.__new__ が呼ばれます。
__init_subclass__ — メタクラスが要らないとき
#
#1 で短く見た __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 自体の動作を変えたいとき
#
isinstance、issubclass のような検査の動作はメタクラスが決定します。ABC (abc.ABCMeta) がその例です。
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 — クラス定義そのものを新しい構文のように #
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 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() が実行されると、次の順序で処理されます。
type(User).__call__(User)が呼ばれる (メタクラスの__call__)- その中で
User.__new__(User)でインスタンス生成 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、並行性パターンを扱います。