Принудительная реализация метода во всех наследуемых классах

У меня есть ситуация, в которой я хочу применить каждый класс, наследуемый от определенного (абстрактного) класса, для реализации метода. Это то, чего я обычно добиваюсь, используя @abstractmethod. Однако, учитывая эту ситуацию множественного наследования:

from abc import ABCMeta, abstractmethod
class A(object):
    __metaclass__ = ABCMeta

    @abstractmethod
    def very_specific_method(self):
        pass

class B(A):
    def very_specific_method(self):
        print 'doing something in B'

class C(B):
    pass

Я также хочу применить C для реализации метода. Я хочу, чтобы каждый класс, прямо или косвенно наследующий A, был вынужден реализовать этот метод. Это возможно?

Пояснение: я хочу, чтобы это применялось к конкретному методу, а не ко всем абстрактным методам. абстрактные методы должны продолжать работать так же, но, возможно, следует создать новый декоратор, сигнализирующий о методах другого типа.

Примечание: я использовал abc в вопросе, потому что это кажется наиболее связанным с проблемой. Я понимаю, как обычно работают абстрактные методы, и регулярно их использую. Это другая ситуация, и я не возражаю, если это не будет сделано через abc.


person Korem    schedule 03.09.2014    source источник
comment
Возможно, вы могли бы создать исключение NotImplementedException в базовом классе.   -  person user3557327    schedule 03.09.2014
comment
@user3557327 user3557327 -- Нет, потому что very_specific_method B не вызывает базовый класс.   -  person mgilson    schedule 03.09.2014
comment
Плохо, я не заметил, что C расширял B, а не A. Вы можете проверить, был ли метод реализован в классе с помощью 'very_specific_method' in vars(C). Я мало что знаю о метаклассах, но с их помощью вы можете проверить это во время создания класса.   -  person user3557327    schedule 03.09.2014
comment
Возможно или нет я не знаю. Однако, желая навязать эту реализацию, вы побеждаете парадигму наследования, в которой B наследует от A, что означает, что B является A. B больше не является A, если он не может использовать свой очень_специфический_метод так же, как это делает A.   -  person Didier Trosset    schedule 03.09.2014
comment
@DidierTrosset C, B и A могут реализовать этот метод, и это все равно будет правильным наследованием. Единственная разница заключается в соблюдении этого. В любом случае, и при всем уважении, хорошо, поэтому я нарушаю парадигму наследования.   -  person Korem    schedule 03.09.2014


Ответы (2)


Модифицированная версия ABCMeta должна помочь.

Здесь вместо проверки методов с __isabstractmethod__, установленным в True, только в базовых классах, мы можем проверить, находится ли он в MRO класса, и если он найден в каком-либо классе в MRO и отсутствует в текущем классе, мы можем добавить это к набору abstracts.

from abc import ABCMeta, abstractmethod
from _weakrefset import WeakSet

class EditedABCMeta(ABCMeta):

    def __new__(mcls, name, bases, namespace):
        cls = type.__new__(mcls, name, bases, namespace)
        # Compute set of abstract method names
        abstracts = set(name
                     for name, value in namespace.items()
                     if getattr(value, "__isabstractmethod__", False))

        for base in cls.__mro__:
            for name, value in base.__dict__.items():
                if getattr(value, "__isabstractmethod__", False) and name not in cls.__dict__:
                    abstracts.add(name)

        cls.__abstractmethods__ = frozenset(abstracts)
        # Set up inheritance registry
        cls._abc_registry = WeakSet()
        cls._abc_cache = WeakSet()
        cls._abc_negative_cache = WeakSet()
        cls._abc_negative_cache_version = ABCMeta._abc_invalidation_counter
        return cls

class A(object):
    __metaclass__ = EditedABCMeta

    @abstractmethod
    def veryspecificmethod(self):
        pass

class B(A):
    def veryspecificmethod(self):
        print 'doing something in B'

    @abstractmethod
    def foo(self):
        print 'foo from B'

class C(B):
    def foo(self):
        pass

class D(C, B):
    pass

if __name__ == '__main__':
    for cls in (C, D):
        try:
            cls().veryspecificmethod
        except TypeError as e:
            print e.message
    print '-'*20
    for cls in (C, D):
        try:
            cls().foo
        except TypeError as e:
            print e.message

Вывод:

Can't instantiate abstract class C with abstract methods veryspecificmethod
Can't instantiate abstract class D with abstract methods foo, veryspecificmethod
--------------------
Can't instantiate abstract class C with abstract methods veryspecificmethod
Can't instantiate abstract class D with abstract methods foo, veryspecificmethod

