Czy dekorator metody instancji może uzyskać dostęp do klasy?

109

Mam coś mniej więcej podobnego do następującego. Zasadniczo muszę uzyskać dostęp do klasy metody instancji z dekoratora używanego w metodzie instancji w jej definicji.

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

Kod taki jaki jest daje:

AttributeError: obiekt „function” nie ma atrybutu „im_class”

Znalazłem podobne pytanie / odpowiedzi - dekorator Pythona sprawia, że ​​funkcja zapomina, że ​​należy do klasy, a klasa Get w dekoratorze Pythona - ale te polegają na obejściu, które przechwytuje instancję w czasie wykonywania, przechwytując pierwszy parametr. W moim przypadku będę wywoływał metodę na podstawie informacji zebranych z jej klasy, więc nie mogę się doczekać, aż zadzwoni.

Carl G
źródło

Odpowiedzi:

68

Jeśli używasz Pythona 2.6 lub nowszego, możesz użyć dekoratora klas, być może czegoś takiego (ostrzeżenie: nieprzetestowany kod).

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

Dekorator metody oznacza metodę jako interesującą poprzez dodanie atrybutu „use_class” - funkcje i metody są również obiektami, więc można do nich dołączyć dodatkowe metadane.

Po utworzeniu klasy dekorator klas przechodzi przez wszystkie metody i robi wszystko, co jest potrzebne, na metodach, które zostały zaznaczone.

Jeśli chcesz, aby miało to wpływ na wszystkie metody, możesz pominąć dekorator metody i po prostu użyć dekoratora klas.

Dave Kirby
źródło
2
Dzięki, myślę, że to jest trasa, którą należy iść. Tylko jedna dodatkowa linia kodu dla dowolnej klasy, której chciałbym użyć tego dekoratora. Może mógłbym użyć niestandardowej metaklasy i wykonać to samo sprawdzenie podczas nowego ...?
Carl G
3
Każdy, kto spróbuje tego użyć z staticmethod lub classmethod, będzie chciał przeczytać ten PEP: python.org/dev/peps/pep-0232 Nie jestem pewien, czy to możliwe, ponieważ nie możesz ustawić atrybutu w metodzie klasowej / statycznej i myślę, że gobble w górę wszelkich niestandardowych atrybutów funkcji, gdy są stosowane do funkcji.
Carl G,
Właśnie tego szukałem, dla mojego ORM opartego na DBM ... Dzięki, stary.
Coyote21
Należy użyć inspect.getmro(cls)do przetwarzania wszystkich klas bazowych w dekoratorze klas w celu obsługi dziedziczenia.
schlamar
1
och, właściwie to wygląda inspectna ratunek stackoverflow.com/a/1911287/202168
Anentropic
16

Od Pythona 3.6 możesz tego użyć object.__set_name__w bardzo prosty sposób. Dokument stwierdza, że __set_name__jest „wywoływany w momencie tworzenia właściciela klasy będącej właścicielem ”. Oto przykład:

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)

Zauważ, że jest wywoływany w czasie tworzenia klasy:

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

Jeśli chcesz dowiedzieć się więcej o tym, jak powstają zajęcia, a zwłaszcza kiedy __set_name__ jest wywoływana, zapoznaj się z dokumentacją „Tworzenie obiektu klasy” .

tyrion
źródło
1
Jak by to wyglądało w przypadku używania dekoratora z parametrami? Np.@class_decorator('test', foo='bar')
luckydonald
2
@luckydonald Możesz podejść do tego podobnie do zwykłych dekoratorów, które przyjmują argumenty . Po prostu mamdef decorator(*args, **kwds): class Descriptor: ...; return Descriptor
Matt Eding,
Wow, bardzo dziękuję. Nie wiedziałem, __set_name__chociaż używam Pythona 3.6+ od dłuższego czasu.
kawing-chiu
Jest jedna wada tej metody: kontroler statyczny w ogóle tego nie rozumie. Mypy pomyśli, że helloto nie jest metoda, ale jest to obiekt typu class_decorator.
kawing-chiu
@ kawing-chiu Jeśli nic innego nie działa, możesz użyć znaku, if TYPE_CHECKINGaby zdefiniować class_decoratorjako normalny dekorator zwracający prawidłowy typ.
tyrion
15

Jak zauważyli inni, klasa nie została utworzona w momencie wywołania dekoratora. Jednakże , możliwe jest opisanie przedmiotu funkcyjny parametrów dekorator, a następnie ponownie dekoracji funkcję metaklasą w __new__metodzie. Będziesz musiał uzyskać __dict__bezpośredni dostęp do atrybutu funkcji , co najmniej dla mnie, func.foo = 1spowodowało powstanie AttributeError.

