Posted at

pythonでクラス内にdef文を書かずにメソッドを定義する

More than 1 year has passed since last update.


クラス属性である関数オブジェクトはいずれも、そのクラスのインスタンスのためのメソッドを定義しています。関数定義は、テキスト上でクラス定義の中に入っている必要はありません。関数オブジェクトをクラスのローカルな変数の中に代入するのも OK です。


( https://docs.python.org/ja/3.6/tutorial/classes.html#random-remarks より )

ということで、いろいろ試して確認しました。

python 3.6.5です。


callableではダメ


foo.py

def f(self):

return f'function: I\'m {self.name}'

f_lambda = lambda self: f'lambda: I\'m {self.name}'

class FCallable:
@staticmethod # 第1引数にFCallableインスタンスが渡ると厄介そうなので
def __call__(self):
return f'callable: I\'m {self.name}'


from . import foo

class C:
f1 = foo.f
f2 = foo.f_lambda
f3 = foo.FCallable()

def __init__(self, name):
self.name = name

c = C('bar')
c.f1() # function: I'm bar
c.f2() # lambda: I'm bar
c.f3() # エラー

という感じで、callableなだけではダメということが確認できました。

なぜなのか? 先の引用を見直してみると「関数オブジェクト」という言葉が目に付きます。これをはっきりと定義した文章が見つけられなかったのですが、標準型の階層という項の「ユーザ定義関数」の部分には


ユーザ定義関数オブジェクトは、関数定義を行うことで生成されます。


という文があるので、関数オブジェクトとは「ユーザ定義関数または組み込み関数のオブジェクト」と考えられます。更に関数定義にはdef文による関数定義の他に


無名関数 (名前に束縛されていない関数) を作成することもできます。これは ラムダ (lambda) の節で解説されているラムダ式を使います。ラムダ式は簡略化された関数定義の省略表現に過ぎないことに注意してください; "def" 文で定義された関数もラムダ式で作成された関数のように、引数として渡せたり、他の名前に割り当てることができます。


とあるので、ラムダ式の生成するものも関数オブジェクトであるとわかります。

以上から、def文とラムダ式は関数オブジェクトを生成するのでOKで、その他のcallableは関数オブジェクトではないからNGということになります。


クラス内にdef文を書いたものとの違いと、その埋め方

以上のように、クラス属性な関数オブジェクトはメソッドとして呼び出せますが、クラス内にdef文を書いた場合と全く同じというわけにはいきません。

c.f1.__name__  # f

c.f1.__module__ # foo

「自分をC.f1だと思いこんでいるfoo.f」といったところでしょうか(逆か?)。

これと似たような問題は関数デコレータの場合に発生し、そこでは@functools.wrapsが解決策となります。

@functools.wrapsは関数デコレータなのでうまく適用できませんが、内部でfunctools.update_wrapper()を使っているので、これを使えばなんとかできそうです。

from functools import update_wrapper

def substitute_function(function):
def decorator(decorated):
update_wrapper(function, decorated)
return function
return decorator

class C:
@substitute_function(foo.f)
def f1(): pass # 仮の定義

クラス内にdef文を書くとどうなるのか知りたいので一旦書き、それをデコレートして乗っ取ります。

c.f1.__name__  # f1

これで解決……ではないです。

class C:

@substitute_function(foo.f)
def f1(): pass

class D:
@substitute_function(foo.f)
def f1(): pass

c.f1.__qualname__ # D.f1

関数オブジェクトは1つしかないので、@substitute_functionに渡す度に属性を書き換えられてしまいます。

しょうがないので都度都度新しい関数オブジェクトを返すようにします。


foo.py

def fgen():

def f(self):
return f'function: I\'m {self.name}'
return f

class C:

@substitute_function(foo.fgen())
def f1(): pass

class D:
@substitute_function(foo.fgen())
def f1(): pass

c.f1.__qualname__ # C.f1


終わりに

そもそも「クラス内にdef文を書かずにメソッドを定義する」ことにどれほどのうまみがあるのか。共通化が目的なら適切に抽象化してクラスを分けて包含するとか、基底クラスを抽出するとかいくらでもやりようがある。

という点については別な記事にしたいと思います。