0
1

More than 3 years have passed since last update.

純粋なインスタンスメソッドを定義するデコレータ(@pureinstansemethod)

Posted at

前置き

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: クラスから呼ぶなタコ

無事怒られた。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1