抽象属性(不是属性)?


78

定义抽象实例属性而不是属性的最佳实践是什么?

我想写一些像:

class AbstractFoo(metaclass=ABCMeta):

    @property
    @abstractmethod
    def bar(self):
        pass

class Foo(AbstractFoo):

    def __init__(self):
        self.bar = 3

代替:

class Foo(AbstractFoo):

    def __init__(self):
        self._bar = 3

    @property
    def bar(self):
        return self._bar

    @bar.setter
    def setbar(self, bar):
        self._bar = bar

    @bar.deleter
    def delbar(self):
        del self._bar

属性很方便,但是对于不需要计算的简单属性来说,它们是过大的。这对于将由用户进行子类化和实现的抽象类尤为重要(我不想强迫某人@property只可以在中编写代码self.foo = foo就使用__init__)。

Python问题中的抽象属性仅建议使用@property和的答案@abstractmethod:它不能回答我的问题。

通过抽象类属性的ActiveState配方AbstractAttribute可能是正确的方法,但是我不确定。它也仅适用于类属性,而不适用于实例属性。


为什么需要强迫某人在其班级上拥有特定的属性?
菲利普·阿德勒

15
这不是ABC的全部内容吗?如果您想要我的具体示例,我希望人们为他们的传感器编写一个类,并且该类应具有self.port属性。
Lapinot 2014年

经过思考,是的,我想是的。虽然我认为这在面对鸭嘴式时还是不对的……
菲利普·阿德勒

也许我只是要求太多的复杂性,但是在做抽象类时不要使用ABC(我想我只是要使用普通的基类)...
Lapinot

熵的解决方案很简单,并且效果很好。为什么不接受答案?
Dave Kielpinski '17

Answers:


29

如果您确实想强制子类定义给定的属性,则可以使用元类。就我个人而言,我认为这可能是矫kill过正,不是很Python,但是您可以这样做:

 class AbstractFooMeta(type):

     def __call__(cls, *args, **kwargs):
         """Called when you call Foo(*args, **kwargs) """
         obj = type.__call__(cls, *args, **kwargs)
         obj.check_bar()
         return obj


 class AbstractFoo(object):
     __metaclass__ = AbstractFooMeta
     bar = None

     def check_bar(self):
         if self.bar is None:
             raise NotImplementedError('Subclasses must define bar')


 class GoodFoo(AbstractFoo):
     def __init__(self):
         self.bar = 3


 class BadFoo(AbstractFoo):
     def __init__(self):
         pass

基本上,重新定义元类__call__以确保 check_bar在实例的init之后被调用。

GoodFoo()  # ok
BadFoo ()  # yield NotImplementedError

46

现在是2018年,我们值得更好的解决方案:

from better_abc import ABCMeta, abstract_attribute    # see below

class AbstractFoo(metaclass=ABCMeta):

    @abstract_attribute
    def bar(self):
        pass

class Foo(AbstractFoo):
    def __init__(self):
        self.bar = 3

class BadFoo(AbstractFoo):
    def __init__(self):
        pass

它的行为如下:

Foo()     # ok
BadFoo()  # will raise: NotImplementedError: Can't instantiate abstract class BadFoo
# with abstract attributes: bar

此答案与接受的答案使用相同的方法,但与内置ABC集成良好,并且不需要样板程序check_bar()

这里是better_abc.py内容:

from abc import ABCMeta as NativeABCMeta

class DummyAttribute:
    pass

def abstract_attribute(obj=None):
    if obj is None:
        obj = DummyAttribute()
    obj.__is_abstract_attribute__ = True
    return obj


class ABCMeta(NativeABCMeta):

    def __call__(cls, *args, **kwargs):
        instance = NativeABCMeta.__call__(cls, *args, **kwargs)
        abstract_attributes = {
            name
            for name in dir(instance)
            if getattr(getattr(instance, name), '__is_abstract_attribute__', False)
        }
        if abstract_attributes:
            raise NotImplementedError(
                "Can't instantiate abstract class {} with"
                " abstract attributes: {}".format(
                    cls.__name__,
                    ', '.join(abstract_attributes)
                )
            )
        return instance

