toggle Engineer Blog

トグルホールディングス株式会社のエンジニアブログでは、私たちの技術的な挑戦やプロジェクトの裏側、チームの取り組みをシェアします。

Python 入門 - metaclass について

こんにちは。トグルホールディングス、AIエンジニアの鳳凰院そらです。※ハンドルネームです

トグルホールディングスエンジニアアドベントカレンダーの11日目の記事です!

Type の深堀

まず python の基本的な type を呼び起こすため,次のコードを考える.

print(f"{type(0)=}")
print(f"{type(1.)=}")
print(f"{type('2')=}")
print(f"{type((3, '4'))=}")

出力は大体予想できると思うが,次の通りになる.

type(0)=<class 'int'>
type(1.)=<class 'float'>
type('2')=<class 'str'>
type((3, '4'))=<class 'tuple'>

明示的に定義する必要はないが,python の全ての変数が type を持っている.a = 0 と書いた時点で変数 a0 を意味するので,その type ももちろん 0 と一致して,<class 'int'> のほかならない.

では,class の type はどうだろう?

class Foo:
    def f(self, x):
        return 2 * x + 1

bar = Foo()
print(f'{type(bar)=}')

実行すると,

type(bar)=<class '__main__.Bar'>

つまり,class をインスタンス化したら,そのインスタンスの type は自身の class である Foo になる.実際,python を含む多くのプログラミング言語では,type, class, object という3つの単語は同じ意味を持つ.故に,type を聞いて,class 名が返ってくることは不思議ではない.

では,class そのものの type は何だろう?

print(f'{type(int)=}')
print(f'{type(str)=}')
print(f'{type(Foo)=}')

その結果,

type(int)=<class 'type'>
type(str)=<class 'type'>
type(Foo)=<class 'type'>

class そのものの type,あるいは class は,type である.bar = Foo()Fooインスタンス化して bar を作るように,typeFoo を作れる.

Foo = type('Foo', (), {'f': lambda self, x: 2 * x + 1})

上記コードで定義した Foo は,前述の class キーワードを用いて定義した Foo はほぼ同じである.

※「ほぼ」というのは,例をわかりやすくするために,一部本質でないかつ繁雑なメタ情報の入力を省いたからだ.そのゆえ,キーワードで class を書いた場合と若干異なる.機能に影響がなく,ほとんどの場合においてそれらの違いを無視しても構わない.それらの違いを調べたいなら,両方の Foo.__dict__ を出力して比べてみよう.

Metaclass

前述のように class を定義するための class は,metaclass と呼ぶ.全ての class に共通する親 class object が存在しているように,全ての metaclass には,type という共通の親 metaclass が存在している.

python における metaclass は,斬新な概念ではなく,decorator の class に特化したものだと考えられる.実際,metaclass を利用して実現できる機能は,type を改造したり,class 定義時に decorator を掛けたりすることで,同様に実現できる.

まず次の問題を解決する metaclass のプログラミング例を見てみよう:

自分のプログラミング習慣を直すために,class 内の attributes の名前を 4 文字以上つけるべきだと思い,どうやってプログラミング的にそのルールをチェックするか?

class LengthCheckMeta(type):
    def __new__(mcls, name, bases, namespace):
        failed = [attribute for attribute in namespace if len(attribute) < 4]
        assert not failed, (
            f'Name check failed: {failed}.\n'
             '\tAttribute name must be greater than or equal to 4 characters.')
        return super().__new__(mcls, name, bases, namespace)
        

class MyClass(metaclass=LengthCheckMeta):
    i = 1
    
    def f(self):
        ...
    
    def g(self):
        ...
    
    def func(self):
        ...
    
    def valid(self):
        ...

実行すると,

Traceback (most recent call last):
  File "M:\CloudStorages\Google\toggle Drive\BLOG\blog_src\blog1\p3.py", line 10, in <module>
    class MyClass(metaclass=LengthCheckMeta):
  File "M:\CloudStorages\Google\toggle Drive\BLOG\blog_src\blog1\p3.py", line 4, in __new__
    assert not failed, (
           ^^^^^^^^^^
AssertionError: Name check failed: ['i', 'f', 'g'].
    Attribute name must be greater than or equal to 4 characters.

Process finished with exit code 1

とエラーが表示される.

これは一例であり,このようなクラスを定義時の行為を変更させるようには,metaclass を用いる.metaclass の主な実用例として,次が挙げられる:

  1. Dataclass や Pydantic のように,class annotation や class attribute を定義するだけで,インスタンスに影響を及ぼすような操作.
  2. method のオーバーロード(精密にいうと,dispatch).

通常の業務上, metaclass を使う場面がほとんどないだろうが,理解することで Pydantic や Dispatch などの特殊な使い方を持つライブラリーはどうやって実現したかという疑問の回答にはなる。