38
53

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Pythonのクラスアトリビュートとメタクラスの話

Last updated at Posted at 2017-01-17

問題

class Base:
    __BASE_PARAMS__ = ["base"]


class A(Base):
    __PARAMS__ = ["a"]

    
class B(Base):
    __PARAMS__ = ["b"]

上記のようにBaseクラスがあり、AクラスとBクラスはBaseクラスを継承している。
AクラスとBクラスに、親クラスの__BASE_PARAMS__と自身の__PARAMS__を統合した__ALL_PARAMS__というクラスアトリビュートを追加したい。つまり、以下のような結果を得たいとする。

A.__ALL_PARAMS__
# ['base', 'a']

B.__ALL_PARAMS__
# ['base', 'b']

普通に実装したら、以下のようになるだろう。

class Base:
    __BASE_PARAMS__ = ["base"]


class A(Base):
    __PARAMS__ = ["a"]
    __ALL_PARAMS__ = Base.__BASE_PARAMS__ + __PARAMS__

    
class B(Base):
    __PARAMS__ = ["b"]
    __ALL_PARAMS__ = Base.__BASE_PARAMS__ + __PARAMS__


A.__ALL_PARAMS__
# ['base', 'a']

B.__ALL_PARAMS__
# ['base', 'b']

しかし、Baseクラスを継承するクラスは多数あり、すべての継承先のクラスに処理とは無関係な__ALL_PARAMS__ = Base.__BASE_PARAMS__ + __PARAMS__というクラスアトリビュートを書くとなると面倒である。

継承先のクラスに無意味な記述をさせずに、簡潔にこの問題を解決する方法はないのだろうか?
皆さんも考えてみてほしい。

メタプログラミング

メタプログラミングとは、プログラムでプログラムを定義する技法である。

ここではメタプログラミンの基本的な話はしない。メタプログラミングの事が知りたい場合は以下の記事などを参考にしてほしい。

Pythonでメタプログラミング
__new__と__init__とメタクラスと
pythonのメタクラスで、ORMのメソッドに比較演算子を文字列として渡して、where句を組み立てる。

さて、メタクラスを使用すると、先程の問題を以下のように解決することができる。

Python3用メタクラス
class MetaClass(type):    
    def __init__(cls, name, bases, attribute):
        super(MetaClass, cls).__init__(name, bases, attribute)
        cls.__ALL_PARAMS__ = cls.__BASE_PARAMS__ + getattr(cls, "__PARAMS__", [])


class Base(metaclass=MetaClass):
    __BASE_PARAMS__ = ["base"]


class A(Base):
    __PARAMS__ = ["a"]

    
class B(Base):
    __PARAMS__ = ["b"]


A.__ALL_PARAMS__
# ['base', 'a']

B.__ALL_PARAMS__
# ['base', 'b']

Python2の場合は特殊なアトリビュート__metaclass__にメタクラスを代入する。

Python2用
class Base(object):
    __metaclass__ = MetaClass

メタプログラミングいろいろ

メタプログラミングを使えば言語を拡張するような事もできる。
JavaにあるFinalクラスはPythonには存在しないが、メタクラスを使えば同じような機能を定義することができる。

final_metaclass.py
class FinalMetaClass(type):
    def __init__(cls, name, bases, namespace):
        super(FinalMetaClass, cls).__init__(name, bases, namespace)
        for _class in bases:
            if isinstance(_class, FinalMetaClass):
                raise TypeError()


class A:
    pass


class B(A, metaclass=FinalMetaClass):
    pass


# Error!!
class C(B):
    pass

__init__か__new__か?

typeを継承したメタクラスで、__init____new__のどちらを使えば良いのだろうか?
基本的に好みの問題で、どちらでもよい。
ただし__new__の方が__slots__を書き換える事ができるなど自由度が高い。

新しい特殊メソッド __prepare__

Python3から特殊メソッド__prepare__が追加されている。
通常、__dict__は順序が保証されていないdict型である(Python3.6ではどうなったのだろうか)、__prepare__でこれを制御することができる。

以下、Pythonドキュメントからの抜粋である。

import collections

class OrderedClass(type):

    @classmethod
    def __prepare__(metacls, name, bases, **kwds):
        return collections.OrderedDict()

    def __new__(metacls, name, bases, namespace, **kwds):
        cls = type.__new__(metacls, name, bases, dict(namespace))
        cls.members = tuple(namespace)
        return cls

class A(metaclass=OrderedClass):
    def one(self): pass
    def two(self): pass
    def three(self): pass
    def four(self): pass
    
A.members
# ('__module__', '__qualname__', 'one', 'two', 'three', 'four')

クラスメンバの列挙が定義順になっている事が確認できる。

メタクラスの継承

class MetaA(type):
    def __new__(mcls, *args, **kwargs):
        cls = type.__new__(mcls, *args, **kwargs)
        cls.MetaA = "Yes"
        return cls


class MetaB(type):
    def __new__(mcls, *args, **kwargs):
        cls = type.__new__(mcls, *args, **kwargs)
        cls.MetaB = "Yes"
        return cls        


class A(metaclass=MetaA):
    pass


class B(A, metaclass=MetaB):
    pass

Bクラスを定義しようとすると、以下のエラーが出る。

TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

metaclassに渡せるのはtypeのサブクラスだけではない。
特定に引数に対応したCallableなものであれば何でも良い、この場合は関数を渡してダイヤモンド継承を解消すればよい。

class MetaA(type):
    def __new__(mcls, *args, **kwargs):
        cls = type.__new__(mcls, *args, **kwargs)
        cls.MetaA = "Yes"
        return cls


class A(metaclass=MetaA):
    pass


def MetaB(mcls, *args, **kwargs):
    cls = type(mcls, *args, **kwargs)
    cls.MetaB = "Yes"
    return cls
    

class B(A, metaclass=MetaAB):
    pass

B.MetaA, B.MetaB
# ('Yes', 'Yes')

メタプログラミングの使い所

メタプログラミングは、言語仕様を変えることができるほど強力なものである。
それゆえに、メタプログラミングの多用はチームプログラミングにおいて混乱の元となりえる。

基本的には設計がしっかりしていれば、メタプログラミングの出番は殆どないはずである。
例外的に継承先のクラスアトリビュートを生成する場合、メタクラスを使うとスッキリと書ける場合が多い。

参考

38
53
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
53

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?