#前置き
Pythonだとクラスメソッドや静的メソッドを@classmethod
や@staticmethod
などのデコレータを使って定義できる。
一方で、普通のメソッド(インスタンスメソッド)は普通に定義すれば良いが、実は二通りの呼び方ができる:
class MyClass:
def mymethod(self,x):
print(x)
x = 1
myclass = MyClass()
myclass.mymethod(x) # 普通の使いかた
MyClass(None,1) # !?!?
二番目の使い方は定義した関数をそのまま使う方法(一番目の引数はself
だが、関数中では使われないのでNone
でもいい)。
しかし、インスタンスメソッドなのにもかかわらずうっかりクラスメソッドみたいに呼んでしまったりするとわかりにくいエラーの原因になる。
例えば、下のように複数引数を取る場合:
class MyClass:
def mymethod(self,*x):
print(x)
x = list(range(4))
myclass = MyClass()
myclass.mymethod(*x) # [0,1,2,3]
MyClass.mymethod(*x) # [1,2,3]
こういうのを避けるために、二番目のような呼び方をすると怒られる様にしておきたい。
補足
Python2では、`myclass.mymethod`はbound method、`MyClass.mymethod`はunbound methodを返すようになっていた。
(bound/unbound) methodは、第一引数が(selfに固定/MyClassのインスタンス以外を取るとエラーを返す)用になっているもの。
つまりPython2時代のUnbound methodを場合に応じて復活させたい、というのが今回の趣旨になる。
#実装
参考1:stackoverflow - "Get defining class of unbound method object in Python 3"
参考2:Mastering Python - 誰がメソッドから関数に変換しているの?
デコレータを定義する。
import functools
import inspect
# a.x -> type(a).__dict__['x'].__get__(a, type(a))
# A.x -> A.__dict__['x'].__get__(None, A)
#
# --> __get__の第一引数がNoneのときは、クラスから呼ばれた時。
# そこで、A.__dict__['x']に__get__を持ったオブジェクトを置いとくと、
# いい感じにHackできる。だたし、__get__(obe,objtype)の返り値にも注意。
# a.x の場合は、返り値はメソッドである必要がある。つまり、関数の第一引数を固定したやつ。
# A.x の場合は、気にしないでよし(エラーにする)。
#def pure_instancemethod(func):
# functools.wraps(func)
# def wrapped_func(self,*args,**kwargs):
# if not isinstance(self,)
# raise TypeError("クラスメソッドじゃないよ")
class PureInstanceMethod:
def __init__(self,func):
self.func = func
def __get__(self,obj,objtype):
if obj is None: # クラスからの呼び出し:
raise TypeError("クラスから呼ぶなタコ")
else: # インスタンスからの呼び出し
def instancemethod(*args,**kwargs):
return self.func(obj,*args,**kwargs)
return instancemethod # メソッドを返してあげよう
def __call__(self,*args,**kwargs):
return self.func(*args,**kwargs)
class MyClass:
@PureInstanceMethod
def mymethod(self,*x):
print(x)
# MyClass.__dict__["mymethod"] = pureinstancemethod(mymethod) とほぼ同じ。
# つまりここにはmymethodで初期化されたpureinstancemethodのインスタンスが入る。
#使い方
myclass.mymethod(*[1,2,3])
# > (1, 2, 3)
MyClass.mymethod(*[1,2,3])
# > ---------------------------------------------------------------------------
# > TypeError Traceback (most recent call last)
# > <ipython-input-152-99447b4e3435> in <module>
# > ----> 1 MyClass.mymethod(*[1,2,3]) # [1,2,3]
# >
# > <ipython-input-147-39b9424a9215> in __get__(self, obj, objtype)
# > 23 def __get__(self,obj,objtype):
# > 24 if obj is None: # クラスからの呼び出し:
# > ---> 25 raise TypeError("クラスから呼ぶなタコ")
# > 26
# > 27 else: # インスタンスからの呼び出し
# >
# > TypeError: クラスから呼ぶなタコ
無事怒られた。