ИЗМЕНИТЬ:

Добавление специального декоратора @enforcedmethod, который может удовлетворить ваши требования, не затрагивая @abstractmethod:

from abc import ABCMeta, abstractmethod

def enforcedmethod(func):
    func.__enforcedmethod__ = True
    return func

class EditedABCMeta(ABCMeta):

    def __call__(cls, *args, **kwargs):

        enforcedmethods = set()
        for base in cls.__mro__:
            for name, value in base.__dict__.items():
                if getattr(value, "__enforcedmethod__", False) and name not in cls.__dict__:
                    enforcedmethods.add(name)
        if enforcedmethods:
            raise TypeError("Can't instantiate abstract class {} "
                            "with enforced methods {}".format(
                                cls.__name__, ', '.join(enforcedmethods)))
        else:
            return super(EditedABCMeta, cls).__call__(*args, **kwargs)

class A(object):
    __metaclass__ = EditedABCMeta

    @enforcedmethod
    def veryspecificmethod(self):
        pass
    @abstractmethod
    def simplemethod(self):
        pass

class B(A):
    def veryspecificmethod(self):
        print 'doing something in B'
    def simplemethod(self):
        pass

class C(B):
    pass

class D(C):
    def veryspecificmethod(self):
        print 'doing something in D'

Вывод:

>>> D().veryspecificmethod()
doing something in D
>>> C().veryspecificmethod()

Traceback (most recent call last):
  File "<pyshell#23>", line 1, in <module>
    C().veryspecificmethod()
  File "C:\Python27\so.py", line 19, in __call__
    cls.__name__, ', '.join(enforcedmethods)))
TypeError: Can't instantiate abstract class C with enforced methods veryspecificmethod
person Ashwini Chaudhary    schedule 03.09.2014
comment
Спасибо за ответ. Но не могли бы вы взглянуть на уточнение, добавленное к вопросу? Я ищу способ, который не испортит абстрактный метод, а скорее добавит метод другого типа. - person Korem; 03.09.2014
comment
@Korem Проверьте правку, может быть, это то, что вы ищете. - person Ashwini Chaudhary; 03.09.2014
comment
Почему return super(ABCMeta, cls).__call__(*args, **kwargs), а не return super(EditedABCMeta, cls).__call__(*args, **kwargs)? - person Korem; 03.09.2014
comment
@Korem Хороший улов, поскольку у ABCMeta нет метода __call__, мы определенно можем это изменить. - person Ashwini Chaudhary; 03.09.2014

Я почти уверен, что это не очень хорошая идея, но я думаю, что вы можете это сделать. Изучите ABCMeta реализацию для вдохновения:

from abc import ABCMeta

def always_override(func):
    func._always_override = True
    return func

class always_override_property(property):
    _always_override = True

class CrazyABCMeta(ABCMeta):
    def __new__(mcls, name, bases, namespace):
        cls = super(ABCMeta, mcls).__new__(mcls, name, bases, namespace)

        abstracts = set()
        # first, get all abstracts from the base classes
        for base in bases:
            abstracts.update(getattr(base, "_all_always_override", set()))

        all_abstracts = abstracts.copy()
        # Now add abstracts from this class and remove abstracts that this class defines
        for name, value in namespace.items():
            always_override = getattr(value, '_always_override', False)
            if always_override:
                abstracts.add(name)
                all_abstracts.add(name)
            elif name in abstracts:
                abstracts.remove(name)

        cls._all_always_override = frozenset(all_abstracts)
        cls._always_override = frozenset(abstracts)
        return cls

    def __call__(cls, *args, **kwargs):
        if cls._always_override:
            raise TypeError(
                'The following methods/properties must '
                'be overridden {}'.format(cls._all_always_override))
        return super(CrazyABCMeta, cls).__call__(*args, **kwargs)

# # # # # # # # # # #
# TESTS!
# # # # # # # # # # #
class A(object):
    __metaclass__ = CrazyABCMeta

    @always_override
    def foo(self):
        pass

    @always_override_property
    def bar(self):
        pass

class B(A):
    def foo(self):
      pass
    bar = 1

class C(B):
    pass

class D(C):
    pass

class E(D):
    def foo(self):
      pass

    @property
    def bar(self):
      return 6

for cls in (B, E):
    cls()
    print ("Pass {}".format(cls.__name__))

for cls in (C, D):
    try:
        print cls()
    except TypeError:
        print ("Pass {}".format(cls.__name__))
person mgilson    schedule 03.09.2014
comment
Я думал о __abstractmethods__ и AttributeError. - person Martijn Pieters; 03.09.2014