Mark Visser
źródło
6
setattrpowinno być używane zamiast dostępu__dict__
schlamar
7

Jak sugeruje Mark:

  1. Każdy dekorator nosi nazwę PRZED zbudowaniem klasy, więc dekorator nie zna go.
  2. Możemy oznaczyć te metody i później wykonać niezbędny proces końcowy.
  3. Mamy dwie opcje przetwarzania końcowego: automatycznie na końcu definicji klasy lub gdzieś przed uruchomieniem aplikacji. Wolę pierwszą opcję używającą klasy bazowej, ale możesz również zastosować drugą metodę.

Ten kod pokazuje, jak to może działać przy użyciu automatycznego przetwarzania końcowego:

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

Wydajność daje:

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}

Zwróć uwagę, że w tym przykładzie:

  1. Do każdej funkcji możemy dodać dowolne parametry.
  2. Każda klasa ma własne uwidocznione metody.
  3. Możemy również dziedziczyć ujawnione metody.
  4. metody mogą być nadpisywane, gdy funkcja ujawniania jest aktualizowana.

Mam nadzieję że to pomoże

asterio gonzalez
źródło
4

Jak wskazał Ants, nie można uzyskać odwołania do klasy z poziomu tej klasy. Jeśli jednak chcesz rozróżniać różne klasy (a nie manipulować rzeczywistym obiektem typu klasy), możesz przekazać ciąg znaków dla każdej klasy. Możesz również przekazać dekoratorowi dowolne inne parametry, używając dekoratorów w stylu klasowym.

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

Wydruki:

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

Zobacz także stronę Bruce'a Eckela o dekoratorach.

Ross Rogers
źródło
Dziękuję za potwierdzenie mojego przygnębiającego wniosku, że nie jest to możliwe. Mógłbym również użyć ciągu znaków, który w pełni kwalifikuje moduł / klasę („module.Class”), przechowywać ciąg (i) do momentu pełnego załadowania wszystkich klas, a następnie samodzielnie pobrać klasy za pomocą importu. To wydaje się żałośnie nie-SUCHY sposób na wykonanie mojego zadania.
Carl G
Nie musisz używać klasy dla tego rodzaju dekoratora: idiomatycznym podejściem jest użycie jednego dodatkowego poziomu funkcji zagnieżdżonych wewnątrz funkcji dekoratora. Jeśli jednak korzystasz z klas, lepiej byłoby nie używać wielkich liter w nazwie klasy, aby sama dekoracja wyglądała „standardowo”, tj @decorator('Bar'). W przeciwieństwie do @Decorator('Bar').
Erik Kaplun,
4

To, co robi flask-classy , to utworzenie tymczasowej pamięci podręcznej, którą przechowuje w metodzie, a następnie używa czegoś innego (fakt, że Flask zarejestruje klasy przy użyciu registermetody klas), aby faktycznie opakować metodę.

Możesz ponownie użyć tego wzorca, tym razem używając metaklasy, aby móc opakować metodę w czasie importu.

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

Na rzeczywistej klasie (możesz zrobić to samo, używając metaklasy):

@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

Źródło: https://github.com/apiguy/flask-classy/blob/master/flask_classy.py

charlax
źródło
to przydatny wzorzec, ale nie rozwiązuje to problemu dekoratora metody, który jest w stanie odwołać się do klasy nadrzędnej metody, do której jest stosowany
Anentropic
Zaktualizowałem moją odpowiedź, aby była bardziej wyraźna, w jaki sposób może to być przydatne, aby uzyskać dostęp do klasy w czasie importu (tj. Używając metaklasy + buforowanie parametru dekoratora w metodzie).
charlax
3

Problem polega na tym, że gdy wywoływany jest dekorator, klasa jeszcze nie istnieje. Spróbuj tego:

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

Ten program wyświetli:

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

Jak widzisz, będziesz musiał wymyślić inny sposób robienia tego, co chcesz.

Ants Aasma
źródło
kiedy definiuje się funkcję, funkcja jeszcze nie istnieje, ale można ją rekurencyjnie wywołać z jej wnętrza. Wydaje mi się, że jest to funkcja języka specyficzna dla funkcji i niedostępna dla klas.
Carl G
DGG Genuine: Funkcja jest wywoływana, a zatem funkcja uzyskuje do niej dostęp dopiero po całkowitym utworzeniu. W takim przypadku klasa nie może być kompletna, gdy wywoływany jest dekorator, ponieważ klasa musi czekać na wynik dekoratora, który zostanie zapisany jako jeden z atrybutów klasy.
u0b34a0f6ae
3

