__init_subclass__フックは、Python 3.6でPEP 487の一部として導入されました。これ以前は、サブクラス化時にアクションを実行したいクラスは、カスタムメタクラスを宣言する必要がありました。メタクラスは強力ですが、注意深く調整しない限り、複数の継承のシナリオで摩擦が生じます。新しいフックにより、基本クラスは特定のメタクラスを採用することを強制されることなく、サブクラスの初期化に参加できるようになり、以前は複雑なメタクラス操作に依存していたDjango ORMやSQLAlchemyのようなフレームワークが簡素化されます。
クラスBが基本クラスAを継承する際、フレームワーク開発者はしばしばクラスBが定義される瞬間にロジックを実行する必要があります—インスタンスが作成される前に。例えば、ORMは、Bからすべてのカラム定義を集めてレジストリに保存する必要があるかもしれません。メタクラスを使用するには、Aがtypeまたはカスタムメタクラスをメタクラスとして持つ必要があり、Bが別のメタクラス(例:ABCや他のフレームワーク)を使用する必要がある場合、問題が発生します。これにより、解決が難しいメタクラスの競合エラーが発生します。さらに、メタクラスの__new__は、クラスの名前空間が完全にポピュレートされる前に実行されるため、最終的なクラス属性を検査しにくくなります。
Pythonは__init_subclass__クラスメソッドを提供しています。このメソッドを定義したクラスが作成されると、自動的に呼び出され、定義クラスを直接の親として持つサブクラスが作成されます。このフックは、新しく作成されたサブクラスを最初の引数として受け取り、クラス定義行で渡されたすべてのキーワード引数を受け取ります(例:class B(A, keyword=value))。
class RegistryBase: _registry = {} def __init_subclass__(cls, category="default", **kwargs): super().__init_subclass__(**kwargs) print(f"Registering {cls.__name__} under category '{category}'") cls._registry[cls.__name__] = {"class": cls, "category": category} class Plugin(RegistryBase, category="audio"): pass class Effect(Plugin, category="reverb"): pass
メタクラスの__new__がクラスオブジェクトが存在する前にクラス作成中に実行されるのに対し、__init_subclass__はクラスオブジェクトが完全に構築された後に実行されます。これにより、フックはcls.__dict__、メソッド、およびアノテーションを安全に検査できます。また、フックはMROを尊重し、super()が呼び出されるときに親クラスの登録が子クラスのロジックの前に行われることを保証します。
大規模なオーディオ処理SaaSプラットフォームでは、エンジニアリングチームがサードパーティの開発者が基本のAudioEffectクラスからサブクラス化してオーディオエフェクトを定義できるプラグインシステムを実装する必要がありました。各サブクラスは、effect_name、latency_ms、categoryなどのメタデータを持つグローバルエフェクトカタログに自動的に登録される必要がありました。問題は、プラットフォームがすでにデータベースモデルにメタクラスを使用するSQLAlchemy宣言ベースを使用しており、一部のオーディオエフェクトがAudioEffectとSQLAlchemyモデルの両方から継承する必要があったことです。AudioEffectにカスタムメタクラスを導入すると、SQLAlchemyのDeclarativeMetaとのメタクラスの競合を引き起こし、アプリケーションの起動が壊れることになりました。
最初のアプローチは、デコレーターを使用した手動登録を含みました。開発者は、各クラス定義の上に@register_effectを書く必要がありました。これでは機能しましたが、エラーが発生しやすく、開発者はデコレーターを忘れることが多く、本番環境でエフェクトが失われるという問題がありました。また、メタデータをデコレーターの引数とクラス定義の両方で繰り返す必要があり、DRY原則に違反していました。
2番目のアプローチでは、DeclarativeMetaとEffectMetaの両方から継承する共通のメタクラスを使用しようとしました。これにより、即座の競合が解決されましたが、壊れやすい依存関係が生まれました。SQLAlchemyが内部メタクラスのロジックを更新するたびにプラットフォームが壊れました。また、すべてのエフェクトクラスをデータベースモデルに強制し、軽量のクライアントサイドエフェクトには不適切でした。
3番目のアプローチでは__init_subclass__を利用しました。AudioEffect基本クラスは、クラス定義中に渡されたキーワード引数(例:effect_idやversion)をキャプチャするために__init_subclass__を定義しました。開発者がclass Reverb(AudioEffect, effect_id="rvb-01", version=2)と書いたとき、フックは自動的にIDの一意性を検証し、スレッドセーフなWeakValueDictionaryレジストリにクラスを登録しました。これにより、__init_subclass__は通常のクラスメソッドであり、あらゆるメタクラスと協力するため、メタクラスの競合を完全に回避しました。
チームは3番目の解決策を選びました。それにより、SQLAlchemyとの互換性が保たれ、デコレーターが不要になり、インポート時に自動登録が行われることが保証されました。その結果、「ただ動作する」プラグインシステムが実現され、開発者はサブクラス化し、パラメータをインラインで宣言するだけでよくなりました。このシステムは150以上のエフェクトを登録し、メタクラスの競合なしで、メタクラスアプローチと比較して起動時間が40%向上しました。MRO計算の複雑さが軽減されたためです。
__init_subclass__は、親がそれを定義していなくても常にsuper().__init_subclass__()を呼び出さなければならないのはなぜですか?
候補者は、objectが__init_subclass__を定義していないため、その呼び出しはオプションであると仮定することがよくあります。しかし、複数継承のシナリオでは、super()を呼び出さないことで、フックを実装する兄弟クラスのチェーンが壊れる可能性があります。Pythonの協調的な複数継承は、ダイヤモンドのすべての参加者がsuper()を呼び出すことを要求し、階層のすべての枝が初期化ロジックを実行することを保証します。AとBがどちらも__init_subclass__を定義しており、C(A, B)がAのフックのみを呼び出すと、Bの登録ロジックは静かにスキップされ、プラグインシステムに微妙なバグが生じます。
__init_subclass__はメソッドを動的に変更したり追加したりできますか?また、__slots__への影響は何ですか?
候補者はしばしば__init_subclass__とメタクラスの__new__を混同します。__init_subclass__はクラスが完全に作成された後に実行されるため、作成前にクラス辞書を変更することはできません(__prepare__やメタクラスの__new__とは異なります)。ただし、setattr(cls, name, value)を使用して属性を動的に追加することはできます。危険は__slots__にあります:親クラスが__slots__を使用している場合、サブクラスはその制約を継承します。__init_subclass__でsetattrを使用してスロットクラスに新しい属性を追加しようとすると、サブクラスが自身で__slots__または__dict__を定義していない限り、AttributeErrorが発生します。この制限により、設計者は登録/メタデータのために__init_subclass__を使用するのか、クラス本体の真の構造的変更のためにメタクラスを使用するのか選択する必要があります。