Абстрактный класс Python должен заставить производные классы инициализировать переменную в __init__ - PullRequest
13 голосов
/ 02 апреля 2019

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

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

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

from abc import ABCMeta, abstractmethod


class Quadrature(object, metaclass=ABCMeta):

    @someMagicKeyword            #<==== This is what I want, but can't get working
    xyz

    @someMagicKeyword            #<==== This is what I want, but can't get working
    weights


    @abstractmethod
    def __init__(self, order):
        pass


    def someStupidFunctionDefinedHere(self, n):
        return self.xyz+self.weights+n



class QuadratureWhichWorks(Quadrature):
    # This shall work because we initialize xyz and weights in __init__
    def __init__(self,order):
        self.xyz = 123
        self.weights = 456

class QuadratureWhichShallNotWork(Quadrature):
    # Does not initialize self.weights
    def __init__(self,order):
        self.xyz = 123 

Вот некоторые из вещей, которые я пробовал:

from abc import ABCMeta, abstractmethod


class Quadrature(object, metaclass=ABCMeta):

    @property
    @abstractmethod
    def xyz(self):
        pass


    @property
    @abstractmethod
    def weights(self):
        pass


    @abstractmethod
    def __init__(self, order):
        pass


    def someStupidFunctionDefinedHere(self, n):
        return self.xyz+self.weights+n



class QuadratureWhichWorks(Quadrature):
    # This shall work because we initialize xyz and weights in __init__
    def __init__(self,order):
        self.xyz = 123
        self.weights = 456

Затем я пытаюсь создать экземпляр:

