Python — использование decorator.py для сохранения строки документации метода

Меня интересует вызов метода экземпляра как метода класса, так и метода экземпляра. Это можно сделать с помощью декоратора class_or_instance следующим образом:

class class_or_instance(object):
    def __init__(self, fn):
        self.fn = fn

    def __get__(self, obj, cls):
        if obj is not None:
            return lambda *args, **kwds: self.fn(obj, *args, **kwds)
        else:
            return lambda *args, **kwds: self.fn(cls, *args, **kwds)

class A(object):
    @class_or_instance
    def func1(self,*args):
         # method body

Теперь я могу вызвать func1 либо как A.func1(*args), либо как A().func1(*args). Однако при этом строка документации func1 исчезает. Один из способов справиться с этим — использовать декоратор из decorator.py, но у меня возникли проблемы с тем, чтобы это работало с декоратором, который является классом, а не функцией. Любые предложения о том, как это сделать?

РЕДАКТИРОВАТЬ: functools.wraps() в этом случае работать не будет. См. похожий вопрос в stackoverflow.


person rk7    schedule 18.06.2013    source источник
comment
Почему бы не использовать @classmethod или @staticmethod?   -  person Ulrich Eckhardt    schedule 18.06.2013
comment
@classmethod позволит ему вести себя только как метод класса. Я хочу иметь возможность вызывать его либо как класс, либо как метод экземпляра.   -  person rk7    schedule 18.06.2013
comment
Если вы вызываете метод класса для экземпляра, первый параметр является ссылкой не на экземпляр (self), а на его класс (cls). Единственное отличие от статического метода заключается в том, что этот первый параметр cls отсутствует, но в остальном они ведут себя одинаково.   -  person Ulrich Eckhardt    schedule 18.06.2013


Ответы (1)


Базовый дескриптор/декоратор

Вам просто нужно помнить, какую функцию вы должны декорировать. Ваша функция создается в __get__, поэтому использование обертки в качестве декоратора не поможет, вместо этого вам нужно применить ее в методе __get__. Кроме того, для этого вы можете использовать либо functools.update_wrapper, либо decorators.decorator. Они работают очень похоже, за исключением того, что вы должны сохранить результат decorators.decorator, тогда как functools.update_wrapper возвращает None. Оба имеют подпись f(wrapper, wrapped).

from functools import update_wrapper
class class_or_instance(object):
    def __init__(self, fn):
        self.fn = fn

    def __get__(self, obj, cls):
        if obj is not None:
            f = lambda *args, **kwds: self.fn(obj, *args, **kwds)
        else:
            f = lambda *args, **kwds: self.fn(cls, *args, **kwds)
        # update the function to have the correct metadata
        update_wrapper(f, self.fn)
        return f

class A(object):
    @class_or_instance
    def func1(self,*args):
        """some docstring"""
        pass

Теперь, если вы сделаете:

print A.func1.__doc__

Вы увидите «какую-то строку документации». Ура!


Кэшированный декоратор свойств

Ключевым моментом здесь является то, что вы можете влиять только на то, что возвращается. Поскольку class_or_instance на самом деле не служит функцией, на самом деле не имеет значения, что вы с ним делаете. Имейте в виду, что этот метод приводит к тому, что функция каждый раз перестраивается. Вместо этого я предлагаю вам добавить немного магии и связать/кэшировать функцию после первого вызова, который на самом деле просто включает добавление вызова setattr.

from functools import update_wrapper
import types

class class_or_instance(object):
    # having optional func in case is passed something that doesn't have a correct __name__
    # (like a lambda function)
    def __init__(self, name_or_func):
        self.fn = fn
        self.name = fn.__name__

    def __get__(self, obj, cls):
        print "GET!!!"
        if obj is not None:
            f = lambda *args, **kwds: self.fn(obj, *args, **kwds)
            update_wrapper(f, self.fn)
            setattr(obj, self.name, types.MethodType(f, obj, obj.__class__))
        else:
            f = lambda *args, **kwds: self.fn(cls, *args, **kwds)
            update_wrapper(f, self.fn)
        return f

И тогда мы можем проверить это... neato:

A.func1 #GET!!!
obj = A()
obj.func1 #GET!!!
obj.func1 is obj.func1 # True
A.func1 # GET!!!
obj2 = A()
obj2.func1 is not obj.fun1 # True + GET!!!
person Jeff Tratner    schedule 18.06.2013
comment
Спасибо, что оба предложенных вами метода правильно возвращают строки документации. Однако использование decorator.decorator имеет проблему с количеством аргументов, переданных декорированному методу, и когда метод вызывается, он жалуется на TypeError. С другой стороны, техника update_wrapper работает безотказно. Мне интересно, в чем проблема с первой техникой? - person rk7; 18.06.2013
comment
@rk7, поэтому decorators.decorator и functools.update_wrapper не настроены для совместимости друг с другом. Я не думаю, что вы можете использовать decorators.decorator, потому что он не настроен для обработки методов (у которых self является первым аргументом)... для этого потребуется много взлома... не уверен, что оно того стоит. - person Jeff Tratner; 20.06.2013