Распространение декораторов класса на унаследованные классы

import inspect
import functools

def for_all_test_methods(decorator):
    def decorate(cls):
        for name, value in inspect.getmembers(cls, inspect.isroutine):
            if name.startswith('test'):
                setattr(cls, name, test_decorator(getattr(cls, name)))
        return cls
    return decorate

def test_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(func.__name__, args, kwargs)
        res = func(*args, **kwargs)
        return res
    return wrapper

@for_all_test_methods(test_decorator)
class Potato(object):
    def test_method(self):
        print('in method')

class Spud(Potato):
    def test_derived(self):
        print('in derived')

Теперь, если я создам экземпляр spud, test_method, который он унаследовал, останется украшенным, но у него будет недекорированный метод test_derived. К сожалению, если я добавлю декоратор класса к Spud, то его test_method будет декорирован дважды!

Как правильно передать декораторы из родительского класса в дочерние?


person wim    schedule 02.01.2014    source источник


Ответы (2)


Вот как вы можете добиться этого, используя метакласс вместо украшения класса:

import inspect
import functools

def test_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(func.__name__, args, kwargs)
        res = func(*args, **kwargs)
        return res
    return wrapper

def make_test_deco_type(decorator):
    class TestDecoType(type):
        def __new__(cls, clsname, bases, dct):
            for name, value in dct.items():
                if name.startswith('test') and inspect.isroutine(value):
                    dct[name] = decorator(value)
            return super().__new__(cls, clsname, bases, dct)
    return TestDecoType

class Potato(object, metaclass=make_test_deco_type(test_decorator)):
    def test_method(self):
        print('in method')

class Spud(Potato):
    def test_derived(self):
        print('in derived')

В Python 2.x вы должны использовать __metaclass__ = make_test_deco_type(test_decorator) в качестве первой строки тела класса вместо части metaclass=... оператора класса. Вам также нужно будет заменить super() на super(TestDecoType, cls).

person Andrew Clark    schedule 02.01.2014
comment
Это выглядит лучше, чем маршрут, по которому я шел. Как вы обрабатываете строку super на python2? - person wim; 03.01.2014
comment
Но самость в это время не определена. Я использую super(TestDecoType, cls)? - person wim; 03.01.2014

Вы не можете избежать украшения производных классов; вы можете найти подклассы класса после того, как подклассы были украшены, но не можете их автоматически украшать. Используйте метакласс вместо того, чтобы вам нужно было такое поведение.

Вы можете сделать одно из двух:

  1. Обнаружение уже оформленных методов; если есть атрибут __wrapped__, у вас есть оболочка:

    def for_all_test_methods(decorator):
        def decorate(cls):
            for name, value in inspect.getmembers(cls, inspect.isroutine):
                if name.startswith('test') and not hasattr(value, '__wrapped__'):
                    setattr(cls, name, test_decorator(getattr(cls, name)))
            return cls
        return decorate
    
  2. Ограничьте декоратор класса только прямыми методами:

    def for_all_test_methods(decorator):
        def decorate(cls):
            for name, value in cls.__dict__.iteritems():
                if name.startswith('test') and inspect.isroutine(value)):
                    setattr(cls, name, test_decorator(getattr(cls, name)))
            return cls
        return decorate
    
person Martijn Pieters    schedule 02.01.2014
comment
Это, сэр, отличный ответ. - person Hyperboreus; 03.01.2014
comment
1. Не мог ли __wrapped__ быть уже установлен каким-то другим декоратором? 2. Тогда мне нужно будет украсить и производный класс? - person wim; 03.01.2014
comment
@wim: Да, __wrapped__ мог быть установлен другим декоратором. Вы конечно должны декорировать производные классы. - person Martijn Pieters; 03.01.2014