Python3 とクラスと super
Python3 では super() を呼び出す際に引数を省略できる。
例えば以下のように書くと、 super() は super(Derived, self) という呼び出しと等価になるらしい。
class Derived(Base): def __init__(self): super().__init__()
そもそも明示を是とする Python で引数が省略できてしまうってのは気持ち悪いというのはかなりあるのだけども、どのように解決しているのだろうかなどと気になってしまったので調べてみた。
実験
引数の所在
そもそも super に渡す引数はどこから取ってくるのか。
第二引数はメソッドの第一引数でいいとして、第一引数の解決をどうやっているのか。
以下のコードで調べてみた。
class Base(object): def __init__(self): pass def test(self, x): print('basetest', x) class Derived(Base): def __init__(self): super().__init__() def outertest(self, x): print('derivedtest', x) super().test(x) Derived.test = outertest a = Derived() a.test(10)
実行してみるとこうなった
$ /usr/local/python3.2/bin/python3 /tmp/aaa.py derivedtest 10 Traceback (most recent call last): File "/tmp/aaa.py", line 26, in <module> a.test(10) File "/tmp/aaa.py", line 20, in outertest super().test(x) SystemError: super(): __class__ cell not found
どうやら class 宣言内で定義したメソッドには __class__ という変数があるらしい。
試しに Derived.__init__ に print('__class__:', __class__) を仕込んでみると
__class__: <class '__main__.Derived'>
と表示されたので __class__ という変数がどこかしらのスコープから見えているということらしい。
これで super() に渡しているらしい暗黙の引数の所在がわかった。かも。
定義の所在
さて、 __class__ という変数がどこかしらに存在して、メソッド中から見えているということはわかったので、この変数が「どのタイミングで」「どのスコープに」保持されるかというところを調べてみる。
関数オブジェクトから引数やらの情報を取ってくる。
__class__ が見えているであろう関数を IPython で補完しつつ調べると、 Derived.__init__.__code__.co_freevars にそれっぽいのがあるらしい。
In [13]: Derived.__init__.__code__.co_freevars Out[13]: ('__class__',)
というわけで co_freevars に含まれているようだ。
co_freevars とは何かというと pythonにおける closure と自由変数 - odz buffer によるとクロージャに含まれる外の関数のスコープの変数であるらしい。
この「環境にある変数」というのがどこで追加されるのかを調べるために色々やってみた。
まず、こんな関数を定義する。
def check__class__(f): print('***', f.__name__, 'defined at line', f.__code__.co_firstlineno) if '__class__' in f.__code__.co_freevars: print('***', f.__name__, id(f), 'has __class__') else: print('***', f.__name__, id(f), '''doesn't have __class__''') return f
これをデコレータとしてメソッドにつけてみる。
こんな感じ。
class Base(object): @check__class__ def __init__(self): print('self', self, 'in Base.__init__') print('aaaa') super().__init__() @check__class__ def test(self, x): print('self', self) print('__class__', __class__) print('basetest', x) class Derived(Base): @check__class__ def __init__(self): print('self', self, 'in Derived.__init__') x = 10 print(locals()) print('__class__:', __class__) print('bbbb') super().__init__() @check__class__ def somefunc(self, y): pass @check__class__ def somefunc2(self, y): super(Derived, self).test(10)
実行したときの標準出力は
*** __init__ defined at line 50 *** __init__ 32934776 has __class__ *** test defined at line 57 *** test 32934912 has __class__ *** __init__ defined at line 68 *** __init__ 32935048 has __class__ *** somefunc defined at line 83 *** somefunc 32935184 doesn't have __class__ *** somefunc2 defined at line 87 *** somefunc2 32935320 has __class__
となっていた。
これを見る限り、メソッド内で super が出てくるような場合は __class__ を持っているのではないか、という予想ができそう。
脇道
ところで、この「クラス定義中のメソッド定義」は __class__ を持っているらしいが、クラス定義が終わる前にはクラスオブジェクトが存在しないはず(Python2では)なので、このデコレータ内での __class__ の値はどうなっているのか。
試しにこんな事をしてみた。
def get__class__(f): try: cls = f(10) print('aaaaaaaaaaa', cls) except Exception as e: print(e) # 略 class Derived(Base): # 略 @get__class__ def somehaveclass(self): if 0: super() return __class__
この結果は
free variable '__class__' referenced before assignment in enclosing scope
だったので、クラス定義が終わる前は __class__ は取れないということらしい。
別の方法
どうやらクラス定義内で __class__ という変数を含む環境を持った関数が定義されるっぽいので、これは type(name, bases, attrs) では同等のことはできないのではないか。
というわけで試してみる。
>>> SomeTest = type('SomeTest', (Base,), {'test': outertest}) >>> b = SomeTest() >>> SomeTest.test(10) SystemError: super(): __class__ cell not found
というわけでこの挙動は type の呼び出しだけではエミュレーションできない。
そもそも定義した関数の環境を弄ることができないので、 __class__ を追加するとかいうことはできないのである。
等価な処理を(わざわざ class を使わずに) やるのであれば
def make_class(): def __init__(self): super().__init__() def somefunc(self): print('test') __class__ = type('classname', (object,), locals()) return __class__ C = make_class() a = C() a.somefunc()
というようなことになると思われる。
なんとも面倒な話である。
まとめ
- super() の暗黙の引数は環境にある __class__ と関数の第一引数
- __class__ は class 定義中で定義した関数であれば環境情報として持っている
- __class__ はクラス定義が終わるまで使えない
- type(name, bases, attrs) だけではエミュレーションできない
- 環境に __class__ があればなんとでもできるので、なんとかできないことはない
- Python2 だと Unbound Method の第一引数の型をチェックしていたけど、 Python3 だとなくなったっぽい
調べたときに使ったコード片は gist にあげた。