인스턴스 메서드의 데코레이터가 클래스에 액세스 할 수 있습니까?


109

대략 다음과 같은 것이 있습니다. 기본적으로 정의에서 인스턴스 메서드에 사용되는 데코레이터에서 인스턴스 메서드의 클래스에 액세스해야합니다.

def decorator(view):
    # do something that requires view's class
    print view.im_class
    return view

class ModelA(object):
    @decorator
    def a_method(self):
        # do some stuff
        pass

있는 그대로 코드는 다음을 제공합니다.

AttributeError : 'function'개체에 'im_class'속성이 없습니다.

- 나는 비슷한 질문 / 답변을 발견 파이썬 장식의 차종이 클래스에 속한다는 것을 잊지 기능파이썬 장식의 클래스를 돌려 - 그러나 이들은 첫 번째 매개 변수를 날치기로 런타임에 인스턴스를 잡고 해결 방법에 의존한다. 제 경우에는 클래스에서 수집 한 정보를 기반으로 메서드를 호출 할 것이므로 호출이 올 때까지 기다릴 수 없습니다.

답변:


68

Python 2.6 이상을 사용하는 경우 다음과 같은 클래스 데코레이터를 사용할 수 있습니다 (경고 : 테스트되지 않은 코드).

def class_decorator(cls):
   for name, method in cls.__dict__.iteritems():
        if hasattr(method, "use_class"):
            # do something with the method and class
            print name, cls
   return cls

def method_decorator(view):
    # mark the method as something that requires view's class
    view.use_class = True
    return view

@class_decorator
class ModelA(object):
    @method_decorator
    def a_method(self):
        # do some stuff
        pass

메소드 데코레이터는 "use_class"속성을 추가하여 메소드를 관심있는 것으로 표시합니다. 함수와 메소드도 객체이므로 추가 메타 데이터를 첨부 할 수 있습니다.

클래스가 생성 된 후 클래스 데코레이터는 모든 메서드를 살펴보고 표시된 메서드에 필요한 모든 작업을 수행합니다.

모든 메소드가 영향을 받기를 원한다면 메소드 데코레이터를 생략하고 클래스 데코레이터를 사용할 수 있습니다.


2
감사합니다. 이것이 갈 길이라고 생각합니다. 이 데코레이터를 사용하려는 모든 클래스에 대해 코드 한 줄만 추가하면됩니다. 아마도 사용자 지정 메타 클래스를 사용하고 새로운 동안 이와 동일한 검사를 수행 할 수 있습니다 ...?
Carl G

3
이것을 staticmethod 또는 classmethod와 함께 사용하려는 사람은 누구나이 PEP를 읽고 싶어 할 것입니다. python.org/dev/peps/pep-0232 클래스 / 정적 메서드에 속성을 설정할 수 없기 때문에 가능하지 않습니다. 사용자 정의 함수 속성이 함수에 적용될 때 위로 올립니다.
Carl G

DBM 기반 ORM을 위해 내가 찾던 것 ... 고마워, 친구.
Coyote21 2013 년

inspect.getmro(cls)상속을 지원하려면 클래스 데코레이터의 모든 기본 클래스를 처리 하는 데 사용해야 합니다.
schlamar

1
아, 실제로 inspect구조 처럼 보입니다. stackoverflow.com/a/1911287/202168
Anentropic

16

파이썬 3.6부터는 object.__set_name__매우 간단한 방법으로이를 수행 할 수 있습니다 . 문서에는 __set_name__"소유 클래스 소유자 가 생성 될 때 호출됩니다"라고 명시되어 있습니다. 다음은 예입니다.

class class_decorator:
    def __init__(self, fn):
        self.fn = fn

    def __set_name__(self, owner, name):
        # do something with owner, i.e.
        print(f"decorating {self.fn} and using {owner}")
        self.fn.class_name = owner.__name__

        # then replace ourself with the original method
        setattr(owner, name, self.fn)

클래스 생성시 호출됩니다.

