Python 抽象 class 应强制派生 classes 初始化 __init__ 中的变量

Python abstract class shall force derived classes to initialize variable in __init__

我想要一个抽象 class,它强制每个派生 class 在其 __init__ 方法中设置某些属性。

我看了几个问题,但没有完全解决我的问题,特别是 or here. This 看起来很有希望,但我无法让它工作。

我假设我想要的结果可能如下所示伪代码:

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__ 方法可以在派生的 classes 中被覆盖,但现在这至少确保(对我而言)我总是知道请求的属性已设置:

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'

这是如何正确实施的?

编辑: 使用自定义元的解决方案class。

值得注意的是,自定义元classes 通常不受欢迎,但您可以用一个来解决这个问题。 Here 是一篇讨论它们如何工作以及何时有用的好文章。这里的解决方案本质上是在调用 __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 属性嵌套在 class 的命名空间中(并且由 class 的实例共享,除非它们覆盖它)。
  • 属性 是一个具有语法快捷方式的函数,使它们可以像属性一样访问,但它们的功能性质允许它们是动态的。

一个没有引入抽象 classes 的小例子是

>>> 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 docs,自 3.3 以来,abstractproperty 是多余的,实际上反映了您尝试的解决方案。 该解决方案的问题是您的子 classes 没有实现具体的 属性,它们只是用实例属性覆盖它。 为了继续使用 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 的子classes。 我的建议是不要将 xyzweights 抽象化,而是处理它们是否在运行时设置,即捕获在访问值时可能弹出的任何 AttributeErrors。

为了强制子类实现一个属性或方法,你需要抛出一个错误,如果这个方法没有被实现:

from abc import ABCMeta, abstractmethod, abstractproperty

class Quadrature(object, metaclass=ABCMeta):

    @abstractproperty
    def xyz(self):
        raise NotImplementedError


Class 注释解决方案

这是可能的,因为对 python 3.7 进行了更改(我希望您正在使用它 - 因为它很酷!),因为它添加了 type hinting and the ability to add class annotations, which were added for dataclasses。它与我能想到的最接近您最初所需的语法。你想要的 superclass 看起来像这样:

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 的子class 的任何子class 必须设置 class 中注释的属性(如果您不提供type 到注解,它不会被认为是注解)因此,如果 quadratic 的构造函数没有设置 xyzweights,则会引发属性错误。请注意,您必须在 init 的末尾调用构造函数,但这应该不是一个真正的问题,如果您 really 不喜欢。

您可以根据需要修改 PropertyEnforcedABC(例如强制执行属性的类型)等等。您甚至可以检查 Optional 并忽略它们。