Абстрактні атрибути в Python [дублікат]


91

Який найкоротший / найелегантніший спосіб реалізації наступного коду Scala з абстрактним атрибутом у Python?

abstract class Controller {

    val path: String

}

Для Controllerвизначення "шляху" компілятором Scala примусово застосовується підклас . Підклас буде виглядати так:

class MyController extends Controller {

    override val path = "/home"

}

1
Що ви пробували? Будь ласка, опублікуйте свій код Python із будь-якими проблемами або питаннями щодо вашого рішення.
S.Lott,

"Підклас Controller примусово визначає" шлях "компілятором Scala." ... Примусово коли? Якщо це час компіляції, тобі не пощастило. Якщо це час виконання, то як саме ви хочете, щоб його "примусили"? Іншими словами, чи є різниця між підвищенням AttributeError і NotImplementedError? Чому?
detly

2
Я знаю, що Python - це динамічна мова, і що інтерпретатор python не може застосовувати статичні типи. Мені важливо, що це не вдається якомога раніше, і що легко знайти місце, де сталася помилка, і чому.
Діамон,


Є кілька відповідних відповідей на новішу копію: stackoverflow.com/questions/23831510/… , але в основному виводом є те, що станом на python 3.8 немає хорошого рішення.
Янус

Відповіді:


92

Python має вбудований виняток для цього, хоча ви не зустрінете виняток до часу виконання.

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


class SubClass(Base):
    path = 'blah'

13
Зокрема, ви не зіткнетеся з винятком, поки не буде здійснено доступ до attrtibute, і в цьому випадку ви все одно отримали б AttributeError.
Ben James

5
Я думаю, що підняття a NotImplementedErrorє більш явним, а тому, мабуть, кращим, ніж залишати його за AttributeError.
blokeley

Також ви можете додати повідомлення типу "Не вдається створити екземпляр абстрактного класу Base", коли ви самостійно створюєте виняток.
Бастієн Леонар,

12
Не те, що це працює, лише якщо pathвстановлено безпосередньо на SubClass. Враховуючи екземпляр sc = SubClass(), якщо ви спробуєте встановити sc.path = 'blah'або мати метод, що містить щось на зразок self.path = 'blah'без визначення pathбезпосередньо на SubClass, ви отримаєте AttributeError: can't set attribute.
Ерік

3
Це не відповідь !! Це не поле екземпляра. Scala, подана в класі, є полем екземпляра, оскільки Scala відокремлює статичний та екземпляр.
WeiChing 林 煒 清

101

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

Мій клас "А" є підкласом винятків, і це, здається, порушує приклад.
Chris2048

1
@ Chris2048 як це зламає? яку помилку ви отримуєте?
Джоел

Чи є спосіб написати це агностичним способом Python 2/3, подібним до цього питання ?
bluenote10

7
Ваше рішення python 3 реалізується aяк властивість класу B, а не як властивість об'єкта. Якщо замість цього ви робите `` `клас B (A): def __init __ (self): self.a = 1 ...` `, ви отримуєте помилку під час створення екземпляра B:" Не вдається створити екземпляр абстрактного класу B за допомогою абстрактних методів a "
Янус

@Janus робить важливий момент ... можливість class MyController(val path: String) extends Controllerбути цінним варіантом, доступним у версії Scala, але тут відсутній
Джоел

46

Оскільки це питання було спочатку задане, 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доступі, роблячи застосування менш жорстким.

Це не винні оригінальні відповіді. Основні зміни в синтаксисі абстрактного класу відбулися з часу їх публікації, що в цьому випадку дозволяє більш акуратну та функціональну реалізацію.


1
здається, це єдина правильна відповідь, яка також заспокоюєmypy
Ентоні

1
Дякую за одну з єдиних відповідей, яка насправді стосується питання. Чи можу я запитати, в чому мета print_constantметоду? Наскільки я можу сказати, це нічого не робить?
Брайан Джозеф

1
Тут ви не можете викликати його без створення екземпляра класу Derived. Таким чином, немає різниці, якщо ви видалите @classmethod decorator
zhukovgreen

1
@BrianJoseph - print_constantМетод існує, щоб проілюструвати, як отримати доступ до константи в батьківському класі, враховуючи, що вона визначена лише в дочірньому класі
Джеймс

1
Якщо ви додасте декларацію типу повернення до визначення, Base.CONSTANTтоді mypy також перевірить, чи використовує підклас правильний тип для визначення CONSTANT.
Флоріан Брукер

23

У Python 3.6+ ви можете анотувати атрибут абстрактного класу (або будь-якої змінної), не надаючи значення для цього атрибута.

class Controller:
    path: str

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

Це робить дуже чистий код, де очевидно, що атрибут є абстрактним. Код, який намагається отримати доступ до атрибута, якщо, якщо він не був перезаписаний, піднімає AttributeError.


21
c = Controller () все ще працює. Клас контролера не є абстрактним.
isilpekel

