Python3でunbound methodが無くなった

Pythonでクラスやインスタンスの属性に関数を代入して呼び出すコードを書いていたらPython2とPython3で違いがあることが わかりました。

最初に状況を説明すると、第一引数にselfを受け取らない単なるグローバル関数をクラス属性として保持しておいて、 後から呼び出すコードを書いていたら、Python3では動作したのにPython2で例外が発生しました。

この挙動の違いはPython3で unbound method という概念が無くなったのが原因でした。

まず動作確認に使用したソースを示します。

def func(*args):
    print(args)


class MethodTest(object):
    pass


c = MethodTest()

# インスタンス属性としてグローバル関数を代入して呼び出すと単なる関数として呼び出される
c.method = func
c.method('hello')  # => ('hello',)
print(c.method)  # => <function func at 0x00000083FEB001E0>
del c.method

# クラス属性としてグローバル関数を代入してインスタンスから呼び出すとbound methodとして呼び出される
MethodTest.method = func
c.method('hello')  # => (<test.MethodTest object at 0x00000069D8AAE3C8>, 'hello')
print(c.method)  # => <bound method MethodTest.func of <test.MethodTest object at 0x00000083FF474860>>

# クラス属性としてグローバル関数を代入してクラスオブジェクトから呼び出すと…
# Python 3.3: 単なる関数として呼び出される
MethodTest.method = func
MethodTest.method('hello')  # => ('hello',)
print(MethodTest.method)  # => <function func at 0x00000083FEB001E0>

# Python 2.6, 2.7: unbound methodをself無しで呼び出そうとしてTypeError例外が発生する
MethodTest.method('hello')  # => TypeError: unbound method func() must be called with MethodTest instance as first argument (got str instance instead)
print(MethodTest.method)  # => <unbound method MethodTest.func>

以下の2点はPython2/3で違いはありません。

  • インスタンス属性としてグローバル関数を代入して呼び出す
  • クラス属性としてグローバル関数を代入してインスタンスから呼び出す

ただし、後者はbound methodとして呼び出されているので、func関数は第一引数に意図しないself(MethodTestオブジェクト)を受けとってしまっています。

一方、クラス属性としてグローバル関数を代入してクラスオブジェクトから呼び出す場合は以下の通りPython2と3で動作が違います。

  • Python 3.3: 単なる関数として呼び出される(私が期待した通りの挙動)
  • Python 2.6, 2.7: unbound methodをself無しで呼び出そうとしてTypeError例外が発生する

Python2系では、メソッドの呼び出し方が間違っているのでエラーが出ました。 これは、MethodTest.method の第一引数に MethodTest のインスタンスを渡すことで呼び出すこと自体はできます。

c = MethodTest()
MethodTest.method = func
MethodTest.method(c, 'hello')  # => (<MethodTest object at 0x0000000004919BE0>, 'hello')

ただ、今回は単なる関数として呼び出すのが目的なのでこれでは解決になっていません。これを解決するにはstaticmethodを使って以下のようにします。

MethodTest.method = staticmethod(func)
MethodTest.method('hello')  # => ('hello',)
print(MethodTest.method)  # => <function func at 0x00000083FEB001E0>

staticmethodにより、unbound methodがfunctionに変わっているので、単なる関数として呼び出せます。

まとめ

  1. Python2ではunbound methodの概念があり、Class.method でメソッドを取得すると unbound method になる。
  2. unbound methodの呼び出しでは渡された第一引数の型をチェックし、Classまたはそのサブクラス以外が渡されたらTypeErrorを発生する。(←この説明はちょっと自信ない)
  3. それを避けて単なる関数として呼び出すにはstaticmethodを使う
  4. Python3ではunbound methodの概念が削除されたため、Class.method で取得すると単なる関数になる。そのため、Python2でやっていたような第一引数の型チェックは行われない。

Pythonに関してあまり詳しくないので、間違いがあったらご指摘等お願いします。