Эквивалент functools.wraps для декоратора класса

Когда мы декорируем функцию, мы используем functools.wraps, чтобы декорированная функция выглядела как оригинал.

Есть ли что делать, когда мы хотим украсить класс?

def some_class_decorator(cls_to_decorate):
    class Wrapper(cls_to_decorate):
        """Some Wrapper not important doc."""
        pass
    return Wrapper


@some_class_decorator
class MainClass:
    """MainClass important doc."""
    pass


help(MainClass)

Вывод:

class Wrapper(MainClass)
 |  Some Wrapper not important doc.
 |  
 |  Method resolution order:
 |      Wrapper
 |      MainClass
 |      builtins.object
 |  
 # ... no MainClass important doc below.

Я попытался написать эквивалент обертки для декоратора класса на основе исходного кода functools.wraps, но моя реализация работает неправильно:

import functools


def wraps_cls(original_cls):
    def wrapper(wrapper_cls):
        """Update wrapper_cls to look like original_cls."""
        for attr in functools.WRAPPER_ASSIGNMENTS:
            try:
                value = getattr(original_cls, attr)
            except AttributeError:
                pass
            else:
                setattr(wrapper_cls, attr, value)
        return wrapper_cls
    return wrapper


def some_class_decorator(cls_to_decorate):
    @wraps_cls(cls_to_decorate)
    class Wrapper(cls_to_decorate):
        """Some Wrapper not important doc."""
        pass
    return Wrapper


@some_class_decorator
class MainClass:
    """MainClass important doc."""
    pass


help(MainClass)

Вывод:

class MainClass(MainClass)
 |  MainClass important doc.
 |  
 |  Method resolution order:
 |      MainClass
 |      MainClass
 |      builtins.object
 |  
 # ... MainClass doc is here but "Method resolution order" is broken.

Есть ли способ полностью заменить декорированный вывод справки MainClass недекорированным выводом справки MainClass?


person Mikhail Gerasimov    schedule 20.02.2015    source источник
comment
Ваш декоратор возвращает новый класс, который является подклассом исходного (обернутого) класса. Вы никак не можете добиться того же порядка разрешения методов, потому что вы добавили дополнительный класс в иерархию.   -  person BrenBarn    schedule 20.02.2015
comment
Спасибо, ваш метод wraps_cls помог мне обернуть класс параметрами, сохранив при этом имя класса.   -  person xlash    schedule 25.10.2018
comment
Использование @functools.wraps(cls, updated=()) спасло бы вас от написания собственной оболочки. Хотя страдает от той же проблемы.   -  person Adrien H    schedule 19.02.2020


Ответы (2)


Нет, если ваш декоратор действительно создает подклассы обернутого класса, как это делает some_class_decorator. Вывод help определяется классом pydoc.Helper, который для классов в конечном итоге вызывает pydoc.text.docclass, содержащий следующий код:

# List the mro, if non-trivial.
mro = deque(inspect.getmro(object))
if len(mro) > 2:
    push("Method resolution order:")
    for base in mro:
        push('    ' + makename(base))
    push('')

Вы можете видеть, что он жестко запрограммирован для отображения реального MRO класса. Это так и должно быть. MRO, отображаемый в вашем последнем примере, не «сломан», это правильно. Сделав свой класс-оболочку наследником обернутого класса, вы добавили дополнительный класс в иерархию наследования. Было бы ошибкой показывать MRO, в котором это не учитывалось, потому что там действительно есть класс. В вашем примере этот класс-оболочка не обеспечивает никакого собственного поведения, но реалистичный класс-оболочка будет (иначе зачем вам вообще делать обертку?), и вы хотели бы знать, какое поведение исходит от оболочки класс и который из обернутого класса.

Если вы хотите, вы можете создать декоратор, который динамически переименовывает класс-оболочку с некоторым именем, полученным из оригинала, поэтому MRO будет показывать что-то вроде DecoratorWrapper_of_MainClass в соответствующей позиции. Будет ли это более читаемым, чем просто наличие Wrapper, спорный вопрос.

person BrenBarn    schedule 20.02.2015
comment
и вы хотели бы знать, какое поведение исходит из класса-оболочки, а какое из класса-оболочки - да, это правда, но, как и в случае с декорированной функцией (functools.wraps), оригинальная документация MainClass важнее, чем документация Wrapper. Получается, что если я декорирую класс, я не могу увидеть документ MainClass с помощью help(). Но я понял ваше объяснение, спасибо. Возможно, лучшим способом было бы просто добавить документ MainClass в документ Wrapper в wraps_cls. - person Mikhail Gerasimov; 20.02.2015
comment
@GerasimovMikhail: Но это отдельная тема. Нетрудно написать оболочку, которая показывает вам __doc__; ваше существующее решение уже делает это, как вы видели. Но help показывает вам больше, чем просто __doc__, и вы не можете вмешиваться в этот другой вывод; вы контролируете только __doc__. - person BrenBarn; 20.02.2015

О, я думаю, теперь я понимаю, чего вы пытаетесь достичь.

Вы хотите прикрепить новые методы из "обертки" с помощью декоратора класса.

Вот рабочий пример:

class Wrapper(object):
    """Some Wrapper not important doc."""
    def another_method(self):
        """Another method."""
        print 'Another method'


def some_class_decorator(cls_to_decorate):
    return type(cls_to_decorate.__name__, cls_to_decorate.__bases__, dict
        (cls_to_decorate.__dict__, another_method=Wrapper.__dict__['another_method']))


class MainClass(object):
    """MainClass important doc."""
    def method(self):
        """A method."""
        print "A method"


help(MainClass)
_MainClass = some_class_decorator(MainClass)
help(_MainClass)
_MainClass().another_method()
MainClass().another_method()

В этом примере создается новый класс, не изменяющий старый класс.

Но я думаю, вы также можете просто внедрить данные методы в старый класс, изменив его на месте.

person warvariuc    schedule 20.02.2015
comment
Спасибо. Этот способ может быть решением. Но я думаю, что БренБарн прав в том, что help() должен показывать настоящий MRO. Я думаю, что изменить имя класса Wrapper (и, возможно, doc) было бы более правильным, чем пытаться полностью заменить вывод help(), как я хотел в первую очередь. - person Mikhail Gerasimov; 20.02.2015
comment
Это решение показывает реальный MRO, потому что вы не наследуете обернутый класс от оболочки. Вы просто берете несколько методов от донора. Но зачем тебе это нужно? Почему бы не сделать класс миксина вместо того, чтобы ковыряться с декораторами? - person warvariuc; 20.02.2015