Pythonの動的クラス生成と特殊メソッドとフレームの謎¶
先日、とある事情からクラス定義を動的に生成する必要があったのですが、そこでおかしな現象にはまってしまい、今もまだ解決出来ていません。
注釈
9/14 2:00 追記: この投稿の内容はWindowsのPython2.6.4, 2.7, FreeBSDのPython2.4.4で試しました。
9/14 12:20 追記: 解決しました.... 勘違いした部分に追記入れておきます
動的なクラス生成¶
クラス定義を動的に生成するのは結構簡単にできます。例えば以下のように静的に定義して使う例があるとして、
>>> class Foo(object):
... def foo(self, a):
... return a
...
>>> f = Foo()
>>> f.foo('hoge')
'hoge'
これと同じことを以下のように書けます。
>>> attrs = {
... 'foo': lambda self, a: a
... }
>>> Foo = type('Foo', (object,), attrs)
>>> f = Foo()
>>> f.foo('hoge')
'hoge'
ここまではtype()の使い方の一つとして知っていれば、詳しい原理などを知らなくても、まあ問題無く使える気がします。
特殊メソッド¶
もう一つ、本題に入る前に特殊メソッドの使い方の例。例えばあるクラスに__len__というメソッドを実装してその動きを見てみます。
>>> class Bar(object):
... def __len__(self):
... return 10
...
>>> b = Bar()
>>> len(b)
10
>>> b.__len__()
10
len(b)で10という値が返ってきているし、__len__()を直接呼び出しても10が返ってきます。でも、len()関数がオブジェクトの__len__()メソッドを呼んでる、のでは無いところには注意。表現としては「len()アダプタはその内部で、対象オブジェクトと__len__プロトコルで通信して10という結果を返している」と書いた方が良いと思います。
ちょっと脱線ですが、試しに以下のように書いてみます。
>>> class Bar2(object): pass
...
>>> b2 = Bar2()
>>> b2.__len__ = lambda: 10
>>> b2.__len__()
10
>>> len(b2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: object of type 'Bar2' has no len()
上記エラーメッセージを見ると、Bar2という型はlen()を持っていないというエラーが出ているので、インスタンスではなくクラスに__len__を後付けしてみます。
>>> class Bar3(object): pass
...
>>> b3 = Bar3()
>>> len(b3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: object of type 'Bar3' has no len()
>>> Bar3.__len__ = lambda self: 10
>>> len(b3)
10
クラスに特殊メソッドを後付けしてもちゃんと動作する事が分かりました。
ここからが本題¶
先の2つの話を組み合わせて、以下のように動的に特殊メソッドを持つクラスを生成します。これはうまく動くので、クラス生成する関数をgen_safe()という名前にしました。
>>> d = {
... '__len__': 10,
... '__str__': 'va-',
... }
...
>>> def gen_safe():
... attrs = {}
... attrs['__len__'] = lambda self: d['__len__']
... attrs['__str__'] = lambda self: d['__str__']
... return type('Gen', (object,), attrs)
...
>>> Gen = gen_safe()
>>> g = Gen()
>>> str(g)
'va-'
>>> len(g)
10
期待通りに動作したので、次に冗長なコードを最適化してみます。でもうまく動かなくなってしまったので、クラス生成関数をgen_fail()という名前にしました。
>>> d = {
... '__len__': 10,
... '__str__': 'va-',
... }
...
>>> def gen_fail():
... attrs = {}
... for name in ('__len__', '__str__'):
... attrs[name] = lambda self: d[name]
... return type('Gen', (object,), attrs)
...
>>> Gen = gen_fail()
>>> g = Gen()
>>> str(g)
'va-'
>>> len(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: an integer is required
attrs
を作成する処理をforループに書き換えたら動かなくなってしまいました。ここで内部的にはlen(g)した時点でgと__len__プロトコルで通信しているわけですが、その結果len()内部で 'va-'
という文字列を受け取ってしまい、__len__プロトコルで受け取る値は数値型であるという条件チェックにひっかかって TypeError: an integer is required
エラーになっている事が分かりました。でも,,,
>>> g.__len__()
10
上記のコードはエラーにならないんですよね。謎は深まるばかりです。
注釈
9/14 12:20 追記: 上記は勘違いです。g.__len__()は'va-'を返します。 色々やっているうちに混乱していたようで… 謎は深まりませんでした。
ところで、先日の エキスパートPythonプログラミング読書会02 で、内包表記で閉じ込められた変数が属しているスタックはどこまで持って行かれるのか、という話が出ていたのに対して、@atsuoishimoto さんが 「スタックってか、フレームオブジェクトが保存される。」 とコメントしてくれていたことから、以下のように書き換えることを思いつきました。
... for name in ('__len__', '__str__'):
... attrs[name] = lambda self, __name=name: d[__name]
nameの値をlambda定義の外から渡すことでフレームオブジェクトを保存しないようにしてみようと思ったわけですが……、なんと!これで期待通りに動いてくれました!
いやー、これで無事解決です。よかったー!
……解決なわけ無いですね。引数有りのメソッドに対応出来ないし、そもそも根本解決してない。
と言うことで解決してません。解決するにはフレームオブジェクトを色々操作して頑張るしかないの?やだなー。
注釈
9/14 12:20 追記: コメントの方で「もう一段,関数でwrapすればよい」という指摘のもと、 解決することが出来ました。結局の所、以下の挙動を理解していればこの問題にはまることも 無かったと思います。
>>> funcs = {}
>>> for name in ('foo', 'bar', 'baz'):
... funcs[name] = lambda: name
...
>>> for n,f in funcs.items():
... print n, f()
...
baz baz
foo baz
bar baz