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

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

Google のコーディング規約には、可読性を低くしてしまうので、 使わない方が良いよ、と書かれているので、 そこまで無理して覚える機能でもないかなと思ったりもします。

強力な機能
このような機能は避ける。

定義:
Python はとても柔軟な言語であり、  メタクラス  、バイトコードへのアクセス、高速コンパイル、動的な継承、オブジェクトの親の変更、 インポートハック、リフレクション、内部システムの変更 など多くの素敵 (変態的) な機能があります。

1. メタクラス

例えばクラス定義時に 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(self, name, bases, name_space):
    print('Hello, world!')

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

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

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

type 関数は引数が1つのときは型を判定に使いますが、 引数が3つのときはクラスを作成するのに使います。 引数は次のような具合です。

Meta = type('Meta',  (type, ),     {'__init__': init})
Meta = type(クラス名, 継承するクラス, クラス変数とメソッド)

ここでのポイントはクラスを作るのに必要なものは、 「クラス名」、「継承するクラス」、「クラス変数とメソッド」の3つだけ ということがわかります。

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

ここで大事なのは、「クラス定義」と 「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

2. クラスデコレータ

例えばクラス定義時に 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!
>>> 

クラスデコレータは、メタクラスよりも簡単に書けそうですね。

3. 使い分けはどうしたらいいの?

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

例えば 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].

基本的に継承より合成の考えに即して、 クラスデコレータでできるときはクラスデコレータで、 継承しないと厳しいときだけメタクラスを使うのかなと考えています。

とは言えメタクラスを使ってもメタクラスを使っても、 動作的にはほぼ変わりはない気はするのですが... よくわかっていません。

4. 実用例

メタクラス、クラスデコレータは特殊属性 __new__ と仲良しです。 __new__ については、なんとなく押さえておいていただければと思います。

◯ immutable なクラスを作る。

クラスデコレータ immutable なオブジェクトを生成するクラスオブジェクトを作りたいと思います。

1. namedtuple をそのまま使う

namedtuple を使うと immutable なオブジェクトを生成するクラスを作ることができます。 直接、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:
    # 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 - GitHubGist

Python の super は super 難しい... いまだにあんまり理解できていない...

◯ 画像スクレイピング

こんな感じで URL を指定するとサムネイルの URL を元に本体の画像をダウンロードするスクリプトを書きました。 対応しているのは Wikipedia と 2ch のまとめサイト1つだけです。

$ python scrape_image.py https://pt.wikipedia.org/wiki/Veneza
scrape_image.py - GitHubGist

拡張の仕方は Site を継承し、以下の3つを定義したクラスを用意します。

  1. サイトの URL
  2. サムネイル画像の URL の正規表現
  3. サムネイル画像の URL から本体画像の URL に変換する関数

サイトを登録するような作業はメタクラスがしてくれるので不要です。

# Wikipedia の例
class Wikipedia(Site):
    # 1) サイトの URL
    site_url = 'ja.wikipedia.org'
    
    # 2) サムネイル画像の正規表現
    thumnail_url_regex = (
        r'upload\.wikimedia\.org'
        r'/wikipedia/commons/thumb/\w+/\w+/.*?\.(jpg|png)'
    )

    # 3) サムネイルから本体画像へ変換する関数
    @staticmethod
    def make_image_url(thumbnail_url):
        return 'https://' + thumbnail_url.replace('/thumb', '')

この例だとメソッドを共有した方がコードが綺麗にまとまるのでクラスデコレータではなくメタクラスで書きました。 メタクラスと __new__ を使っています。

Site クラスはメタクラス SiteRegister を継承しています。

class Site(metaclass=SiteRegister):

そのため Wikipedia クラスが定義されると、 自動的に SiteRegister クラスの __init__ が呼び出されて クラス変数 site_dispatch という辞書に登録されます。

class SiteRegister(abc.ABCMeta):
    site_dispatch = {}

    def __init__(self, name, bases, name_space):
        self.site_dispatch[self.site_url] = self
        self.site_name = name.lower()

Site クラスをインスタンス化したときに、

def scrape_image(page_url):
    # 1. Instantiate a site object.
    page = Site(page_url)

Site クラスの __new__ が呼ばれます。 このとき与えられた URL, page_url から対応する Site クラスを継承した子クラスを site_dispatch から取り出して、 インスタンス化します。

class Site(metaclass=SiteRegister):
    def __new__(cls, page_url):
        site_url = cls._make_site_url(page_url)
        if site_url in cls.site_dispatch:
            ConcreteSite = cls.site_dispatch[site_url]
            return super().__new__(ConcreteSite)
        else:
            raise Exception(f'{site_url} is not registered.')

Site クラスをインスタンス化すればいいだけという感じになって使いやすい雰囲気もあるのですが、 いざ一旦実装を読み解こうとすると、なんだか説明が煩雑になってしまいます。

説明が難しいのなら、その実装は良くないということ
If the implementation is hard to explain, it's a bad idea.
The Zen of Python - PEP 20

またここでの問題は、インスタンス化したと思ったクラスの、 子クラスのインスタンスが返されてしまいます。

def scrape_image(page_url):
    # Site クラスではなく 
    # Wikipedia クラスのインスタンスが返される。
    page = Site(page_url) 

親クラスのメソッドが呼び出されるところでは、 子クラスのメソッドも使えないといけない。 というリスコフの置換則を守っていれば問題ないですが、 こんなことしていいのかなという疑問もあります。

使い方次第だとは思うのですが、 Google のコーディング規約がメタクラスに対して否定的なのも なんとなくわかる気がします。

手堅く一旦、辞書からクラスオブジェクトを取り出してから、 インスタンス化するコードにしようかなとも思ったのですが

def scrape_image(page_url):
    # 一旦辞書から取り出してから
    ConcreteSite = SiteRegister.site_dispatch(page_url)

    # インスタンス化
    page = ConcreteSite(page_url) 

ここでは __new__ と メタクラスをフルに使う例をご紹介させていただきました。

5. まとめ

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

Google のコーディング規約とは対照的に、 Python 言語リファレンスでは好意的に評価されています。

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

Last Updated: 5/24/2019, 2:47:11 AM