Абстрактные атрибуты в Python

Каков самый короткий/самый элегантный способ реализовать следующий Scala код с абстрактным атрибутом в Python?

abstract class Controller {

    val path: String

}

Подкласс Controller применяется для определения "пути" компилятором Scala. Подкласс будет выглядеть следующим образом:

class MyController extends Controller {

    override val path = "/home"

}

Ответ 1

Для этого у Python есть встроенное исключение, хотя вы не столкнетесь с этим исключением до выполнения.

class Base(object):
    @property
    def path(self):
        raise NotImplementedError


class SubClass(Base):
    path = 'blah'

Ответ 2

Python 3. 3+

from abc import ABCMeta, abstractmethod


class A(metaclass=ABCMeta):
    def __init__(self):
        # ...
        pass

    @property
    @abstractmethod
    def a(self):
        pass

    @abstractmethod
    def b(self):
        pass


class B(A):
    a = 1

    def b(self):
        pass

Неспособность объявить a или b в производном классе B вызовет TypeError такую как:

TypeError: Не удается создать экземпляр абстрактного класса B с помощью абстрактных методов a

Python 2.7

Для этого есть декоратор @abstractproperty:

from abc import ABCMeta, abstractmethod, abstractproperty


class A:
    __metaclass__ = ABCMeta

    def __init__(self):
        # ...
        pass

    @abstractproperty
    def a(self):
        pass

    @abstractmethod
    def b(self):
        pass


class B(A):
    a = 1

    def b(self):
        pass

Ответ 3

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

from abc import ABC, abstractmethod


class Base(ABC):

    @property
    @classmethod
    @abstractmethod
    def CONSTANT(cls):
        return NotImplementedError

    def print_constant(self):
        print(type(self).CONSTANT)


class Derived(Base):
    CONSTANT = 42

Это заставляет производный класс определять константу, иначе возникнет исключение TypeError, когда вы попытаетесь создать экземпляр подкласса. Если вы хотите использовать константу для любой функциональности, реализованной в абстрактном классе, вы должны получить доступ к константе подкласса по type(self).CONSTANT вместо просто CONSTANT, поскольку значение в базовом классе не определено.

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

Все предыдущие ответы касались полезных моментов, но я чувствую, что принятый ответ не дает прямого ответа на вопрос, потому что

  • Вопрос требует реализации в абстрактном классе, но принятый ответ не следует за абстрактным формализмом.
  • Вопрос требует, чтобы реализация была принудительной. Я бы сказал, что в этом ответе соблюдение более строгое, поскольку оно вызывает ошибку во время выполнения, когда создается экземпляр подкласса, если CONSTANT не определен. Принятый ответ позволяет создать экземпляр объекта и выдает ошибку только при обращении к CONSTANT, делая исполнение менее строгим.

Это не вина оригинальных ответов. Со времени их публикации произошли серьезные изменения в синтаксисе абстрактного класса, что в данном случае позволяет более аккуратную и функциональную реализацию.

Ответ 4

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

В следующем коде используется подсказка типа PEP 484, чтобы помочь PyCharm правильно статически анализировать тип атрибута path.

import abc


class Controller(abc.ABC):
    path = NotImplemented  # type: str


class MyController(Controller):
    path = '/home'

Ответ 5

В Python 3. 6+ вы можете аннотировать атрибут абстрактного класса (или любую переменную) без указания значения для этого атрибута.

class Controller:
    path: str

class MyController(Controller):
    path = "/home"

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

Ответ 6

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

class Controller(object):
    def __new__(cls, *args, **kargs):
        if not hasattr(cls,'path'): 
            raise NotImplementedError("'Controller' subclasses should have a 'path' attribute")
        return object.__new__(cls,*args,**kargs)

class C1(Controller):
    path = 42

class C2(Controller):
    pass


c1 = C1() 
# ok

c2 = C2()  
# NotImplementedError: 'Controller' subclasses should have a 'path' attribute

Таким образом возникает ошибка при создании экземпляра

Ответ 7

Посмотрите модуль abc (Abtract Base Class): http://docs.python.org/library/abc.html

Однако, на мой взгляд, самым простым и наиболее распространенным решением является повышение исключения, когда создается экземпляр базового класса или когда его свойство доступно.

Ответ 8

Реализация Python3.6 может выглядеть так:

In [20]: class X:
    ...:     def __init_subclass__(cls):
    ...:         if not hasattr(cls, 'required'):
    ...:             raise NotImplementedError

In [21]: class Y(X):
    ...:     required = 5
    ...:     

In [22]: Y()
Out[22]: <__main__.Y at 0x7f08408c9a20>

Ответ 9

Начиная с Python 3.6 вы можете использовать __init_subclass__ для проверки переменных класса дочернего класса при инициализации:

from abc import ABC

class A(ABC):
    @classmethod
    def __init_subclass__(cls):
        required_class_variables = [
            "foo",
            "bar"
        ]
        for var in required_class_variables:
            if not hasattr(cls, var):
                raise NotImplementedError(
                    f'Class {cls} lacks required '{var}' class attribute'
                )

Это приводит к ошибке при инициализации дочернего класса, если отсутствующая переменная класса не определена, поэтому вам не нужно ждать, пока не будет получена доступная переменная класса.

Ответ 10

Для Python 3.3+ есть элегантное решение

from abc import ABC, abstractmethod

class BaseController(ABC):
    @property
    @abstractmethod
    def path(self) -> str:
        ...

class Controller(BaseController):
    path = "/home"

Чем он отличается от других ответов?

  1. ... в теле абстрактного метода предпочтительнее, чем pass. В отличие от pass, ... подразумевает никаких операций, где pass означает только отсутствие фактической реализации

  2. ... более рекомендуется, чем бросать NotImplementedError(...). Это автоматически выдает чрезвычайно подробную ошибку, если в подклассе отсутствует реализация абстрактного поля. Напротив, сам TG48 не говорит, почему реализация отсутствует. Более того, для его поднятия требуется ручной труд.

Ответ 11

Bastien Léonard отвечает на абстрактный модуль базового класса, а ответ Брендана Абеля отвечает на нереализованные атрибуты, вызывающие ошибки. Чтобы гарантировать, что класс не реализован вне модуля, вы можете префикс базового имени с подчеркиванием, которое обозначает его как закрытое для модуля (т.е. Оно не импортируется).

то есть.

class _Controller(object):
    path = '' # There are better ways to declare attributes - see other answers

class MyController(_Controller):
    path = '/Home'

Ответ 12

class AbstractStuff:
    @property
    @abc.abstractmethod
    def some_property(self):
        pass

По состоянию на 3.3 abc.abstractproperty устарела, я думаю.