web-dev-qa-db-ja.com

Pythonデコレータは関数がクラスに属していることを忘れさせます

ロギングを行うデコレータを書こうとしています:

def logger(myFunc):
    def new(*args, **keyargs):
        print 'Entering %s.%s' % (myFunc.im_class.__name__, myFunc.__name__)
        return myFunc(*args, **keyargs)

    return new

class C(object):
    @logger
    def f():
        pass

C().f()

これを印刷したい:

Entering C.f

しかし、代わりに私はこのエラーメッセージを受け取ります:

AttributeError: 'function' object has no attribute 'im_class'

おそらくこれは「ロガー」内の「myFunc」のスコープと関係があるのでしょうが、私には何がわかりません。

57

Claudiuの答えは正しいですが、self引数からクラス名を取得することによって、カンニングすることもできます。これにより、継承の場合に誤解を招くログステートメントが提供されますが、メソッドが呼び出されているオブジェクトのクラスがわかります。例えば:

from functools import wraps  # use this to preserve function signatures and docstrings
def logger(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print "Entering %s.%s" % (args[0].__class__.__name__, func.__name__)
        return func(*args, **kwargs)
    return with_logging

class C(object):
    @logger
    def f(self):
        pass

C().f()

すでに述べたように、これは親クラスから関数を継承した場合には正しく機能しません。この場合、あなたは言うかもしれません

class B(C):
    pass

b = B()
b.f()

メッセージを取得しますEntering B.f実際にメッセージを取得する場所Entering C.fそれが正しいクラスだからです。一方、これは許容できるかもしれません。その場合、私はClaudiuの提案よりもこのアプローチをお勧めします。

43
Eli Courtwright

関数は実行時にのみメソッドになります。つまり、_C.f_を取得すると、バインドされた関数(および_C.f.im_class is C_)が取得されます。関数が定義された時点では、それは単なる関数であり、どのクラスにもバインドされていません。このバインドされておらず、関連付けられていない機能は、ロガーによって装飾されます。

_self.__class__.__name___はクラスの名前を提供しますが、記述子を使用して、これをやや一般的な方法で実現することもできます。このパターンは DecolatorとDescriptorsに関するブログ投稿で と説明されており、特にロガーデコレーターの実装は次のようになります。

_class logger(object):
    def __init__(self, func):
        self.func = func
    def __get__(self, obj, type=None):
        return self.__class__(self.func.__get__(obj, type))
    def __call__(self, *args, **kw):
        print 'Entering %s' % self.func
        return self.func(*args, **kw)

class C(object):
    @logger
    def f(self, x, y):
        return x+y

C().f(1, 2)
# => Entering <bound method C.f of <__main__.C object at 0x...>>
_

明らかに、出力は(たとえば、getattr(self.func, 'im_class', None)を使用して)改善できますが、この一般的なパターンはメソッドと関数の両方で機能します。ただし、は機能しません古いスタイルのクラスでは機能しません(ただし、それらを使用しないでください;)

25
ianb

ここで提案されているアイデアは優れていますが、いくつかの欠点があります。

  1. _inspect.getouterframes_および_args[0].__class__.__name___は、単純な関数や静的メソッドには適していません。
  2. ___get___は、_@wraps_によって拒否されるクラス内になければなりません。
  3. _@wraps_自体がトレースをより適切に非表示にする必要があります。

それで、このページ、リンク、ドキュメント、そして自分の頭からのアイデアをいくつか組み合わせました
最後に、上記の3つの欠点すべてを欠く解決策が見つかりました。

その結果、_method_decorator_:

  • デコレートされたメソッドがバインドされているクラスを知っています。
  • functools.wraps()よりも正確にシステム属性に応答することにより、デコレータトレースを非表示にします。
  • バインドされた、バインドされていないインスタンスメソッド、クラスメソッド、静的メソッド、およびプレーン関数の単体テストでカバーされています。

使用法:

_pip install method_decorator
from method_decorator import method_decorator

class my_decorator(method_decorator):
    # ...
_

使用方法の詳細については、完全な単体テスト を参照してください。

そして、ここに_method_decorator_クラスのコードがあります:

_class method_decorator(object):

    def __init__(self, func, obj=None, cls=None, method_type='function'):
        # These defaults are OK for plain functions
        # and will be changed by __get__() for methods once a method is dot-referenced.
        self.func, self.obj, self.cls, self.method_type = func, obj, cls, method_type

    def __get__(self, obj=None, cls=None):
        # It is executed when decorated func is referenced as a method: cls.func or obj.func.

        if self.obj == obj and self.cls == cls:
            return self # Use the same instance that is already processed by previous call to this __get__().

        method_type = (
            'staticmethod' if isinstance(self.func, staticmethod) else
            'classmethod' if isinstance(self.func, classmethod) else
            'instancemethod'
            # No branch for plain function - correct method_type for it is already set in __init__() defaults.
        )

        return object.__getattribute__(self, '__class__')( # Use specialized method_decorator (or descendant) instance, don't change current instance attributes - it leads to conflicts.
            self.func.__get__(obj, cls), obj, cls, method_type) # Use bound or unbound method with this underlying func.

    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)

    def __getattribute__(self, attr_name): # Hiding traces of decoration.
        if attr_name in ('__init__', '__get__', '__call__', '__getattribute__', 'func', 'obj', 'cls', 'method_type'): # Our known names. '__class__' is not included because is used only with explicit object.__getattribute__().
            return object.__getattribute__(self, attr_name) # Stopping recursion.
        # All other attr_names, including auto-defined by system in self, are searched in decorated self.func, e.g.: __module__, __class__, __name__, __doc__, im_*, func_*, etc.
        return getattr(self.func, attr_name) # Raises correct AttributeError if name is not found in decorated self.func.

    def __repr__(self): # Special case: __repr__ ignores __getattribute__.
        return self.func.__repr__()
