[Python] メタクラスをたおした

新年、あけましておめでとうございます。さっそくですがメタクラスやりましょう。

え、メタグロスじゃないですよ。何言ってるんですか?メタクラスはコメットパンチとか覚えませんし、はかいこうせんもできません。
あ、滑ってますか?そろそろやめますね。

メタクラスとはクラスを拡張するために有用な手段です。インスタンスの拡張ではなくクラス自体の拡張です。

注意: この記事はPython3以上を前提としています。2系でもほとんど同じですが旧クラス(classobj)とか考えたくないので。

基本

少し基本的なメタい話をします。Pythonではすべてがオブジェクトという扱いでしたね。
インスタンスはもちろん、インスタンスのもとになるクラスもオブジェクトです。そして、クラスはtypeのインスタンスです。ここ大事。

あまり知られていないのかもしれませんが、typeビルトイン関数は型を調べるほかにクラスオブジェクトのインスタンス作成(コンストラクタ)としても利用されます。
詳細はリンク先を参照いただくのがよろしいかと。

とはいえ、動作くらいは見ておきましょう。

>>> class A(object):
...     test = 1
 
>>> # 上記クラス「A」をtype関数で表現すると以下のようになる
>>> A = type('A', (object,), {'test': 1})
 
>>> # クラス名, 継承クラス, 属性を引数として渡すとクラスオブジェクトを返却する
>>> a = A()
>>> a.test
1

簡単にまとめると

  • class文はtype呼び出しのシンタックスシュガー
  • typeはデフォルトのメタクラス

です。
これらの情報を踏まえ、どのようにメタクラスを指定するのかを見ていきましょう。これからが本当の地獄だ(AA略)。

応用

クラスオブジェクトの__setattr__を拡張したい

Aクラスインスタンスの__setattr__を拡張するのであれば以下のように記述します。(これはメタクラスではありません)

>>> class A(object):
...     """例えばこんなふうに代入時に画面出力するとしよう"""
...     def __setattr__(self, name, value):
...         self.__dict__[name] = value
...         print('{}属性に{}を設定しました'.format(name, value))
 
>>> a = A()
>>> # __setattr__を拡張するとインスタンスへの代入を制御できる
>>> a.test = 1
test属性に1を設定しました
>>> a.test
1
>>> A.test = 1

このやり方ではAインスタンスの__setattr__は制御できても、Aクラス自体の__setattr__は制御できません。

Aクラスがどのように動作してほしいかを定義したクラスをメタクラスとして指定する必要があります。そこで以下のように記述します。

>>> class MetaClass(type):
...     def __setattr__(cls, name, value):
...         super(MetaClass, cls).__setattr__(name, value)
...         print('{}属性に{}を設定しました'.format(name, value))
 
>>> # 定義時にmetaclass引数を指定する
>>> class A(object, metaclass=MetaClass):
...     pass
 
>>> A.test = 1
test属性に1を設定しました

ちなみにmetaclass引数を指定できるのはPython3系以降で、Python2系では__metaclass__属性にクラスを指定します。

>>> class MetaClass(type):
...     def __setattr__(cls, name, value):
...         super(MetaClass, cls).__setattr__(name, value)
...         print('{}属性に{}を設定しました'.format(name, value))
 
>>> class A(object):
...     __metaclass__ = MetaClass
 
>>> A.test = 1
test属性に1を設定しました

バージョンによる記述方法の差異を吸収するためには、class構文を使わずに書くかsixライブラリを使うのがよいでしょう。

>>> class MetaClass(type):
...    def __setattr__(cls, name, value):
...        super(MetaClass, cls).__setattr__(name, value)
...        print('{}属性に{}を設定しました'.format(name, value))
 
>>> A = MetaClass('A', (object,), {})
>>> A.test = 1
test属性に1を設定しました
 
>>> # six.with_metaclassを使う
>>> from six import with_metaclass
>>> class A(with_metaclass(MetaClass, object)):
...     pass
 
>>> A.test = 1
test属性に1を設定しました
 
>>> # six.add_metaclassを使う
>>> from six import add_metaclass
>>> @add_metaclass(MetaClass)
... class A(object):
...     pass
 
>>> A.test = 1
test属性に1を設定しました

sixの内部は__new__が指定されたクラスのファクトリとなっているようです。

参考リンク:python2 python3両方に対応できるmetaclassの書き方

クラスに定義された属性の順番を知る

これは以前カレー飯先輩が記事にしてくれましたが、少しだけ補足しようと思います。
スキーマやフォームなど定義されたフィールドの順番が重要な意味を持つ場合があります。例えばDjangoのmodelやcolanderのTupleSchemaなどです。

こういった場合、属性値に順番となる数値を何らかの形で持たせ、クラスオブジェクトが完成したら属性をソートするのが一般的です。
ただし、統一された形式で属性が指定されている必要があります。
例えば、Djangoであれば、以下のようにField関連のインスタンスが指定されている必要があります。

from django.db import models
 
class Book(models.Model):
    title = models.CharField(max_length=255)
    isbn13 = models.CharField(max_length=13)
    price = models.IntegerField(null=False)

Fieldクラスインスタンス作成時にインクリメントすることでその順番を保証します。colanderも同様です。
もし、単なる数値(int)や文字列(str)の順番を保証したい場合はクラスでラップする等の対応が考えられますが少々冗長ですよね。

python3.3からは __prepare__というクラスメソッドが用意され、このような泥臭いことをせずとも実現できるようになりました。やったぜ。

>>> from collections import OrderedDict
>>> class OrderedClass(type):
...     @classmethod
...     def __prepare__(cls, *args, **kwargs):
...         # 辞書の代わりにキー作成順を記憶するOrderedDictを返却する
...         return OrderedDict()
...
...     def __new__(cls, name, bases, namespace, **kwargs):
...         result = type.__new__(cls, name, bases, dict(namespace))
...         # membersに定義された属性を順に入れたい
...         result.members = tuple(namespace)
...         return result
 
>>> class A(metaclass=OrderedClass):
...     def one(self): pass
...     def two(self): pass
...     def three(self): pass
...     def four(self): pass
...     five = 5
...     six = 6
...     seven = 7
...     eight = 8
...     nine = 9
 
>>> # A.membersに属性名が順に格納されている
>>> A.members
('__module__', '__qualname__', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine')

まとめ

割と便利なメタクラスですが、通常の用途で必要になるケースは滅多にありません。むやみに使えばレビューで突き返されること請け合いです。
なくても大抵の場合は代替手段があったり、普通使わなくても対応できるからです。

使い勝手が重視されるライブラリでは利用されるかもしれませんね。ライブラリを作りたいと思っている方は知っておいて損はない技術だと思います。
頭の片隅に入れておいて必要になったら試してみましょう。