>>> from example1 import * 
>>> Q = QuadratureWhichWorks(10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class QuadratureWhichWorks with abstract methods weights, xyz
>>> 

, который говорит мне, чтобы реализовать методы, но ядумал, что я сказал, что это properties?

Мой текущий обходной путь имеет недостаток, что метод __init__ может быть перезаписан в производных классах, но на данный момент это по крайней мере гарантирует (для меня), чтоЯ всегда знаю, что запрошенные свойства установлены:

from abc import ABCMeta, abstractmethod


class Quadrature(object, metaclass=ABCMeta):

    @abstractmethod
    def computexyz(self,order):
        pass


    @abstractmethod
    def computeweights(self,order):
        pass


    def __init__(self, order):
        self.xyz = self.computexyz(order)
        self.weights = self.computeweights(order)

    def someStupidFunctionDefinedHere(self, n):
        return self.xyz+self.weights+n



class QuadratureWhichWorks(Quadrature):

    def computexyz(self,order):
        return order*123

    def computeweights(self,order):
        return order*456


class HereComesTheProblem(Quadrature):

    def __init__(self,order):
        self.xyz = 123
        # but nothing is done with weights

    def computexyz(self,order):
        return order*123

    def computeweights(self,order): # will not be used
        return order*456

Но проблема в

>>> from example2 import * 
>>> Q = HereComesTheProblem(10)
>>> Q.xyz
123
>>> Q.weights
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'HereComesTheProblem' object has no attribute 'weights'

Как это правильно реализовано?

Ответы [ 3 ]

6 голосов
/ 08 апреля 2019

Редактировать: Решение с использованием пользовательского метакласса.

Стоит отметить, что пользовательские метаклассы часто осуждаются, но вы можете решить эту проблему с помощью одного. Здесь - это хорошая статья, в которой обсуждается, как они работают и когда они полезны.Решение здесь, по сути, состоит в том, чтобы установить проверку атрибута, который вы хотите после вызова __init__.

from abc import ABCMeta, abstractmethod

# our version of ABCMeta with required attributes
class MyMeta(ABCMeta):
    required_attributes = []

    def __call__(self, *args, **kwargs):
        obj = super(MyMeta, self).__call__(*args, **kwargs)
        for attr_name in obj.required_attributes:
            if not getattr(obj, attr_name):
                raise ValueError('required attribute (%s) not set' % attr_name)
        return obj

# similar to the above example, but inheriting MyMeta now
class Quadrature(object, metaclass=MyMeta):
    required_attributes = ['xyz', 'weights']

    @abstractmethod
    def __init__(self, order):
        pass


class QuadratureWhichWorks(Quadrature):
    # This shall work because we initialize xyz and weights in __init__
    def __init__(self,order):
        self.xyz = 123
        self.weights = 456

q = QuadratureWhichWorks('foo')

class QuadratureWhichShallNotWork(Quadrature):
    def __init__(self, order):
        self.xyz = 123

q2 = QuadratureWhichShallNotWork('bar')

Ниже приведен мой оригинальный ответ, который более подробно рассматривает эту тему.

Оригинальный ответ

Я думаю, что это происходит из-за путаницы атрибутов экземпляра с объектами, обернутыми декоратором property.

  • Атрибут экземпляра представляет собой простой кусок данных, вложенных в пространство имен экземпляра.Аналогично, атрибут класса вложен в пространство имен класса (и совместно используется экземплярами этого класса, если они не перезаписывают его).
  • Свойство - это функция с синтаксическими ярлыками, чтобы сделать их доступными, как если бы они былиатрибуты, но их функциональная природа позволяет им быть динамическими.

Небольшой пример без введения абстрактных классов будет

>>> class Joker(object):
>>>     # a class attribute
>>>     setup = 'Wenn ist das Nunstück git und Slotermeyer?'
>>> 
>>>     # a read-only property
>>>     @property
>>>     def warning(self):
>>>         return 'Joke Warfare is explicitly banned bythe Geneva Conventions'
>>> 
>>>     def __init__(self):
>>>         self.punchline = 'Ja! Beiherhund das Oder die Flipperwaldt gersput!'

>>> j = Joker()

>>> # we can access the class attribute via class or instance
>>> Joker.setup == j.setup

>>> # we can get the property but cannot set it
>>> j.warning
'Joke Warfare is explicitly banned bythe Geneva Conventions'
>>> j.warning = 'Totally safe joke...'
AttributeError: cant set attribute

>>> # instance attribute set in __init__ is only accessible to that instance
>>> j.punchline != Joker.punchline
AttributeError: type object 'Joker' has no attribute 'punchline'

Согласно Документам Python , начиная с версии 3.3, abstractproperty является избыточным и фактически отражает вашу попытку решения.Проблема с этим решением состоит в том, что ваши подклассы не реализуют конкретное свойство, они просто перезаписывают его атрибутом экземпляра.Чтобы продолжить использование пакета abc, вы могли бы обработать это, реализовав эти свойства, то есть

>>> from abc import ABCMeta, abstractmethod
>>> class Quadrature(object, metaclass=ABCMeta):
>>> 
>>>     @property
>>>     @abstractmethod
>>>     def xyz(self):
>>>         pass
>>> 
>>>     @property
>>>     @abstractmethod
>>>     def weights(self):
>>>         pass
>>> 
>>>     @abstractmethod
>>>     def __init__(self, order):
>>>         pass
>>> 
>>>     def someStupidFunctionDefinedHere(self, n):
>>>         return self.xyz+self.weights+n
>>> 
>>> 
>>> class QuadratureWhichWorks(Quadrature):
>>>     # This shall work because we initialize xyz and weights in __init__
>>>     def __init__(self,order):
>>>         self._xyz = 123
>>>         self._weights = 456
>>> 
>>>     @property
>>>     def xyz(self):
>>>         return self._xyz
>>> 
>>>     @property
>>>     def weights(self):
>>>         return self._weights
>>> 
>>> q = QuadratureWhichWorks('foo')
>>> q.xyz
123
>>> q.weights
456

Я думаю, что это немного неуклюже, но это действительно зависит от того, как вы намереваетесь внедрять подклассы.Quadrature.Мое предложение было бы не делать xyz или weights абстрактными, а вместо этого обрабатывать, были ли они установлены во время выполнения, то есть перехватывать любые AttributeError s, которые могут появиться при доступе к значению.

2 голосов
/ 12 апреля 2019

Решение для аннотаций класса

Это возможно из-за изменений в Python 3.7 (который, я надеюсь, вы используете - потому что это круто!), Так как он добавляет type hinting и возможность добавления класса аннотаций , которые были добавлены для dataclasses. Это настолько близко к вашему первоначальному желаемому синтаксису, насколько я могу придумать. Суперкласс, который вы захотите, будет выглядеть примерно так:

from abc import ABC, abstractmethod
from typing import List

class PropertyEnfocedABC(ABC):

    def __init__(self):
        annotations = self.__class__.__dict__.get('__annotations__', {})
        for name, type_ in annotations.items():
            if not hasattr(self, name):
                raise AttributeError(f'required attribute {name} not present '
                                     f'in {self.__class__}')

Теперь, чтобы увидеть это в действии.

class Quadratic(PropertyEnfocedABC):

    xyz: int 
    weights: List[int] 

    def __init__(self):
        self.xyz = 2
        self.weights = [4]
        super().__init__()

или, точнее, в вашем случае со смесью абстрактных методов и атрибутов:

class Quadrature(PropertyEnforcedABC):

    xyz: int
    weights: int


    @abstractmethod
    def __init__(self, order):
        pass

    @abstractmethod
    def some_stupid_function(self, n):
        return self.xyz + self.weights + n

Теперь любой подкласс подкласса PropertyEnforcedABC должен установить атрибуты, аннотируемые в классе (если вы не предоставите тип аннотации, он не будет считаться аннотацией) и, следовательно, если конструктор quadratic не установил xyz или weights, возникнет ошибка атрибута. Обратите внимание, что вы должны вызывать конструктор в конце init, но это не должно быть реальной проблемой, и вы можете решить эту проблему, обернув свой собственный метакласс вокруг вышеуказанного кода, если вам действительно это не нравится.

Вы можете изменить PropertyEnforcedABC так, как вы хотите (например, принудительно задать тип свойств) и многое другое. Вы можете даже проверить на Optional и игнорировать их.

2 голосов
/ 08 апреля 2019

Чтобы заставить подкласс реализовать свойство или метод, вам нужно вызвать ошибку, если этот метод не реализован:

from abc import ABCMeta, abstractmethod, abstractproperty

class Quadrature(object, metaclass=ABCMeta):

    @abstractproperty
    def xyz(self):
        raise NotImplementedError


...