_
16
Denis Ryzhkov

クラスが作成されている間、Pythonは通常の関数オブジェクトを作成します。それらは後で非バインドメソッドオブジェクトに変換されるだけです。知っていることは、これが私があなたにできる唯一の方法です。欲しいです:

def logger(myFunc):
    def new(*args, **keyargs):
        print 'Entering %s.%s' % (myFunc.im_class.__name__, myFunc.__name__)
        return myFunc(*args, **keyargs)

    return new

class C(object):
    def f(self):
        pass
C.f = logger(C.f)
C().f()

これは望ましい結果を出力します。

クラス内のすべてのメソッドをラップする場合は、おそらく次のように使用できるwrapClass関数を作成する必要があります。

C = wrapClass(C)
7
Claudiu

inspectライブラリを使用して、非常に類似した問題の別の解決策を見つけました。デコレーターが呼び出されると、関数がまだクラスにバインドされていなくても、スタックを検査して、どのクラスがデコレーターを呼び出しているかを検出できます。必要な場合は、少なくともクラスの文字列名を取得できます(作成中のため、おそらくまだ参照できません)。その後、クラスが作成された後に何も呼び出す必要はありません。

import inspect

def logger(myFunc):
    classname = inspect.getouterframes(inspect.currentframe())[1][3]
    def new(*args, **keyargs):
        print 'Entering %s.%s' % (classname, myFunc.__name__)
        return myFunc(*args, **keyargs)
    return new

class C(object):
    @logger
    def f(self):
        pass

C().f()

これは必ずしも他のbetterである必要はありませんが、デコレータの呼び出し中にfutureメソッドのクラス名を発見するためのonly方法です。 inspectライブラリのドキュメントで、フレームへの参照を維持しないことに注意してください。

6
user398139

クラス関数は常に最初の引数としてselfを取る必要があるため、im_classの代わりにそれを使用できます。

def logger(myFunc):
    def new(self, *args, **keyargs):
        print 'Entering %s.%s' % (self.__class__.__name__, myFunc.__name__)
        return myFunc(self, *args, **keyargs)

    return new 

class C(object):
    @logger
    def f(self):
        pass
C().f()

最初はself.__name__を使用したかったのですが、インスタンスに名前がないため機能しません。クラスの名前を取得するには、self.__class__.__name__を使用する必要があります。

6
Asa Ayers

new.instancemethod()を使用して、関数からインスタンスメソッド(バインドされているかどうかにかかわらず)を作成することもできます。

0
Andrew Beyer

定義時に装飾コードを挿入する代わりに、関数がそのクラスを知らない場合は、関数がアクセスまたは呼び出されるまでこのコードの実行を遅らせます。記述子オブジェクトを使用すると、アクセス/呼び出し時に独自のコードを後から挿入できます。

class decorated(object):
    def __init__(self, func, type_=None):
        self.func = func
        self.type = type_

    def __get__(self, obj, type_=None):
        return self.__class__(self.func.__get__(obj, type_), type_)

    def __call__(self, *args, **kwargs):
        name = '%s.%s' % (self.type.__name__, self.func.__name__)
        print('called %s with args=%s kwargs=%s' % (name, args, kwargs))
        return self.func(*args, **kwargs)

class Foo(object):
    @decorated
    def foo(self, a, b):
        pass

これで、アクセス時に両方のクラスを検査できます(__get__)と呼び出し時(__call__)。このメカニズムは、単純なメソッドとstatic | classメソッドで機能します。

>>> Foo().foo(1, b=2)
called Foo.foo with args=(1,) kwargs={'b': 2}

完全な例: https://github.com/aurzenligl/study/blob/master/python-robotwrap/Example4.py

0
aurzenligl

Asa Ayers 'answer に示すように、クラスオブジェクトにアクセスする必要はありません。 Python 3.3なので、 __qualname__ は、完全修飾名を提供します。

>>> def logger(myFunc):
...     def new(*args, **keyargs):
...         print('Entering %s' % myFunc.__qualname__)
...         return myFunc(*args, **keyargs)
... 
...     return new
... 
>>> class C(object):
...     @logger
...     def f(self):
...         pass
... 
>>> C().f()
Entering C.f

PEP 3155 からのこの例に示すように、これにはネストされたクラスの場合にも機能するという追加の利点があります。

>>> class C:
...   def f(): pass
...   class D:
...     def g(): pass
...
>>> C.__qualname__
'C'
>>> C.f.__qualname__
'C.f'
>>> C.D.__qualname__
'C.D'
>>> C.D.g.__qualname__
'C.D.g'

Python 3ではim_class属性がなくなっているため、デコレータでクラスにアクセスしたい場合は、別のメソッドが必要です。私が現在使用しているアプローチには、 object.__set_name__ と詳細は (「Pythonインスタンスメソッドのデコレータがクラスにアクセスできますか?)」に対する私の回答)

0
tyrion