16

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

Наступний код використовує підказку типу PEP 484, щоб допомогти PyCharm правильно статично аналізувати тип pathатрибута.

import abc


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

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

1
Зверніть увагу, що це не поле екземпляра. Поле Scala завжди є членом екземпляра у визначенні класу. Оскільки Scala відокремлює статику та екземпляр.
WeiChing 林 煒 清

11

Для Python 3.3 + є елегантне рішення

from abc import ABC, abstractmethod

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

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


# Instead of an elipsis, you can add a docstring,
# This approach provides more clarity
class AnotherBaseController(ABC):
    @property
    @abstractmethod
    def path(self) -> str:
    """
    :return: the url path of this controller
    """

Чим він відрізняється від інших відповідей:

  1. ...в абстрактному методі тіло переважніше, ніж pass. На відміну від pass, ...передбачає відсутність операцій , де passлише означає відсутність фактичної реалізації

  2. ...рекомендується більше, ніж метання NotImplementedError(...). Це автоматично викликає надзвичайно багатослівну помилку, якщо реалізація абстрактного поля відсутня в підкласі. На відміну від цього, NotImplementedErrorсам по собі не говорить, чому реалізація відсутня. Більше того, для його фактичного підвищення потрібна ручна праця.


1
Ваша думка №2 неправильна. Незалежно від того, набираєте ви ...або підвищуєте NotImplementedErrorчи просто passне впливає на помилку, що виникає при спробі створити екземпляр абстрактного класу (або підкласу) за допомогою нереалізованих методів.
Jephron

І коли ви піднімаєте NotImplementedError, ви можете передати рядкове повідомлення, щоб повідомити абоненту більш детальну інформацію про те, чому виникає помилка.
DtechNet

Замість pass або elipsis ( ...) краще оголосити принаймні документ із абстрактним методом. мені це сказав пілінт.
sausix

Дякую, соусік, це хороший момент. Я включу це у відповідь
Сергій Войтович

6

Починаючи з 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'
                )

Це викликає помилку при ініціалізації дочірнього класу, якщо відсутня змінна класу не визначена, тому вам не доведеться чекати, поки буде доступна змінна відсутнього класу.


__init_subclass__батьківського класу викликається перед ініціалізацією підкласу. Отже, якщо ви хочете перевірити властивості, встановлені в підкласі __init__, це не буде працювати.
нонамі

2
@notnami Це правда. Однак питання задає змінні класу, а не, наприклад, змінні, які будуть встановлені під час __init__методу.
jonathan.scholbach

1
Хам ... Це я чи ви єдиний, хто суворо вирішив питання? :) +1.
keepAlive

5

Ваш базовий клас може реалізувати __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)

class C1(Controller):
    path = 42

class C2(Controller):
    pass


c1 = C1() 
# ok

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

Таким чином помилка виникає під час створення


цікаво, це не працює, якщо вам потрібно використовувати def __init __ (self).
szeitlin

4

Реалізація 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>

Не впевнений, що це призведе до помилки типу, однак, якщо ваші підкласи не реалізують атрибут, у випадку, якщо ви використовуєте перевірку типу з IDE
Аллен Ван,

3

Я трохи змінив відповідь @James , щоб усі ці декоратори не займали так багато місця. Якщо у вас було кілька таких абстрактних властивостей для визначення, це зручно:

from abc import ABC, abstractmethod

def abstractproperty(func):
   return property(classmethod(abstractmethod(func)))

class Base(ABC):

    @abstractproperty
    def CONSTANT(cls): ...

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


class Derived(Base):
    CONSTANT = 42

class BadDerived(Base):
    BAD_CONSTANT = 42

Derived()       # -> Fine
BadDerived()    # -> Error


1

Погляньте на модуль abc (Abtract Base Class): http://docs.python.org/library/abc.html

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


12
Будь ласка, детальніше поясніть: Як модуль abc допомагає в цьому контексті?
guettli

1

У відповіді Бастієна Леонарда згадується абстрактний модуль базового класу, а у відповіді Брендана Абеля йдеться про нереалізовані атрибути, що викликають помилки. Щоб гарантувати, що клас не реалізований за межами модуля, ви можете до префікса базового імені наголосити символом підкреслення, який позначає його як приватний для модуля (тобто він не імпортується).

тобто

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

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

1
чи можна викликати деяку помилку, якщо підклас не перевизначає атрибут? Це було б просто для методів, а як щодо атрибутів?
Mario F

Чи не було б краще залишити оголошення про шлях у _Controllerкласі? Качине введення не набирає чинності, якщо вже існує (недійсне) значення. Інакше в якийсь момент, коли мені потрібно визначити pathполе, помилки не було б, оскільки значення вже є.
deamon

@Mario - так, відповідь Брендана Абеля дає хороший спосіб це зробити
Брендан

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

Станом на 3.3 abc.abstractpropertyзастарілий, я думаю.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.