こんにちは。トグルホールディングス、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 と書いた時点で変数 a が 0 を意味するので,その 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 を作るように,type で Foo を作れる.
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 の主な実用例として,次が挙げられる:
- Dataclass や Pydantic のように,class annotation や class attribute を定義するだけで,インスタンスに影響を及ぼすような操作.
- method のオーバーロード(精密にいうと,dispatch).
通常の業務上, metaclass を使う場面がほとんどないだろうが,理解することで Pydantic や Dispatch などの特殊な使い方を持つライブラリーはどうやって実現したかという疑問の回答にはなる。