Принудително изпълнение на метод във всички наследяващи класове

Имам ситуация, в която искам да наложа всеки клас, наследяващ от определен (абстрактен) клас, за да реализира метод. Това е нещо, което обикновено бих постигнал с помощта на @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 -- Не, защото 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, ако не може да изпълнява своя very_specific_method по същия начин, както го прави 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
Благодаря за отговора. Но можете ли да погледнете пояснението, добавено към въпроса? Търся начин, който не обърква abstractmethod, а по-скоро добавя различен вид метод. - 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