program story

파이썬 데코레이터는 함수가 클래스에 속한다는 것을 잊게 만듭니다.

inputbox 2020. 12. 24. 23:40
반응형

파이썬 데코레이터는 함수가 클래스에 속한다는 것을 잊게 만듭니다.


로깅을 수행하는 데코레이터를 작성하려고합니다.

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'

아마도 이것은 'logger'내부의 'myFunc'범위와 관련이 있지만 무엇인지 모르겠습니다.


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의 제안보다이 접근 방식을 권장합니다.


함수는 런타임에만 메서드가됩니다. 즉, C.f바인딩 된 함수 (및 C.f.im_class is C) 를 얻습니다 . 함수가 정의 된 시점에는 일반 함수일 뿐이며 어떤 클래스에도 바인딩되지 않습니다. 이 언 바운드 및 연결 해제 기능은 로거가 장식 한 것입니다.

self.__class__.__name__클래스의 이름을 제공하지만 디스크립터를 사용하여 좀 더 일반적인 방법으로이를 수행 할 수도 있습니다. 이 패턴은 Decorators and 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))이 일반적인 패턴은 메서드와 함수 모두에서 작동합니다. 그러나 구식 클래스 에서는 작동하지 않습니다 (하지만 사용하지 마십시오;)


여기에 제안 된 아이디어는 훌륭하지만 몇 가지 단점이 있습니다.

  1. inspect.getouterframesargs[0].__class__.__name__일반 기능과 정전기 방법이 적합하지 않습니다.
  2. __get__에서 거부 한 클래스에 있어야합니다 @wraps.
  3. @wraps 그 자체가 흔적을 더 잘 숨겨야합니다

그래서 저는이 페이지, 링크, 문서 및 내 머리의 몇 가지 아이디어를 결합
하고 마침내 위의 세 가지 단점이 모두없는 솔루션을 찾았습니다.

결과적으로 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__()

클래스가 생성되는 동안 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)

클래스 함수는 항상 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()

at first I wanted to use self.__name__ but that doesn't work because the instance has no name. you must use self.__class__.__name__ to get the name of the class.


I found another solution to a very similar problem using the inspect library. When the decorator is called, even though the function is not yet bound to the class, you can inspect the stack and discover which class is calling the decorator. You can at least get the string name of the class, if that is all you need (probably can't reference it yet since it is being created). Then you do not need to call anything after the class has been created.

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()

While this is not necessarily better than the others, it is the only way I can figure out to discover the class name of the future method during the call to the decorator. Make note of not keeping references to frames around in the inspect library documentation.


You can also use new.instancemethod() to create an instance method (either bound or unbound) from a function.


Instead of injecting decorating code at definition time, when function doesn't know it's class, delay running this code until function is accessed/called. Descriptor object facilitates injecting own code late, at access/call time:

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

Now we can inspect class both at access time (__get__) and at call time (__call__). This mechanism works for plain methods as well as static|class methods:

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

Full example at: https://github.com/aurzenligl/study/blob/master/python-robotwrap/Example4.py


As shown in Asa Ayers' answer, you don't need to access the class object. It may be worth to know that since Python 3.3, you can also use __qualname__, which gives you the fully qualified name:

>>> 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

This has the added advantage of working also in the case of nested classes, as shown in this example taken from 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'

Notice also that in Python 3 the im_class attribute is gone, therefore if you really wish to access the class in a decorator, you need an other method. The approach I currently use involves object.__set_name__ and is detailed in my answer to "Can a Python decorator of an instance method access the class?"

ReferenceURL : https://stackoverflow.com/questions/306130/python-decorator-makes-function-forget-that-it-belongs-to-a-class

반응형