不错的是,您可以执行以下操作:

class AbstractFoo(metaclass=ABCMeta):
    bar = abstract_attribute()

它的工作原理与上述相同。

也可以使用:

class ABC(ABCMeta):
    pass

定义自定义ABC助手。PS。我认为这段代码是CC0。

可以通过使用AST解析器通过扫描__init__代码更早地(在类声明时)引发此问题来进行改进,但目前看来,这已经过高(除非有人愿意实现)。


1
非常简洁,这确实是我怀疑存在的一种解决方案(我想我将更改接受的答案)。事后看来,声明的语法确实很奇怪。从长远来看,在python中起作用的是鸭嘴式输入:ABC和注释只会使代码看起来像可怜的Java(语法和结构)。
Lapinot

3
我希望我能对此再投票。希望该解决方案可以在某个时候集成到标准库中。谢谢。
BCR

有什么理由为什么要在装饰器中创建DummyAttribute而不是设置?obj = object()abstract_attribute
安迪·佩雷斯

1
@AndyPerez,如果您具有命名类的对象而不是普通对象,则调试起来可能会更容易,但我认为,obj = object()如果愿意,可以轻松地在代码中将其更改为。
krassowski

谢谢,@ krassowski。继续提问:我尝试在ABC中实现此功能,但是我遇到了问题,因为__setattr__在设置属性之前,我要重写,对提供的值执行验证。它总是抛出一个错误,指出该属性为“只读”。有办法解决这个问题吗?
安迪·佩雷斯

21

仅仅因为您将其定义为abstractproperty抽象基类上的对象,并不意味着您必须在子类上创建属性。

例如,您可以:

In [1]: from abc import ABCMeta, abstractproperty

In [2]: class X(metaclass=ABCMeta):
   ...:     @abstractproperty
   ...:     def required(self):
   ...:         raise NotImplementedError
   ...:

In [3]: class Y(X):
   ...:     required = True
   ...:

In [4]: Y()
Out[4]: <__main__.Y at 0x10ae0d390>

如果要初始化其中的值,__init__可以执行以下操作:

In [5]: class Z(X):
   ...:     required = None
   ...:     def __init__(self, value):
   ...:         self.required = value
   ...:

In [6]: Z(value=3)
Out[6]: <__main__.Z at 0x10ae15a20>

因为Python 3.3abstractproperty弃用。因此,Python 3用户应改用以下内容:

from abc import ABCMeta, abstractmethod

class X(metaclass=ABCMeta):
    @property
    @abstractmethod
    def required(self):
        raise NotImplementedError

遗憾的是,ABC检查没有在实例初始化而不是在类定义时发生,这样抽象属性才有可能。这将使我们拥有一个abstractattribute_or_property-这就是操作员想要的
克里斯

init中添加attr对我不起作用。python3.6
zhukovgreen

1
@ArtemZhukov上面的示例是IPython中的Python 3.6会话,它可以正常工作。required = None如上所示,您同样需要类中的主体。
Anentropic

1
@chris,请参阅我的stackoverflow.com/a/50381071/6646912,了解ABC子类,该子类在初始化时进行检查。
krassowski

@krassowski只能实现上述@abstractproperty定义,但效果更糟,因为它不处理在类主体中定义的属性,而不是__init__
Anentropic

7

问题不是什么,而是什么时候

from abc import ABCMeta, abstractmethod

class AbstractFoo(metaclass=ABCMeta):
    @abstractmethod
    def bar():
        pass

class Foo(AbstractFoo):
    bar = object()

isinstance(Foo(), AbstractFoo)
#>>> True

没关系,这bar不是方法!问题在于__subclasshook__,进行检查的方法是classmethod,因此仅关心(而不是实例)是否具有属性。


我建议您不要强迫这样做,因为这是一个难题。另一种方法是强制他们预定义属性,但是只会留下使错误静音的虚拟属性。


是的,这是我从现在开始所做的事情(但是将其定义为抽象方法很奇怪)。但这并不能解决实例属性的问题。
Lapinot 2014年