>>> class A:
...     @class_decorator
...     def hello(self, x=42):
...         return x
...
decorating <function A.hello at 0x7f9bedf66bf8> and using <class '__main__.A'>
>>> A.hello
<function __main__.A.hello(self, x=42)>
>>> A.hello.class_name
'A'
>>> a = A()
>>> a.hello()
42

클래스가 어떻게 생성되는지, 특히 정확히 언제 __set_name__호출 되는지에 대해 더 알고 싶다면 "Creating the class object"문서를 참조하십시오 .


1
매개 변수와 함께 데코레이터를 사용하면 어떻게 보일까요? 예@class_decorator('test', foo='bar')
luckydonald

2
@luckydonald 인수를 취하는 일반 데코레이터 와 비슷하게 접근 할 수 있습니다 . 그냥이def decorator(*args, **kwds): class Descriptor: ...; return Descriptor
매트 대한 수정 사항

와, 정말 감사합니다. __set_name__오랫동안 Python 3.6 이상을 사용해 왔지만 몰랐 습니다.
kawing-chiu

이 방법에는 한 가지 단점이 있습니다. 정적 검사기는이를 전혀 이해하지 못합니다. Mypy는 이것이 hello메소드가 아니라 유형의 객체 라고 생각할 것입니다 class_decorator.
kawing-chiu

@ kawing-chiu 아무것도 작동하지 않으면를 사용 하여 올바른 유형을 반환하는 일반 데코레이터로 if TYPE_CHECKING정의 할 수 있습니다 class_decorator.
tyrion

15

다른 사람들이 지적했듯이 데코레이터가 호출 될 때 클래스가 생성되지 않았습니다. 그러나 데코레이터 매개 변수로 함수 객체에 주석을 단 다음 메타 클래스의 __new__메서드 에서 함수를 다시 데코레이션 할 수 있습니다 . __dict__적어도 저 func.foo = 1에게는 AttributeError가 발생했듯이 함수의 속성에 직접 액세스해야합니다 .


6
setattr접근 대신 사용한다__dict__
schlamar

7

Mark가 제안한대로 :

  1. 모든 데코레이터는 클래스가 빌드되기 전에 호출되므로 데코레이터는 알 수 없습니다.
  2. 이러한 메서드에 태그를 지정 하고 나중에 필요한 사후 처리를 수행 할 수 있습니다 .
  3. 사후 처리를위한 두 가지 옵션이 있습니다. 클래스 정의가 끝날 때 자동으로 또는 애플리케이션이 실행되기 전 어딘가에 있습니다. 기본 클래스를 사용하는 첫 번째 옵션을 선호하지만 두 번째 방법을 따를 수도 있습니다.

이 코드는 자동 후 처리를 사용하여 어떻게 작동하는지 보여줍니다.

def expose(**kw):
    "Note that using **kw you can tag the function with any parameters"
    def wrap(func):
        name = func.func_name
        assert not name.startswith('_'), "Only public methods can be exposed"

        meta = func.__meta__ = kw
        meta['exposed'] = True
        return func

    return wrap

class Exposable(object):
    "Base class to expose instance methods"
    _exposable_ = None  # Not necessary, just for pylint

    class __metaclass__(type):
        def __new__(cls, name, bases, state):
            methods = state['_exposed_'] = dict()

            # inherit bases exposed methods
            for base in bases:
                methods.update(getattr(base, '_exposed_', {}))

            for name, member in state.items():
                meta = getattr(member, '__meta__', None)
                if meta is not None:
                    print "Found", name, meta
                    methods[name] = member
            return type.__new__(cls, name, bases, state)

class Foo(Exposable):
    @expose(any='parameter will go', inside='__meta__ func attribute')
    def foo(self):
        pass

class Bar(Exposable):
    @expose(hide=True, help='the great bar function')
    def bar(self):
        pass

class Buzz(Bar):
    @expose(hello=False, msg='overriding bar function')
    def bar(self):
        pass

class Fizz(Foo):
    @expose(msg='adding a bar function')
    def bar(self):
        pass

print('-' * 20)
print("showing exposed methods")
print("Foo: %s" % Foo._exposed_)
print("Bar: %s" % Bar._exposed_)
print("Buzz: %s" % Buzz._exposed_)
print("Fizz: %s" % Fizz._exposed_)

