Python3 とクラスと super

Python3 では super() を呼び出す際に引数を省略できる。
例えば以下のように書くと、 super() は super(Derived, self) という呼び出しと等価になるらしい。

class Derived(Base):

    def __init__(self):

        super().__init__()

そもそも明示を是とする Python で引数が省略できてしまうってのは気持ち悪いというのはかなりあるのだけども、どのように解決しているのだろうかなどと気になってしまったので調べてみた。

仕様

この仕様はどこで定義されているのかと PEP を調べてみたら PEP3135 で定義されているらしい。
調べてみた結果とは微妙に違うけどまあそれくらいあるのかも?

実験

引数の所在

そもそも 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 にあげた。