В Python, как заставить абстрактный метод быть статическим в дочернем классе?

Это настройка, которую я хочу: A должен быть абстрактным базовым классом со статическим и абстрактным методом f(). B должен наследоваться от A. Требования: 1. У вас не должно быть возможности создать экземпляр A 2. У вас не должно быть возможности создать экземпляр B, если он не реализует static f()

Вдохновившись этим вопросом, я попробовал несколько подходов . С этими определениями:

class abstractstatic(staticmethod):
    __slots__ = ()
    def __init__(self, function):
        super(abstractstatic, self).__init__(function)
        function.__isabstractmethod__ = True
    __isabstractmethod__ = True

class A:
    __metaclass__ = abc.ABCMeta
    @abstractstatic
    def f():
        pass

class B(A):
    def f(self):
        print 'f'

class A2:
    __metaclass__ = abc.ABCMeta
    @staticmethod
    @abc.abstractmethod
    def f():
        pass

class B2(A2):
    def f(self):
        print 'f'

Здесь A2 и B2 определяются с использованием обычных соглашений Python, а A и B определяются с использованием способа, предложенного в этот ответ. Ниже приведены некоторые операции, которые я пробовал, и результаты, которые были нежелательными.

С классами А/В:

>>> B().f()
f
#This should have thrown, since B doesn't implement a static f()

С классами A2/B2:

>>> A2()
<__main__.A2 object at 0x105beea90>
#This should have thrown since A2 should be an uninstantiable abstract class

>>> B2().f()
f
#This should have thrown, since B2 doesn't implement a static f()

Поскольку ни один из этих подходов не дает мне желаемого результата, как мне добиться того, чего я хочу?


person GrowinMan    schedule 05.04.2015    source источник


Ответы (1)


Вы не можете делать то, что хотите, используя только ABCMeta. Применение ABC не выполняет никаких проверок типов, принудительно применяется только наличие атрибута с правильным именем.

Возьмем, к примеру:

>>> from abc import ABCMeta, abstractmethod, abstractproperty
>>> class Abstract(object):
...     __metaclass__ = ABCMeta
...     @abstractmethod
...     def foo(self): pass
...     @abstractproperty
...     def bar(self): pass
... 
>>> class Concrete(Abstract):
...     foo = 'bar'
...     bar = 'baz'
... 
>>> Concrete()
<__main__.Concrete object at 0x104b4df90>

Мне удалось построить Concrete(), несмотря на то, что и foo, и bar являются простыми атрибутами.

Метакласс ABCMeta отслеживает только то, сколько объектов осталось с атрибутом __isabstractmethod__, имеющим значение true; при создании класса из метакласса (вызывается ABCMeta.__new__) атрибут cls.__abstractmethods__ затем устанавливается в объект frozenset со всеми именами, которые все еще являются абстрактными.

Затем type.__new__ проверяет этот frozenset и выдает TypeError, если вы пытаетесь создать экземпляр.

Здесь вам придется создать свой собственный __new__ метод; подкласс ABCMeta и добавить проверку типов в новый метод __new__. Этот метод должен искать наборы __abstractmethods__ в базовых классах, находить соответствующие объекты с атрибутом __isabstractmethod__ в MRO, а затем выполнять проверку типов для атрибутов текущего класса.

Это означало бы, что вы вызовете исключение при определении класса, а не экземпляра. Чтобы это работало, вы должны добавить метод __call__ в свой подкласс ABCMeta и заставить его генерировать исключение на основе информации, собранной вашим собственным методом __new__ о том, какие типы были неправильными; аналогичный двухэтапный процесс, который сейчас делают ABCMeta и type.__new__. В качестве альтернативы, обновите набор __abstractmethods__ для класса, чтобы добавить все имена, которые были реализованы, но с неправильным типом, и оставьте значение type.__new__ для создания исключения.

Следующая реализация использует этот последний подход; добавьте имена обратно в __abstractmethods__, если реализованный тип не соответствует (используя сопоставление):

from types import FunctionType

class ABCMetaTypeCheck(ABCMeta):
    _typemap = {  # map abstract type to expected implementation type
        abstractproperty: property,
        abstractstatic: staticmethod,
        # abstractmethods return function objects
        FunctionType: FunctionType,
    }
    def __new__(mcls, name, bases, namespace):
        cls = super(ABCMetaTypeCheck, mcls).__new__(mcls, name, bases, namespace)
        wrong_type = set()
        seen = set()
        abstractmethods = cls.__abstractmethods__
        for base in bases:
            for name in getattr(base, "__abstractmethods__", set()):
                if name in seen or name in abstractmethods:
                    continue  # still abstract or later overridden
                value = base.__dict__.get(name)  # bypass descriptors
                if getattr(value, "__isabstractmethod__", False):
                    seen.add(name)
                    expected = mcls._typemap[type(value)]
                    if not isinstance(namespace[name], expected):
                        wrong_type.add(name)
        if wrong_type:
            cls.__abstractmethods__ = abstractmethods | frozenset(wrong_type)
        return cls

С помощью этого метакласса вы получите ожидаемый результат:

>>> class Abstract(object):
...     __metaclass__ = ABCMetaTypeCheck
...     @abstractmethod
...     def foo(self): pass
...     @abstractproperty
...     def bar(self): pass
...     @abstractstatic
...     def baz(): pass
... 
>>> class ConcreteWrong(Abstract):
...     foo = 'bar'
...     bar = 'baz'
...     baz = 'spam'
... 
>>> ConcreteWrong()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class ConcreteWrong with abstract methods bar, baz, foo
>>> 
>>> class ConcreteCorrect(Abstract):
...     def foo(self): return 'bar'
...     @property
...     def bar(self): return 'baz'
...     @staticmethod
...     def baz(): return  'spam'
... 
>>> ConcreteCorrect()
<__main__.ConcreteCorrect object at 0x104ce1d10>
person Martijn Pieters    schedule 05.04.2015