2016-01-07

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

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

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

あ、滑ってますか?そろそろやめますね。

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

info
  • この記事は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の書き方 - Qiitapython2.xとpython3.xでmetaclassを利用する構文が異なっている。一方でひとつのファイルで両方のバージョンをサポートしたいことがある。そのような場合の書き方について。me…https://qiita.com/podhmo/items/c601050b20f70d27aa07

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

これは以前カレーメシ先輩が記事にしてくれましたが、少しだけ補足しようと思います。 クラス内のフィールドが定義された順番を保持する(Python・メタクラス) - c-bata webcolanderはメタクラスを作って `id`, `name`, `age` と定義された順番を把握しているとのこと。 [colanderのソースコード](https://github.com/Pylons/colander/blob/master/colander/__init__.py) を見ながら、どのように `id`, `name`, `age` の定義された順番が保持されているのか見てみるhttps://nwpct1.hatenablog.com/entry/meta-classes-in-python

スキーマやフォームなど定義されたフィールドの順番が重要な意味を持つ場合があります。 例えば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')

まとめ

割と便利なメタクラスですが、通常の用途で必要になるケースは滅多にありません。むやみに使えばレビューで突き返されること請け合いです。

なくても大抵の場合は代替手段があったり、普通使わなくても対応できるからです。

そういう意味では使い勝手が重視されるライブラリでは利用されるかもしれません。 ライブラリを作りたいと思っている方は知っておいて損はない技術だと思います。

頭の片隅に入れておいて必要になったら試してみましょう。