はじめに
普通のPythonのclassはフィールドが定義された順番を持たないけど、それが知りたい場合メタクラスで実現できると @crohaco 先生が教えてくれた。
メタクラスは基本的に使わない方がいいけど、こういう特殊な事をするにはメタクラスによってクラスを拡張する方法しか無いらしい。 せっかく教えてもらったので忘れないようにメモ(間違い等あればコメント下さい)。
2016-01-07 追記
@crohaco 先生がメタクラスについて記事書いてくれた
2016-09-30 追記
tell-k先生のPyCon JPの発表がとても分かりやすかった。この記事よむよりこの動画見ましょう。
https://www.youtube.com/watch?v=807SjfhRuUY
2016-12-23 追記
Python 3.6では、今回のことがもっと簡単に行えそう。
https://docs.python.org/3.6/whatsnew/3.6.html#pep-487-simpler-customization-of-class-creation
2020-08-29 追記
露木さんのPyCon JP 2020のセッションもとてもわかりやすかったです。この記事で扱ったフィールドの定義順の保持の仕方だけでなく、メタクラスの用途を網羅的に扱ってくれています。
このセッションで言われているように、Python 3.6からdictが順序を保持する実装に変わり、3.7から仕様化されたためこの記事で扱った内容はもうPython2.7とPython 3.5が無くなりつつある今となっては必要ありません。
メタクラス
Pythonでは型を調べるのにtype
を使ったりしてますが、type
はPythonの全てのクラスを生成するのに使われているものらしいです(参考: oop - What are metaclasses in Python? - Stack Overflow)。
ドキュメントによると、type
の引数は class type(name, bases, dict)
。
type
を使って以下のようにクラスを動的に生成することができます。
# type(name, bases, attrs) で動的に生成。型は (str, tuple, dict)。 >>> type("Hoge", (), {}) <class '__main__.Hoge'> # 静的に生成するならこうかな >>> class Hoge(type): ... def __init__(self, name, bases, attrs): ... super(Hoge, self).__init__(name, bases, attrs) ... >>> Hoge <class '__main__.Hoge'>
colanderのソースコードを読む
colanderっていうvalidationライブラリがある。 colanderの使用例は以下(参考: [Python] colander備忘録 - くろのて)。
>>> import colander >>> class User(colander.MappingSchema): ... id = colander.SchemaNode(colander.Int(), missing=None) ... name = colander.SchemaNode(colander.Str()) ... age = colander.SchemaNode(colander.Int())
colanderは id
, name
, age
と定義された順番を把握しているとのこと。
内部的にはメタクラスを使ってそうなので、colanderのソースコード を見ながら、どのように id
, name
, age
の定義された順番が保持されているのか見てみる
SchemaNode
クラスと MappingSchema
クラス
上の例で使われていた2つのクラスを探した。
# https://github.com/Pylons/colander/blob/master/colander/__init__.py#L2181 SchemaNode = _SchemaMeta( 'SchemaNode', (_SchemaNode,), {'__doc__': _SchemaNode.__doc__} ) class Schema(SchemaNode): schema_type = Mapping MappingSchema = Schema
_SchemaMeta
というメタクラスを使って _SchemaNode
をベースとした SchemaNode
という名前のクラスを生成しているらしい。
この2つのクラスのコードを読む必要がありそう。
_SchemaNode
クラス
長いので何行か抽出しました。
# https://github.com/Pylons/colander/blob/master/colander/__init__.py#L1781 class _SchemaNode(object): _counter = itertools.count() def __new__(cls, *args, **kw): node = object.__new__(cls) node._order = next(cls._counter) node.children = [] _add_node_children(node, cls.__all_schema_nodes__) return node # https://github.com/Pylons/colander/blob/master/colander/__init__.py#L1781 def _add_node_children(node, children): ...
オブジェクトを生成する前に呼ばれる __new__
のところで _counter
という属性に itertools.count()
を使って呼び出された順番を保持している。itertools.count
は以下のように動作する。
>>> counter = itertools.count() >>> next(counter) 0 >>> next(counter) 1
オブジェクトが生成された順番を記憶し、object.__new__(cls)
で生成したオブジェクトの _order
フィールドに格納しています。
_SchemaMeta
クラス
# https://github.com/Pylons/colander/blob/master/colander/__init__.py#L2158 class _SchemaMeta(type): def __init__(cls, name, bases, clsattrs): nodes = [] for name, value in clsattrs.items(): if isinstance(value, _SchemaNode): delattr(cls, name) if not value.name: value.name = name if value.raw_title is _marker: value.title = name.replace('_', ' ').title() nodes.append((value._order, value)) nodes.sort() cls.__class_schema_nodes__ = [ n[1] for n in nodes ] # Combine all attrs from this class and its _SchemaNode superclasses. cls.__all_schema_nodes__ = [] for c in reversed(cls.__mro__): csn = getattr(c, '__class_schema_nodes__', []) cls.__all_schema_nodes__.extend(csn)
clsattrs.items()
で定義された属性の名前と変数を取り出します。上の例ならclsattrsは以下のようになるはず。
{ id: colander.SchemaNode(colander.Int(), missing=None), name: colander.SchemaNode(colander.Str()), age: colander.SchemaNode(colander.Int()) }
Pythonの辞書型は順序を持たないですが、value
に入っている_SchemaNode
にはorder
フィールドは先程確認したように定義された順番を保持しています。nordes
リストに(value.order, value)
をappendしてからsortすることでnodes
リストには、nodes.sort()
によってソートされた_SchemaNode
クラスのオブジェクトが格納されるようです。
自分でも簡単な例を書いてみる
gistに張りました
Pythonのメタクラスを使ってフィールドが定義された順番を保持する
おわりに
とりあえず、どうやってフィールドの定義された順番を保持しているのか分かった気がする。 ふぅ...疲れた pandas_validatorでも必要そうなら実装します。