Эквивалент NotImplementedError для полей в Python

В Python 2.x, когда вы хотите пометить метод как абстрактный, вы можете определить его так:

class Base:
    def foo(self):
        raise NotImplementedError("Subclasses should implement this!")

Затем, если вы забудете переопределить его, вы получите замечательное напоминание. Существует ли эквивалентный способ отметить поле как абстрактное? Или вы указываете, что это в классе docstring, что вы можете сделать?

Сначала я думал, что могу установить поле NotImplemented, но когда я посмотрел, на что он на самом деле (богатые сравнения), он казался оскорбительным.

Ответ 1

Да, вы можете. Используйте декоратор @property. Например, если у вас есть поле под названием "пример", вы не можете сделать что-то вроде этого:

class Base(object):

    @property
    def example(self):
        raise NotImplementedError("Subclasses should implement this!")

Запустив следующую команду, выполните NotImplementedError так, как вы хотите.

b = Base()
print b.example

Ответ 2

Альтернативный ответ:

@property
def NotImplementedField(self):
    raise NotImplementedError

class a(object):
    x = NotImplementedField

class b(a):
    # x = 5
    pass

b().x
a().x

Это похоже на Evan's, но кратким и дешевым - вы получите только один экземпляр NotImplementedField.

Ответ 3

Лучший способ сделать это - использовать Абстрактные базовые классы:

import abc

class Foo(abc.ABC):

    @property
    @abc.abstractmethod
    def demo_attribute(self):
        raise NotImplementedError

    @abc.abstractmethod
    def demo_method(self):
        raise NotImplementedError

class BadBar(Foo):
    pass

class GoodBar(Foo):

    demo_attribute = 'yes'

    def demo_method(self):
        return self.demo_attribute

bad_bar = BadBar()
# TypeError: Can't instantiate abstract class BadBar \
# with abstract methods demo_attribute, demo_method

good_bar = GoodBar()
# OK

Обратите внимание, что вы все равно должны иметь raise NotImplementedError вместо pass, потому что ничего не мешает наследующему классу вызвать super().demo_method(), а если абстрактный demo_method равен pass, это не удастся тихо.

Ответ 4

def require_abstract_fields(obj, cls):
    abstract_fields = getattr(cls, "abstract_fields", None)
    if abstract_fields is None:
        return

    for field in abstract_fields:
        if not hasattr(obj, field):
            raise RuntimeError, "object %s failed to define %s" % (obj, field)

class a(object):
    abstract_fields = ("x", )
    def __init__(self):
        require_abstract_fields(self, a)

class b(a):
    abstract_fields = ("y", )
    x = 5
    def __init__(self):
        require_abstract_fields(self, b)
        super(b, self).__init__()

b()
a()

Обратите внимание на прохождение типа класса в require_abstract_fields, поэтому, если это используется несколькими унаследованными классами, они не все проверяют поля самого производного класса. Возможно, вы сможете автоматизировать это с помощью метакласса, но я не понял этого. Определение поля в None принято.

Ответ 5

И вот мое решение:

def not_implemented_method(func):
    from functools import wraps
    from inspect import getargspec, formatargspec

    @wraps(func)
    def wrapper(self, *args, **kwargs):
        c = self.__class__.__name__
        m = func.__name__
        a = formatargspec(*getargspec(func))
        raise NotImplementedError('\'%s\' object does not implement the method \'%s%s\'' % (c, m, a))

    return wrapper


def not_implemented_property(func):
    from functools import wraps
    from inspect import getargspec, formatargspec

    @wraps(func)
    def wrapper(self, *args, **kwargs):
        c = self.__class__.__name__
        m = func.__name__
        raise NotImplementedError('\'%s\' object does not implement the property \'%s\'' % (c, m))

    return property(wrapper, wrapper, wrapper)

Его можно использовать как

class AbstractBase(object):
    @not_implemented_method
    def test(self):
        pass

    @not_implemented_property
    def value(self):
        pass

class Implementation(AbstractBase):
    value = None

    def __init__(self):
        self.value = 42

    def test(self):
        return True

Ответ 6

Интересный способ справиться с этим - установить для атрибута значение None в родительском классе и получить доступ к атрибуту с помощью функции, обеспечивающей его установку в дочернем классе.

Вот пример из django-rest-framework:

class GenericAPIView(views.APIView):

    [...]

    serializer_class = None

    [...]

    def get_serializer_class(self):
        assert self.serializer_class is not None, (
            "'%s' should either include a 'serializer_class' attribute, "
            "or override the 'get_serializer_class()' method."
            % self.__class__.__name__
        )

        return self.serializer_class

Ответ 7

Кажется, что этот вопрос был открыт как для атрибутов экземпляра, так и для атрибутов класса, я сосредоточусь только на первой теме.

Так, например, для атрибутов, альтернативный ответ Эвану - определить обязательное поле, используя pyfields:

from pyfields import field

class Base(object):
    example = field(doc="This should contain an example.")

b = Base()
b.example

дает

pyfields.core.MandatoryFieldInitError: 
   Mandatory field 'example' has not been initialized yet 
   on instance <__main__.Base object at 0x000002C1000C0C18>.

Конечно, он не дает вам возможности редактировать сообщение об ошибке, говоря о подклассах. Но в некотором смысле более реалистично не говорить о подклассах - действительно, в python атрибуты могут быть переопределены в экземплярах базового класса - не только в подклассах.

Примечание: я автор pyfields. Подробности смотрите в документации.