Python のメタクラスとクラスデコレータって何?






クラス定義時に
共通して実行したい処理が
ある時に使います。





1. クラスデコレータ

例えばクラス定義時に Hello, world! を表示したいとします。そんな時はクラスデコレータを使います。

def decorator(cls):
    print('Hello, world!')
    return cls

@decorator
class Cls(object):
    pass
>>> def decorator(cls):
...     print('Hello, world!')
...     return cls
... 
>>> @decorator
... class Cls(object):
...     pass
... 
Hello, world!
>>> 

2. メタクラス

例えばクラス定義時に Hello, world! を表示したいとします。そんな時は メタクラスで __init__ を作ります。

class Meta(type):
    def __init__(self, name, bases, name_space):
        print('Hello, world!')

class Cls(metaclass=Meta):
    pass
>>> class Meta(type):
...     def __init__(self, name, bases, name_space):
...         print('Hello, world!')
... 
>>> class Cls(metaclass=Meta):
...     pass
... 
Hello, world!
>>> 
>>> 


◯ 何でこんな動作をするの?

a と b は等価です。 a と b が等価であることを理解できると、メタクラスを理解できた気がするようになります。

# a.
class Meta(type):
    def __init__(self, name, bases, name_space):
        print('Hello, world!')

class Cls(metaclass=Meta):
    pass
# b.
def init(meta, name, bases, name_space):
    print('Hello, world!')

Meta = type('Meta', (type, ), {'__init__': init})
Cls = Meta('Cls', (object, ), {})


Cls は Meta からインスタンス化されたクラスです。 このようにしてクラスをインスタンス化するクラスのことをメタクラスと言うらしいです。

オブジェクト指向プログラミングにおいてメタクラスとは、 インスタンスがクラスとなるクラスのことである。
メタクラス - Wikipedia

◯ とは言え、よくわからない...

ここで大事なのは、「クラス定義」と「type クラスからインスタンス化してクラスオブジェクトを生成すること」が同じであるということ。

# a, b は等価

# a. クラス定義
class Cls(object):
    pass

# b. インスタンス化
Cls = type('Cls', (object, ) {})


そのため、もし type クラスの __init__ をオーバーライドできれば、クラス定義時に実行したい処理が追加できるはずです。

#  Step1. メタクラス定義
class Meta(type):
    def __init__(meta, name, bases, name_space):
        print('Hello, world!')

# Step2. クラス定義
Cls = Meta('Cls', (object, ), {})


実行すると Hello, wolrd! を表示してくれます。

>>> Cls = Meta('Cls', (object, ), {})
Hello, world!
>>> 


でも、クラス定義のために Cls = Meta('Cls', (object, ), {}) と書くのは、何だか読みづらいですよね。 そこで Pythonメタクラスのための専用の構文を用意してくれています。それが、これです。

class Meta(type):
    def __init__(self, name, bases, name_space):
        print('Hello, world!')

class Cls(metaclass=Meta):
    pass

クラスデコレータととメタクラスの使い分けはどうしたらいいの?

そんなに大したことをしないときはデコレータ、大掛かりなことをするときはメタクラスくらいにしか理解できていない。

例えば Python 3.7 で追加された dataclasses では、メタクラスではなくデコレータが採用されました。

PEP 557 - Data Classes
基底クラスもしくはメタクラスは、Data Classes においては使われない。Data class を使うユーザは、継承やメタクラスを Data Class から受けることなく自由に使うことができる。デコレートされたクラスは、完全に "普通" の Python のクラスである。Data Class デコレータで作成されたクラスは、普通のクラスと完全に同じように使えなければいけない。
No base classes or metaclasses are used by Data Classes. Users of these classes are free to use inheritance and metaclasses without any interference from Data Classes. The decorated classes are truly "normal" Python classes. The Data Class decorator should not interfere with any usage of the class.


もともとは Guido はメタクラスがあるからクラスデコレータには反対だったらしいですが、最終的にはクラスデコレータを承認しました。PEP 3129 は読解中..

PEP 3129 - Class Decorators
関数デコレータがもともと Python 2.4 から議論されていた時、クラスデコレータはメタクラスがあるので不明瞭で不必要だと思われていた。Python 2.4 系のリリースとそれに伴う関数デコレータへの習熟、実際の使用例が増加した数年の実体験の後、 BDFL と Python のコミュニティはクラスデコレータを再評価し Python 3.0 からクラスデコレータを含めるように勧告した。
When function decorators were originally debated for inclusion in Python 2.4, class decorators were seen as obscure and unnecessary [1] thanks to metaclasses. After several years' experience with the Python 2.4.x series of releases and an increasing familiarity with function decorators and their uses, the BDFL and the community re-evaluated class decorators and recommended their inclusion in Python 3.0 [2].

The motivating use-case was to make certain constructs more easily expressed and less reliant on implementation details of the CPython interpreter. While it is possible to express class decorator-like functionality using metaclasses, the results are generally unpleasant and the implementation highly fragile [3]. In addition, metaclasses are inherited, whereas class decorators are not, making metaclasses unsuitable for some, single class-specific uses of class decorators. The fact that large-scale Python projects like Zope were going through these wild contortions to achieve something like class decorators won over the BDFL.

immutable なクラスを作る。

クラスデコレータ、メタクラスを使って immutable なオブジェクトを生成するクラスオブジェクトを作りたいと思います。immutable とは何かについて、なんとなく押さえておいてもらえればと思います。



