Python のメタクラスで関数の自動登録を実装する

メタクラスを使うと、クラス定義時に関数やメソッドを自動的に登録する仕組みを作れます。プラグインシステムやコマンドパターンの実装に有用です。

基本的なアイデア

クラスが定義されるとき、メタクラスの __new__ または __init__ が呼ばれます。ここでクラスの属性を走査し、特定の条件を満たす関数を登録できます。

class PluginMeta(type):
    registry = {}
    
    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        if name != "PluginBase":
            mcs.registry[name] = cls
        return cls

class PluginBase(metaclass=PluginMeta):
    pass

class EmailPlugin(PluginBase):
    def execute(self):
        print("Sending email")

class SMSPlugin(PluginBase):
    def execute(self):
        print("Sending SMS")

print(PluginMeta.registry)
# {'EmailPlugin': <class 'EmailPlugin'>, 'SMSPlugin': <class 'SMSPlugin'>}

特定のデコレータが付いたメソッドを登録

メソッドにマーカーを付けて、それを自動収集するパターンです。

def command(name):
    def decorator(func):
        func._command_name = name
        return func
    return decorator

class CommandMeta(type):
    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        cls._commands = {}
        for attr_name, attr_value in namespace.items():
            if hasattr(attr_value, "_command_name"):
                cls._commands[attr_value._command_name] = attr_value
        return cls

class Bot(metaclass=CommandMeta):
    @command("hello")
    def say_hello(self):
        return "Hello!"
    
    @command("bye")
    def say_goodbye(self):
        return "Goodbye!"

print(Bot._commands)
# {'hello': <function Bot.say_hello>, 'bye': <function Bot.say_goodbye>}

init_subclass による代替

Python 3.6 以降では、メタクラスを使わずに __init_subclass__ で同様のことができます。

class PluginBase:
    registry = {}
    
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        PluginBase.registry[cls.__name__] = cls

メタクラスはより強力ですが、__init_subclass__ で十分な場合はそちらを選ぶとコードがシンプルになります。