Декодер 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" внутри "logger", но я понятия не имею, что.

Ответ 1

Ответ 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, так как это правильный класс. С другой стороны, это может быть приемлемым, и в этом случае я бы рекомендовал этот подход по предложению Клаудиу.

Ответ 2

Функции только становятся методами во время выполнения. То есть, когда вы получаете 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)), но этот общий шаблон будет работать как для обоих методов, так и для функций. Однако это не будет работать для классов старого стиля (но просто не используйте их;)

Ответ 3

Идеи, предлагаемые здесь, превосходны, но имеют некоторые недостатки:

  • inspect.getouterframes и args[0].__class__.__name__ не подходят для простых функций и статических методов.
  • __get__ должен быть в классе, который отклоняется @wraps.
  • @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__()

Ответ 4

Кажется, что, пока класс создается, Python создает регулярные объекты функции. После этого они превращаются в объекты unbound method. Зная, что это единственный способ найти то, что вы хотите:

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)

Ответ 5

Функции класса всегда должны считать себя в качестве первого аргумента, поэтому вы можете использовать это вместо 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

Я нашел другое решение для очень похожей проблемы, используя библиотеку 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()

Хотя это не обязательно лучше, чем другие, это единственный способ узнать, как узнать имя класса будущего метода во время вызова декоратора. Обратите внимание на то, что ссылки на фреймы не поддерживаются в документации библиотеки inspect.

Ответ 7

Вы также можете использовать new.instancemethod() для создания метода экземпляра (связанного или несвязанного) из функции.

Ответ 8

Вместо того, чтобы вводить декорирование кода во время определения, когда функция не знает этого класса, задержка запуска этого кода до тех пор, пока функция не будет вызвана/вызвана. Объект дескриптора облегчает инъекцию собственного кода поздно, при времени доступа/вызова:

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__). Этот механизм работает как для простых методов, так и для статических методов класса:

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

Ответ 9

Как показано в ответе Асы Айерс, вам не нужен доступ к объекту класса. Возможно, стоит знать, что начиная с 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 метода экземпляра получить доступ к классу?"