1, 3 は Python 公式のドキュメントに記載されていた書き方。 2, 4 はワイのやり方。2 はいいかなと思うけど 4 は悲惨な実装なので非推奨。

1. namedtuple を使う

直接、namedtuple が生成してくれたクラスを継承してしまいます。わかりやすいですね。

import collections

class Region(collections.namedtuple('ImmutableRegion', 
      ('x1', 'y1', 'x2', 'y2',
       'is_rectangle', 'is_line', 'is_dot'))):
    def __new__(cls, x1, y1, x2, y2):
        # point 1. __init__ ではなく、__new__ を使う。
        width_0 = (x1 - x2 == 0)
        height_0 = (y1 - y2 == 0)
        is_rectangle = (not width_0 and not height_0)  # 0 0
        is_line = (width_0 != height_0)  # 0 1 or 1 0; xor
        is_dot = (width_0 and height_0)  # 1 1
        args = (x1, y1, x2, y2, is_rectangle, is_line, is_dot)
        
        # point 2. 必要なオブジェクトが揃ったところで
        #          オブジェクトを生成する。
        self = super().__new__(cls, *args)
        
        # point 3. 生成したオブジェクトを return
        return self

Region(0, 0, 1, 1)

2. クラスデコレータ(namedtuple を使う)

1 の syntax suger のような感じになります。super が口惜しい感じですが。一応、回避策を模索したのですが、直接 super() と書けるような方法はなさそうでした。

@immutable(
    'x1', 'y1', 'x2', 'y2',
    'is_rectangle', 'is_line', 'is_dot')
class Region(object):
    # point 2. __init__ ではなく、__new__ を使う。
    #          オブジェクトが生成される前に呼び出される.
    def __new__(cls, x1, y1, x2, y2):
        width_0 = (x1 - x2 == 0)
        height_0 = (y1 - y2 == 0)
        is_rectangle = (not width_0 and not height_0)  # 0 0
        is_line = (width_0 != height_0)  # 0 1 or 1 0; xor
        is_dot = (width_0 and height_0)  # 1 1
        args = (x1, y1, x2, y2, is_rectangle, is_line, is_dot)
        
        # point 3. 必要なオブジェクトが揃ったところで
        #          オブジェクトを生成する。
        self = super(Region, cls).__new__(cls, *args)
        
        # point 4. 生成したオブジェクトを return
        return self
        
        """
        # 請注意
        # デコレータを使うと省略記法では書けません
        self = super().__new__(cls, *args)
        """

immutable_classdecrator.py

3. メタクラス(namedtuple を使う)

typing.NmaedTuple の実装で使われています。

from typing import NamedTuple

class Employee(NamedTuple):
    name: str
    id: int = 3

employee = Employee('Guido')
assert employee.id == 3

標準ライブラリ - 26.1. typing
cpython/Lib/typing.py - GitHub

4. メタクラス(namedtuple を使わない)

namedtuple があるのに namedtuple を使わないで immutable にするメタクラスを自作してしまいました...

class Region(metaclass=Immutable):
    __slots__ = (
        'x1', 'y1', 'x2', 'y2',
        'is_rectangle', 'is_line', 'is_dot')
         
        def __new__(cls, x1, y1, x2, y2):
            width_0 = (x1 - x2 == 0)
            height_0 = (y1 - y2 == 0)
            is_rectangle = (not width_0 and not height_0)  # 0 0
            is_line = (width_0 != height_0)  # 0 1 or 1 0; xor
            is_dot = (width_0 and height_0)  # 1 1
            args = (x1, y1, x2, y2, is_rectangle, is_line, is_dot)
        
            # メタクラスで定義されたクラスメソッドを読んでインスタンス化
            self = cls.new(*args)

            return self

immutable_metaclass.py


なぜ自作したかというと、namedtuple の実装が少し変だったからです。どう変かというとクラス定義時に文字列を使って実装していました。

文字列でコードを書いて実装させるなんてことは、リファクタリングをしたりするときに結構問題を起こすような気がするのでよくないと思い込んでいたのですが...

自分でも実装してみてわかったのですが、これはおそらく高速化のためにそうしています。

namedtuple を使わずに作っては見たものの、インスタンス化が重すぎる orz 1 回生成したオブジェクトにつき属性参照を 20 回以上やるようなケースではメタクラスで作ったものの方が速くはなるけど..

1つのオブジェクトに対して 20 回以上も属性参照するようなことってあるのやろか.. 文字列からクラスを生成するという裏技まで使ったけど、これ以上は速くできそうにない。

基本的な考え方は、一旦 mutable なクラスからインスタンス化して、そのあと immutable なクラスにキャストするということをしています。

# インスタンス
namedtuple: 0.5539945139989868
metaclass: 0.9362985030002164

# 属性参照
namedtuple: 0.07781907100070384
metaclass: 0.04243788100029633

まとめ

メタクラス自体の応用範囲は、色々と広いそうです。 自分は「クラス定義時に実行させたい共通の処理があったらに使う」くらいの理解しか、まだできていません。

メタクラスは限りない潜在的利用価値を持っています。 これまで試されてきたアイデアには、 列挙型、ログ記録、インタフェースのチェック、 自動デリゲーション、 自動プロパティ生成、プロキシ、フレームワーク、そして自動リソースロック/同期といったものがあります。
3.3.3.6. メタクラスの例 - Python 言語リファレンス