print('-' * 20)
print('examine bar functions')
print("Bar.bar: %s" % Bar.bar.__meta__)
print("Buzz.bar: %s" % Buzz.bar.__meta__)
print("Fizz.bar: %s" % Fizz.bar.__meta__)

결과는 다음과 같습니다.

Found foo {'inside': '__meta__ func attribute', 'any': 'parameter will go', 'exposed': True}
Found bar {'hide': True, 'help': 'the great bar function', 'exposed': True}
Found bar {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
Found bar {'msg': 'adding a bar function', 'exposed': True}
--------------------
showing exposed methods
Foo: {'foo': <function foo at 0x7f7da3abb398>}
Bar: {'bar': <function bar at 0x7f7da3abb140>}
Buzz: {'bar': <function bar at 0x7f7da3abb0c8>}
Fizz: {'foo': <function foo at 0x7f7da3abb398>, 'bar': <function bar at 0x7f7da3abb488>}
--------------------
examine bar functions
Bar.bar: {'hide': True, 'help': 'the great bar function', 'exposed': True}
Buzz.bar: {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
Fizz.bar: {'msg': 'adding a bar function', 'exposed': True}

이 예에서는 다음을 참고하십시오.

  1. 임의의 매개 변수로 모든 함수에 주석을 달 수 있습니다.
  2. 각 클래스에는 자체 노출 된 메서드가 있습니다.
  3. 노출 된 메서드도 상속 할 수 있습니다.
  4. 노출 기능이 업데이트되면 메서드를 재정의 할 수 있습니다.

도움이 되었기를 바랍니다


4

Ants가 지적했듯이 클래스 내에서 클래스에 대한 참조를 가져올 수 없습니다. 그러나 실제 클래스 유형 객체를 조작하지 않고 다른 클래스를 구별하는 데 관심이있는 경우 각 클래스에 대한 문자열을 전달할 수 있습니다. 클래스 스타일 데코레이터를 사용하여 원하는 다른 매개 변수를 데코레이터에 전달할 수도 있습니다.

class Decorator(object):
    def __init__(self,decoratee_enclosing_class):
        self.decoratee_enclosing_class = decoratee_enclosing_class
    def __call__(self,original_func):
        def new_function(*args,**kwargs):
            print 'decorating function in ',self.decoratee_enclosing_class
            original_func(*args,**kwargs)
        return new_function


class Bar(object):
    @Decorator('Bar')
    def foo(self):
        print 'in foo'

class Baz(object):
    @Decorator('Baz')
    def foo(self):
        print 'in foo'

print 'before instantiating Bar()'
b = Bar()
print 'calling b.foo()'
b.foo()

인쇄물:

before instantiating Bar()
calling b.foo()
decorating function in  Bar
in foo

또한 데코레이터에 대한 Bruce Eckel의 페이지를 참조하십시오.


이것이 불가능하다는 내 우울한 결론을 확인 해주셔서 감사합니다. 또한 모듈 / 클래스 ( 'module.Class')를 정규화 한 문자열을 사용하고 클래스가 모두 완전히로드 될 때까지 문자열을 저장 한 다음 가져 오기를 통해 직접 클래스를 검색 할 수 있습니다. 그것은 내 임무를 완수하기 위해 비참하게 건조하지 않은 방법처럼 보입니다.
Carl G

이런 종류의 데코레이터에는 클래스를 사용할 필요가 없습니다. 관용적 접근 방식은 데코레이터 함수 내에서 하나의 추가 중첩 함수를 사용하는 것입니다. 당신은 클래스와 함께 갈 경우에는, 그 자체 즉, "표준"보이는 장식하기 위해 클래스 이름에 대문자를 사용하지 좋을 수도 @decorator('Bar')에 반대를 @Decorator('Bar').
Erik Kaplun 2012-08-25

4

무엇 플라스크 - 고급이 는 방식에 저장하는 임시 캐시를 만드는 것입니다 않습니다, 그것은 뭔가 다른 (플라스크를 사용하여 클래스를 등록 것이라는 사실 사용 register에 실제로 방법을 래핑 클래스의 방법).

이 패턴을 재사용 할 수 있습니다. 이번에는 메타 클래스를 사용하여 가져올 때 메서드를 래핑 할 수 있습니다.

def route(rule, **options):
    """A decorator that is used to define custom routes for methods in
    FlaskView subclasses. The format is exactly the same as Flask's
    `@app.route` decorator.
    """

    def decorator(f):
        # Put the rule cache on the method itself instead of globally
        if not hasattr(f, '_rule_cache') or f._rule_cache is None:
            f._rule_cache = {f.__name__: [(rule, options)]}
        elif not f.__name__ in f._rule_cache:
            f._rule_cache[f.__name__] = [(rule, options)]
        else:
            f._rule_cache[f.__name__].append((rule, options))

        return f

    return decorator

실제 클래스에서 (메타 클래스를 사용하여 동일한 작업을 수행 할 수 있음) :

@classmethod
def register(cls, app, route_base=None, subdomain=None, route_prefix=None,
             trailing_slash=None):

    for name, value in members:
        proxy = cls.make_proxy_method(name)
        route_name = cls.build_route_name(name)
        try:
            if hasattr(value, "_rule_cache") and name in value._rule_cache:
                for idx, cached_rule in enumerate(value._rule_cache[name]):
                    # wrap the method here

출처 : https://github.com/apiguy/flask-classy/blob/master/flask_classy.py


이것은 유용한 패턴이지만, 메서드 데코레이터가 그것이 적용된 메서드의 부모 클래스를 참조 할 수있는 문제를 해결하지 못합니다
Anentropic

가져 오기 시간에 클래스에 액세스하는 데 유용 할 수있는 방법을보다 명확하게하기 위해 내 대답을 업데이트했습니다 (예 : 메타 클래스 사용 + 메서드에서 데코레이터 매개 변수 캐싱).
charlax 2014

3

문제는 데코레이터가 호출 될 때 클래스가 아직 존재하지 않는다는 것입니다. 이 시도:

def loud_decorator(func):
    print("Now decorating %s" % func)
    def decorated(*args, **kwargs):
        print("Now calling %s with %s,%s" % (func, args, kwargs))
        return func(*args, **kwargs)
    return decorated

class Foo(object):
    class __metaclass__(type):
        def __new__(cls, name, bases, dict_):
            print("Creating class %s%s with attributes %s" % (name, bases, dict_))
            return type.__new__(cls, name, bases, dict_)

    @loud_decorator
    def hello(self, msg):
        print("Hello %s" % msg)

Foo().hello()

이 프로그램은 다음을 출력합니다.

Now decorating <function hello at 0xb74d35dc>
Creating class Foo(<type 'object'>,) with attributes {'__module__': '__main__', '__metaclass__': <class '__main__.__metaclass__'>, 'hello': <function decorated at 0xb74d356c>}
Now calling <function hello at 0xb74d35dc> with (<__main__.Foo object at 0xb74ea1ac>, 'World'),{}
Hello World

보시다시피, 원하는 작업을 수행하는 다른 방법을 찾아야합니다.


함수를 정의 할 때 함수는 아직 존재하지 않지만 자체 내에서 함수를 재귀 적으로 호출 할 수 있습니다. 나는 이것이 기능에 특정한 언어 기능이며 클래스에는 사용할 수 없다고 생각합니다.
Carl G

DGGenuine : 함수는 호출 될 뿐이므로 완전히 생성 된 후에 만 ​​함수가 자체적으로 액세스합니다. 이 경우, 클래스는 데코레이터의 결과를 기다려야하기 때문에 데코레이터가 호출 될 때 클래스가 완료 될 수 없으며, 이는 클래스의 속성 중 하나로 저장됩니다.
u0b34a0f6ae 2010 년

3

다음은 간단한 예입니다.

def mod_bar(cls):
    # returns modified class

    def decorate(fcn):
        # returns decorated function

        def new_fcn(self):
            print self.start_str
            print fcn(self)
            print self.end_str

        return new_fcn

    cls.bar = decorate(cls.bar)
    return cls

@mod_bar
class Test(object):
    def __init__(self):
        self.start_str = "starting dec"
        self.end_str = "ending dec" 

    def bar(self):
        return "bar"

출력은 다음과 같습니다.

>>> import Test
>>> a = Test()
>>> a.bar()
starting dec
bar
ending dec

1

이것은 오래된 질문이지만 Venusian을 발견했습니다. http://venusian.readthedocs.org/en/latest/

메서드를 장식하고 그렇게하는 동안 클래스와 메서드에 모두 액세스 할 수있는 기능이있는 것 같습니다. 호출 setattr(ob, wrapped.__name__, decorated)은 금성을 사용하는 일반적인 방법이 아니며 다소 목적에 위배됩니다.

어느 쪽이든 ... 아래 예제가 완료되었으며 실행되어야합니다.

import sys
from functools import wraps
import venusian

def logged(wrapped):
    def callback(scanner, name, ob):
        @wraps(wrapped)
        def decorated(self, *args, **kwargs):
            print 'you called method', wrapped.__name__, 'on class', ob.__name__
            return wrapped(self, *args, **kwargs)
        print 'decorating', '%s.%s' % (ob.__name__, wrapped.__name__)
        setattr(ob, wrapped.__name__, decorated)
    venusian.attach(wrapped, callback)
    return wrapped

class Foo(object):
    @logged
    def bar(self):
        print 'bar'

scanner = venusian.Scanner()
scanner.scan(sys.modules[__name__])

if __name__ == '__main__':
    t = Foo()
    t.bar()

1

함수는 데코레이터 코드가 실행될 때 정의 지점에서 메서드인지 여부를 알지 못합니다. 클래스 / 인스턴스 식별자를 통해 액세스 할 때만 해당 클래스 / 인스턴스를 알 수 있습니다. 이 한계를 극복하기 위해, 접근 / 호출 시간까지 실제 장식 코드를 지연시키기 위해 서술자 객체로 장식 할 수 있습니다 :

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

    def __get__(self, obj, type_=None):
        func = self.func.__get__(obj, type_)
        print('accessed %s.%s' % (type_.__name__, func.__name__))
        return self.__class__(func, 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

    @decorated
    @staticmethod
    def bar(a, b):
        pass

    @decorated
    @classmethod
    def baz(cls, a, b):
        pass

class Bar(Foo):
    pass

이제 내부 검사를 위해 데코레이터 코드를 사용할 수 있습니다.

>>> Foo.foo
accessed Foo.foo
>>> Foo.bar
accessed Foo.bar
>>> Foo.baz
accessed Foo.baz
>>> Bar.foo
accessed Bar.foo
>>> Bar.bar
accessed Bar.bar
>>> Bar.baz
accessed Bar.baz

... 그리고 함수 동작 변경 :

>>> Foo().foo(1, 2)
accessed Foo.foo
called Foo.foo with args=(1, 2) kwargs={}
>>> Foo.bar(1, b='bcd')
accessed Foo.bar
called Foo.bar with args=(1,) kwargs={'b': 'bcd'}
>>> Bar.baz(a='abc', b='bcd')
accessed Bar.baz
called Bar.baz with args=() kwargs={'a': 'abc', 'b': 'bcd'}

안타깝게도이 접근 방식은 Will McCutchen똑같이 적용 할 수없는 답변 과 기능적으로 동일합니다 . 이 대답과 그 대답은 모두 원래 질문에서 요구하는 것처럼 메서드 장식 시간이 아닌 메서드 호출 시간에 원하는 클래스를 얻습니다 . 충분히 초기에이 클래스를 얻는 유일한 합리적인 방법은 클래스 정의 시간에 모든 메서드를 검사하는 것입니다 (예 : 클래스 데코레이터 또는 메타 클래스를 통해). </sigh>
Cecil Curry

1

다른 답변에서 지적했듯이 데코레이터는 기능적인 것이므로 클래스가 아직 생성되지 않았 으므로이 메서드가 속한 클래스에 액세스 할 수 없습니다. 그러나 데코레이터를 사용하여 함수를 "표시"한 다음 메타 클래스 기술을 사용하여 나중에 메서드를 처리하는 것은 완전히 괜찮습니다. __new__단계에서 클래스는 해당 메타 클래스에 의해 생성 되었기 때문 입니다.

다음은 간단한 예입니다.

우리가 사용하는 @field특수 필드와 방법을 표시하고 메타 클래스에서 처리 할 수 있습니다.

def field(fn):
    """Mark the method as an extra field"""
    fn.is_field = True
    return fn

class MetaEndpoint(type):
    def __new__(cls, name, bases, attrs):
        fields = {}
        for k, v in attrs.items():
            if inspect.isfunction(v) and getattr(k, "is_field", False):
                fields[k] = v
        for base in bases:
            if hasattr(base, "_fields"):
                fields.update(base._fields)
        attrs["_fields"] = fields

        return type.__new__(cls, name, bases, attrs)

class EndPoint(metaclass=MetaEndpoint):
    pass


# Usage

class MyEndPoint(EndPoint):
    @field
    def foo(self):
        return "bar"

e = MyEndPoint()
e._fields  # {"foo": ...}

이 줄에 오타 if inspect.isfunction(v) and getattr(k, "is_field", False):가 있습니다 : getattr(v, "is_field", False)대신 있어야합니다 .
EvilTosha

0

데코레이터가 반환해야하는 데코 레이팅 된 메서드에서 메서드가 호출되는 객체의 클래스에 액세스 할 수 있습니다. 이렇게 :

def decorator(method):
    # do something that requires view's class
    def decorated(self, *args, **kwargs):
        print 'My class is %s' % self.__class__
        method(self, *args, **kwargs)
    return decorated

ModelA 클래스를 사용하여 수행하는 작업은 다음과 같습니다.

>>> obj = ModelA()
>>> obj.a_method()
My class is <class '__main__.ModelA'>

1
고마워하지만 이것은 내 질문에서 언급 한 솔루션이며 나를 위해 작동하지 않습니다. 데코레이터를 사용하여 관찰자 패턴을 구현하려고 시도하고 있으며 관찰 디스패처에 메서드를 추가하는 동안 어느 시점에서 클래스가 없으면 관찰 디스패처에서 올바른 컨텍스트에서 메서드를 호출 할 수 없습니다. 메서드 호출시 클래스를 얻는 것은 처음에 메서드를 올바르게 호출하는 데 도움이되지 않습니다.
Carl G

워, 전체 질문을 읽지 않은 게 게으름에 대해 죄송합니다.
Will McCutchen

0

데코 레이팅 된 메서드에서 클래스에 액세스하기 위해 생각할 수있는 모든 것이 포함되어 있으므로 예제를 추가하고 싶습니다. @tyrion이 제안한대로 설명자를 사용합니다. 데코레이터는 인수를 가져 와서 설명자에 전달할 수 있습니다. 클래스의 메서드 또는 클래스가없는 함수를 모두 처리 할 수 ​​있습니다.

import datetime as dt
import functools

def dec(arg1):
    class Timed(object):
        local_arg = arg1
        def __init__(self, f):
            functools.update_wrapper(self, f)
            self.func = f

        def __set_name__(self, owner, name):
            # doing something fancy with owner and name
            print('owner type', owner.my_type())
            print('my arg', self.local_arg)

        def __call__(self, *args, **kwargs):
            start = dt.datetime.now()
            ret = self.func(*args, **kwargs)
            time = dt.datetime.now() - start
            ret["time"] = time
            return ret
        
        def __get__(self, instance, owner):
            from functools import partial
            return partial(self.__call__, instance)
    return Timed

class Test(object):
    def __init__(self):
        super(Test, self).__init__()

    @classmethod
    def my_type(cls):
        return 'owner'

    @dec(arg1='a')
    def decorated(self, *args, **kwargs):
        print(self)
        print(args)
        print(kwargs)
        return dict()

    def call_deco(self):
        self.decorated("Hello", world="World")

@dec(arg1='a function')
def another(*args, **kwargs):
    print(args)
    print(kwargs)
    return dict()

if __name__ == "__main__":
    t = Test()
    ret = t.call_deco()
    another('Ni hao', world="shi jie")
    
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.