我认为是一个更好(和更多pythonic)的解决方案。
kawing-chiu

3

正如Anentropic所说,您不必将anabstractproperty作为另一个实现property

但是,所有答案似乎都忽略了一件事,那就是Python的成员插槽(__slots__类属性)。__slots__如果只需要数据属性,则实现抽象属性所需的ABC用户可以简单地在其中定义它们。

所以像这样

class AbstractFoo(abc.ABC):
    __slots__ = ()

    bar = abc.abstractproperty()

用户可以定义子类,就像

class Foo(AbstractFoo):
    __slots__ = 'bar',  # the only requirement

    # define Foo as desired

    def __init__(self):
        self.bar = ...

在这里,Foo.bar其行为类似于常规实例属性,只是以不同的方式实现。这是简单,高效的方法,并且避免了@property您描述的样板。

无论ABC是否__slots__在其班级的身体中定义,这都是可行的。然而,__slots__一路走来不仅节省了内存并提供了更快的属性访问,而且还提供了有意义的描述符,而不是bar = None在子类中使用了中间体(例如类似的东西)。1

一些答案建议实例化(即在元类__call__()方法中)进行“抽象”属性检查但是我发现这不仅浪费而且潜在地效率低下,因为初始化步骤可能是一个耗时的过程。

简而言之,ABC的子类需要重写相关的描述符(无论是属性还是方法),这无关紧要,并且向您的用户证明可以__slots__用作抽象属性的实现对我来说是更适当的方法。


1 在任何情况下,至少ABC都应始终定义一个空的__slots__类属性,因为在实例化时,子类将被迫具有__dict__(动态属性访问)和__weakref__(弱引用支持)。有关标准库中这种情况的示例,请参见abccollections.abc模块。


0

我已经搜索了一段时间,但没有看到我喜欢的东西。您可能知道是否这样做:

class AbstractFoo(object):
    @property
    def bar(self):
        raise NotImplementedError(
                "Subclasses of AbstractFoo must set an instance attribute "
                "self._bar in it's __init__ method")

class Foo(AbstractFoo):
    def __init__(self):
        self.bar = "bar"

f = Foo()

你得到一个AttributeError: can't set attribute烦人的。

要解决此问题,您可以执行以下操作:

class AbstractFoo(object):

    @property
    def bar(self):
        try:
            return self._bar
        except AttributeError:
            raise NotImplementedError(
                "Subclasses of AbstractFoo must set an instance attribute "
                "self._bar in it's __init__ method")

class OkFoo(AbstractFoo):
    def __init__(self):
        self._bar = 3

class BadFoo(AbstractFoo):
    pass

a = OkFoo()
b = BadFoo()
print a.bar
print b.bar  # raises a NotImplementedError

这就避免了,AttributeError: can't set attribute但是如果您只放弃了abstract属性,那么可以避免:

class AbstractFoo(object):
    pass

class Foo(AbstractFoo):
    pass

f = Foo()
f.bar

您可以得到AttributeError: 'Foo' object has no attribute 'bar'几乎与NotImplementedError一样好的。因此,实际上我的解决方案只是交换来自​​另一个的错误消息..而您必须在init中使用self._bar而不是self.bar 。


首先,我想提一下,尽管命名了您的类AbstractFoo,但实际上并没有使您的类抽象化,并且此答案与OP设置的上下文完全无关。也就是说,针对常规属性的“无法设置的属性”问题的解决方法可能会很昂贵,具体取决于访问属性的频率。一个便宜的(基于您的第一个代码块)是您的Foo.__init__()self.__dict__['bar'] = "bar"
mikenerone19年

0

https://docs.python.org/2/library/abc.html之后,您可以在Python 2.7中执行以下操作:

from abc import ABCMeta, abstractproperty


class Test(object):
    __metaclass__ = ABCMeta

    @abstractproperty
    def test(self): yield None

    def get_test(self):
        return self.test


class TestChild(Test):

    test = None

    def __init__(self, var):
        self.test = var


a = TestChild('test')
print(a.get_test())
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.