Oto prosty przykład:

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"

Wynik to:

>>> import Test
>>> a = Test()
>>> a.bar()
starting dec
bar
ending dec
nicodjimenez
źródło
1

To stare pytanie, ale trafiło na wenusjański. http://venusian.readthedocs.org/en/latest/

Wydaje się, że ma możliwość dekorowania metod i zapewnia dostęp zarówno do klasy, jak i do metody. Zwróć uwagę, że dzwonienie setattr(ob, wrapped.__name__, decorated)nie jest typowym sposobem używania wenusjańskiego i w pewnym sensie przeczy celowi.

Tak czy inaczej ... poniższy przykład jest kompletny i powinien działać.

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()
eric.frederich
źródło
1

Funkcja nie wie, czy jest to metoda w punkcie definicji, kiedy działa kod dekoratora. Tylko wtedy, gdy uzyskuje się do niego dostęp za pośrednictwem identyfikatora klasy / instancji, może znać swoją klasę / instancję. Aby przezwyciężyć to ograniczenie, możesz dekorować za pomocą obiektu deskryptora, aby opóźnić faktyczny kod dekorujący do czasu dostępu / połączenia:

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)

Pozwala to na dekorowanie pojedynczych (statycznych | klasowych) metod:

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

Teraz możesz użyć kodu dekoratora do introspekcji ...

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

... i do zmiany zachowania funkcji:

>>> 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'}
aurzenligl
źródło
Niestety, takie podejście jest funkcjonalnym odpowiednikiem Will McCutchen „s równie nieskuteczne w stosunku odpowiedź . Zarówno ta, jak i ta odpowiedź uzyskują żądaną klasę w czasie wywołania metody, a nie w czasie dekoracji metody , zgodnie z pierwotnym pytaniem. Jedynym rozsądnym sposobem uzyskania tej klasy w wystarczająco wczesnym czasie jest introspekcja wszystkich metod w czasie definiowania klasy (np. Poprzez dekorator klas lub metaklasę). </sigh>
Cecil Curry
1

Jak wskazywały inne odpowiedzi, dekorator jest rzeczą związaną z funkcją, nie możesz uzyskać dostępu do klasy, do której należy ta metoda, ponieważ klasa ta nie została jeszcze utworzona. Jednak całkowicie w porządku jest użyć dekoratora do „zaznaczenia” funkcji, a następnie użyć technik metaklas, aby zająć się metodą później, ponieważ na __new__etapie klasa została utworzona za pomocą metaklasy.

Oto prosty przykład:

Używamy @fielddo oznaczenia metody jako specjalnego pola i zajmujemy się nią w metaklasie.

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": ...}
ospider
źródło
Masz literówkę w tym wierszu: zamiast if inspect.isfunction(v) and getattr(k, "is_field", False):tego powinno być getattr(v, "is_field", False).
EvilTosha
0

Będziesz mieć dostęp do klasy obiektu, dla którego wywoływana jest metoda w metodzie dekorowanej, którą Twój dekorator powinien zwrócić. Tak jak to:

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

Używając klasy ModelA, oto co to robi:

>>> obj = ModelA()
>>> obj.a_method()
My class is <class '__main__.ModelA'>
Will McCutchen
źródło
1
Dzięki, ale to jest dokładnie to rozwiązanie, o którym wspomniałem w swoim pytaniu, które nie działa dla mnie. Próbuję zaimplementować wzorzec obserwatora za pomocą dekoratorów i nigdy nie będę w stanie wywołać metody w odpowiednim kontekście z mojego dyspozytora obserwacji, jeśli nie mam klasy w pewnym momencie podczas dodawania metody do modułu wysyłającego obserwacje. Uzyskanie klasy po wywołaniu metody nie pomaga mi w prawidłowym wywołaniu metody.
Carl G
Przepraszam za moje lenistwo, że nie przeczytałem całego twojego pytania.
Will McCutchen
0

Chcę tylko dodać mój przykład, ponieważ zawiera on wszystkie rzeczy, o których mógłbym pomyśleć, aby uzyskać dostęp do klasy z metody dekorowanej. Używa deskryptora, jak sugeruje @tyrion. Dekorator może przyjąć argumenty i przekazać je do deskryptora. Może obsługiwać zarówno metodę w klasie, jak i funkcję bez klasy.

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")
    
djiao
źródło