python/3 の class/metaclass のDSL的応用


別記事にも書いたが、python2/3では、「class定義」は厳密には、typeクラスインスタンスの作成であり、「いわゆるインスタンス生成」に見えるものは、typeクラスの実行である。

逆説的に、「class定義を流用して「classでないもの」を作ることができる」たとえば、そのひとつが、enumである。

lib/enumの場合

話のとっかかりとしては、適当な実装例があったほうが都合がよいので、オフィシャルのドキュメントを使わせてもらう。

8.13. enum — 列挙型のサポート — Python 3.5.4 ドキュメント

python3.5からenumが追加された。とここでは述べている。のだが、実際に試してみると、表面上はclass定義にしか観えない。例えば上記ドキュメントには次のようなソースが載ってる。

class Color(Enum):
     red = 1
     green = 2
     blue = 3

Colorは、この定義単体で完結しており、コンストラクタを持たないし、呼ぼうとしても機能しない。その意味では「クラス」ではない。しかしpython3の構文上はどう見てもclass定義である。

しかもColor.redには整数1を定義したはずが、使う時点ではオブジェクトになっている。

リバースエンジニアリングしてみる

jupyter-notebook等で、適当なenumを作成して、そのインスタンスをダンプしてみると面白いことがわかる。

まずは適当に定義してみる。

from enum import Enum,auto
class httpmethod(Enum):
    HEAD=0
    GET=auto()
    POST=auto()
    PUT=auto()
    DELETE=auto()

そしてそれをダンプする。

httpmethod.__dict__

すると、定義より遥かに大量の情報が現れる。

mappingproxy({'_generate_next_value_': <function enum.Enum._generate_next_value_(name, start, count, last_values)>,
              '__module__': '__main__',
              '__doc__': 'An enumeration.',
              '_member_names_': ['HEAD', 'GET', 'POST', 'PUT', 'DELETE'],
              '_member_map_': {'HEAD': <httpmethod.HEAD: 0>,
               'GET': <httpmethod.GET: 1>,
               'POST': <httpmethod.POST: 2>,
               'PUT': <httpmethod.PUT: 3>,
               'DELETE': <httpmethod.DELETE: 4>},
              '_member_type_': object,
              '_value2member_map_': {0: <httpmethod.HEAD: 0>,
               1: <httpmethod.GET: 1>,
               2: <httpmethod.POST: 2>,
               3: <httpmethod.PUT: 3>,
               4: <httpmethod.DELETE: 4>},
              'HEAD': <httpmethod.HEAD: 0>,
              'GET': <httpmethod.GET: 1>,
              'POST': <httpmethod.POST: 2>,
              'PUT': <httpmethod.PUT: 3>,
              'DELETE': <httpmethod.DELETE: 4>,
              '__new__': <function enum.Enum.__new__(cls, value)>})

この中で定義した覚えがあるのは後半の数個だけだが、色々便利そうな値が追加されているのがわかるだろう。

  • _member_names_ 定義の文字列名の列挙
  • _member_map_ 名前→定数インスタンスへの参照表
  • _value2member_map_ 定数インスタンスから数値への参照表

ではenum.Enumは一体なにをやったのだろうか?

/usr/lib/python/enum.py の一部を抜粋する

class EnumMeta(type):
class Enum(metaclass=EnumMeta):
    """Generic enumeration.

前述のクラスが直接継承してるのはEnumだが、そのEnumには更にmetaclass=EnumMetaという記述があり、EnumMetaクラスはtypeから継承されてる。
このmetaclassという名前付きパラメタが肝なのだが、実はclass定義時にはmetaclassパラメタを毎回使うことができる。

しかもデフォルトはtypeだったりする。
これこそ通常は「class定義をするとtypeインスタンスができる」所以である。

class定義時の、EnumMeta.__new__ が介入して、class定義の中身を加工する。
githubのソースから容易に参照できる。先の表群を作ってるのもわかるだろう。

cpython/enum.py at 3.9 · python/cpython

また auto() の列挙数展開もここで行われている。

多言語信者から見れば、「イケてない」と言われることが多いが、実に簡素な機能で、多様性を実現してるのには驚きというほかない。