【问题标题】:Python abstract class shall force derived classes to initialize variable in __init__Python 抽象类应强制派生类在 __init__ 中初始化变量
【发布时间】:2019-08-24 04:01:58
【问题描述】:

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

我查看了几个没有完全解决我的问题的问题,特别是herehereThis 看起来很有希望,但我无法让它发挥作用。

我假设我想要的结果可能类似于以下伪代码

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'

这是如何正确实现的?

【问题讨论】:

  • 一种选择是让子类调用超类的__init__ 函数。请参阅此问题以了解如何做到这一点stackoverflow.com/questions/2399307/…
  • HereComesTheProblem的情况下,您是否希望weights attr 初始化为None,或者您是否希望在创建HereComesTheProblem 的实例时,应该抛出错误“未提供重量”?
  • @pnv 我在尝试创建HereComesTheProblem 的实例时需要一条错误消息。
  • @k1next 你能看看我在下面给出的解决方案并给出一些反馈

标签: python oop abstract-class


【解决方案1】:

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

值得注意的是,自定义元类通常不受欢迎,但你可以用一个来解决这个问题。 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')

以下是我的原始答案,它更广泛地探讨了该主题。

原答案

我认为其中一些原因是由于将 instance 属性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 docs,由于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 的子类。 我的建议是不要将 xyzweights 抽象化,而是处理它们是否在运行时设置,即捕获在访问值时可能弹出的任何 AttributeErrors。

【讨论】:

  • 不错的解决方案!不过,在调用 super 的 __call__ 时,您在 args 前面错过了 *
【解决方案2】:

为了强制子类实现某个属性或方法,如果没有实现这个方法,需要报错:

from abc import ABCMeta, abstractmethod, abstractproperty

class Quadrature(object, metaclass=ABCMeta):

    @abstractproperty
    def xyz(self):
        raise NotImplementedError


【讨论】:

【解决方案3】:

类注解解决方案

这是可能的,因为对 python 3.7 的更改(我希望你正在使用它 - 因为这很酷!)因为它添加了 type hinting 和添加类 annotations 的能力,这是为 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 没有设置xyzweights,会引发属性错误。请注意,您必须在 init 结束时调用构造函数,但这不应该是一个真正的问题,如果您真的不喜欢它,您可以通过将您自己的元类包装在上面的代码周围来解决这个问题。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-10-03
    • 2011-07-05
    • 2021-09-24
    • 1970-01-01
    • 2016-07-15
    • 1970-01-01
    相关资源
    最近更新 更多