Python decorator sprawia, że funkcja zapomina, że należy do klasy

Próbuję napisać dekoratora do logowania:

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

Chciałbym to wydrukować:

Entering C.f

Ale zamiast tego dostaję komunikat o błędzie:

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

Prawdopodobnie ma to coś wspólnego z zakresem 'myFunc' wewnątrz 'loggera', ale nie mam pojęcia co.

Author: xian, 2008-11-20

9 answers

Odpowiedź Claudiu jest poprawna, ale można również oszukiwać, pobierając nazwę klasy z argumentu self. To da mylące instrukcje logu w przypadku dziedziczenia, ale powie Ci klasę obiektu, którego metoda jest wywoływana. Na przykład:

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

Jak powiedziałem, to nie będzie działać poprawnie w przypadkach, gdy odziedziczyłeś funkcję po klasie rodzica; w tym przypadku możesz powiedzieć

class B(C):
    pass

b = B()
b.f()

I pobierz wiadomość Entering B.f gdzie naprawdę chcesz uzyskać wiadomość Ponieważ to właściwa Klasa. Z drugiej strony, to może być do przyjęcia, w takim przypadku polecam takie podejście zamiast sugestii Claudiu.

 47
Author: Eli Courtwright,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-21 14:34:35

Funkcje stają się metodami tylko w czasie wykonywania. Oznacza to, że gdy otrzymujesz C.f, otrzymujesz funkcję związaną (oraz C.f.im_class is C). W momencie zdefiniowania funkcji jest ona tylko funkcją prostą, nie jest związana z żadną klasą. Ta niezwiązana i niezwiązana funkcja jest tym, co dekoruje logger.

self.__class__.__name__ poda ci nazwę klasy, ale możesz również użyć deskryptorów, aby to osiągnąć w nieco bardziej ogólny sposób. Wzór ten jest opisany w wpisie na blogu o Dekoratorach i Deskryptory , a implementacja twojego dekoratora loggera w szczególności wyglądałaby następująco:

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

Oczywiście wyjście można poprawić (używając na przykład getattr(self.func, 'im_class', None)), ale ten ogólny wzorzec będzie działał zarówno dla metod, jak i funkcji. Jednak to będzie Nie pracować dla klas starego stylu (ale po prostu nie korzystać z nich;)

 29
Author: ianb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-06-09 07:27:16

Pomysły proponowane tutaj są doskonałe, ale mają pewne wady:

  1. inspect.getouterframes i args[0].__class__.__name__ nie są odpowiednie dla funkcji prostych i metod statycznych.
  2. __get__ musi być w klasie, która jest odrzucana przez @wraps.
  3. Sama powinna lepiej ukrywać ślady.
Tak więc, połączyłem kilka pomysłów z tej strony, linków, dokumentów i mojej własnej głowy.]} i wreszcie znalazłem rozwiązanie, które nie ma wszystkich trzech wad powyżej.

W wyniku, method_decorator:

  • zna klasę, do której jest przypisana metoda dekorowania.
  • ukrywa ślady dekoratora, odpowiadając na atrybuty systemu bardziej poprawnie niż functools.wraps().
  • jest pokryta testami jednostkowymi dla wiązania niezwiązanej instancji-metod, klas-metod, statycznych-metod i zwykłych funkcji.

Użycie:

pip install method_decorator
from method_decorator import method_decorator

class my_decorator(method_decorator):
    # ...

Zobacz pełne testy jednostkowe dla szczegółów użytkowania .

A oto tylko Kod klasy 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__()
 18
Author: Denis Ryzhkov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-02-04 15:04:27

Wygląda na to, że podczas tworzenia klasy Python tworzy regularne obiekty funkcyjne. Dopiero potem zostają przekształcone w niezwiązane obiekty metody. Wiedząc o tym, to jedyny sposób, w jaki mogę znaleźć, aby zrobić to, co chcesz:

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

Daje to pożądany rezultat.

Jeśli chcesz zawinąć wszystkie metody w klasę, prawdopodobnie chcesz utworzyć funkcję wrapClass, której możesz użyć w następujący sposób:

C = wrapClass(C)
 7
Author: Claudiu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-20 17:47:52

Funkcje klasowe powinny zawsze przyjmować self jako pierwszy argument, więc można go używać zamiast 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()

Na początku chciałem użyć self.__name__, ale to nie działa, ponieważ instancja nie ma nazwy. musisz użyć self.__class__.__name__, aby uzyskać nazwę klasy.

 6
Author: Asa Ayers,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-20 18:05:01

Znalazłem inne rozwiązanie bardzo podobnego problemu używając biblioteki inspect. Kiedy dekorator jest wywoływany, nawet jeśli funkcja nie jest jeszcze powiązana z klasą, możesz sprawdzić stos i dowiedzieć się, która klasa wywołuje dekorator. Możesz przynajmniej uzyskać nazwę łańcuchową klasy, jeśli to wszystko, czego potrzebujesz (prawdopodobnie nie możesz się do niej odwołać, ponieważ jest tworzona). Wtedy nie musisz niczego wywoływać po utworzeniu klasy.

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

Choć nie jest to koniecznie lepiej niż pozostałe, jest to tylko } sposób, w jaki mogę odkryć nazwę klasy przyszłej metody podczas wywołania dekoratora. Należy pamiętać, aby w dokumentacji biblioteki inspect nie przechowywać odniesień do ramek.

 6
Author: user398139,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-07-22 13:15:05

Jak pokazano w Asa Ayers' answer , nie musisz mieć dostępu do obiektu klasy. Warto wiedzieć, że od wersji Pythona 3.3 można również używać __qualname__, co daje pełną kwalifikowaną nazwę:

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

Ma to tę dodatkową zaletę, że działa również w przypadku klas zagnieżdżonych, jak pokazano w tym przykładzie zaczerpniętym z 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'

Zauważ również, że w Pythonie 3 atrybut im_class zniknął, dlatego jeśli naprawdę chcesz uzyskać dostęp do klasy w a dekoratorze, potrzebujesz innej metody. Podejście, z którego obecnie korzystam, obejmuje object.__set_name__ i jest szczegółowo w moja odpowiedź na "czy dekorator Pythona metody instancji może uzyskać dostęp do klasy?"

 2
Author: tyrion,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-01-22 21:26:06

Możesz również użyć new.instancemethod() do utworzenia metody instancji (bound lub unbound) z funkcji.

 0
Author: Andrew Beyer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-20 18:12:34

Zamiast wstrzykiwać dekorowanie kodu w czasie definicji, gdy funkcja nie wie, że jest klasą, opóźnij uruchomienie tego kodu, dopóki funkcja nie zostanie wywołana. Obiekt deskryptora ułatwia wprowadzanie własnego kodu późno, w czasie dostępu/wywołania:

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

Teraz możemy sprawdzić klasę zarówno w czasie dostępu (__get__), jak i w czasie wywołania (__call__). Mechanizm ten działa zarówno dla metod prostych, jak i metod statycznych/klasowych:

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

Pełny przykład na: https://github.com/aurzenligl/study/blob/master/python-robotwrap/Example4.py

 0
Author: aurzenligl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-28 20:47:57