問題
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句を組み立てる。
さて、メタクラスを使用すると、先程の問題を以下のように解決することができる。
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__
にメタクラスを代入する。
class Base(object):
__metaclass__ = MetaClass
メタプログラミングいろいろ
メタプログラミングを使えば言語を拡張するような事もできる。
JavaにあるFinalクラスはPythonには存在しないが、メタクラスを使えば同じような機能を定義することができる。
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')
メタプログラミングの使い所
メタプログラミングは、言語仕様を変えることができるほど強力なものである。
それゆえに、メタプログラミングの多用はチームプログラミングにおいて混乱の元となりえる。
基本的には設計がしっかりしていれば、メタプログラミングの出番は殆どないはずである。
例外的に継承先のクラスアトリビュートを生成する場合、メタクラスを使うとスッキリと書ける場合が多い。
参考