抽象属性(非属性) ? ?

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

我想写下这样的话:

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,因为他本可以在 __init__中编写 self.foo = foo)。

Python 中的抽象属性 问题建议将其作为使用 @property@abstractmethod的唯一答案: 它没有回答我的问题。

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

35228 次浏览

The problem isn't what, but when:

from abc import ABCMeta, abstractmethod


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


class Foo(AbstractFoo):
bar = object()


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

It doesn't matter that bar isn't a method! The problem is that __subclasshook__, the method of doing the check, is a classmethod, so only cares whether the class, not the instance, has the attribute.


I suggest you just don't force this, as it's a hard problem. The alternative is forcing them to predefine the attribute, but that just leaves around dummy attributes that just silence errors.

If you really want to enforce that a subclass define a given attribute, you can use metaclasses:

 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

Basically the meta class redefine __call__ to make sure check_bar is called after the init on an instance.

GoodFoo()  # ok
BadFoo ()  # yield NotImplementedError

I've searched around for this for awhile but didn't see anything I like. As you probably know if you do:

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()

You get an AttributeError: can't set attribute which is annoying.

To get around this you can do:

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

This avoids the AttributeError: can't set attribute but if you just leave off the abstract property all together:

class AbstractFoo(object):
pass


class Foo(AbstractFoo):
pass


f = Foo()
f.bar

You get an AttributeError: 'Foo' object has no attribute 'bar' which is arguably almost as good as the NotImplementedError. So really my solution is just trading one error message from another .. and you have to use self._bar rather than self.bar in the init.

Just because you define it as an abstractproperty on the abstract base class doesn't mean you have to make a property on the subclass.

e.g. you can:

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>

If you want to initialise the value in __init__ you can do this:

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>

Since Python 3.3 abstractproperty is deprecated. So Python 3 users should use the following instead:

from abc import ABCMeta, abstractmethod


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

Following https://docs.python.org/2/library/abc.html you could do something like this in 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())

A possibly a bit better solution compared to the accepted answer:

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

It will behave like this:

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

This answer uses same approach as the accepted answer, but integrates well with built-in ABC and does not require boilerplate of check_bar() helpers.

Here is the better_abc.py content:

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

The nice thing is that you can do:

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

and it will work same as above.

Also one can use:

class ABC(ABCMeta):
pass

to define custom ABC helper. PS. I consider this code to be CC0.

This could be improved by using AST parser to raise earlier (on class declaration) by scanning the __init__ code, but it seems to be an overkill for now (unless someone is willing to implement).

2021: typing support

You can use:

from typing import cast, Any, Callable, TypeVar




R = TypeVar('R')




def abstract_attribute(obj: Callable[[Any], R] = None) -> R:
_obj = cast(Any, obj)
if obj is None:
_obj = DummyAttribute()
_obj.__is_abstract_attribute__ = True
return cast(R, _obj)

which will let mypy highlight some typing issues

class AbstractFooTyped(metaclass=ABCMeta):


@abstract_attribute
def bar(self) -> int:
pass




class FooTyped(AbstractFooTyped):
def __init__(self):
# skipping assignment (which is required!) to demonstrate
# that it works independent of when the assignment is made
pass




f_typed = FooTyped()
_ = f_typed.bar + 'test'   # Mypy: Unsupported operand types for + ("int" and "str")




FooTyped.bar = 'test'    # Mypy: Incompatible types in assignment (expression has type "str", variable has type "int")
FooTyped.bar + 'test'    # Mypy: Unsupported operand types for + ("int" and "str")

and for the shorthand notation, as suggested by @SMiller in the comments:

class AbstractFooTypedShorthand(metaclass=ABCMeta):
bar: int = abstract_attribute()




AbstractFooTypedShorthand.bar += 'test'   # Mypy: Unsupported operand types for + ("int" and "str")

As Anentropic said, you don't have to implement an abstractproperty as another property.

However, one thing all answers seem to neglect is Python's member slots (the __slots__ class attribute). Users of your ABCs required to implement abstract properties could simply define them within __slots__ if all that's needed is a data attribute.

So with something like,

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


bar = abc.abstractproperty()

Users can define sub-classes simply like,

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


# define Foo as desired


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

Here, Foo.bar behaves like a regular instance attribute, which it is, just implemented differently. This is simple, efficient, and avoids the @property boilerplate that you described.

This works whether or not ABCs define __slots__ at their class' bodies. However, going with __slots__ all the way not only saves memory and provides faster attribute accesses but also gives a meaningful descriptor instead of having intermediates (e.g. bar = None or similar) in sub-classes.1

A few answers suggest doing the "abstract" attribute check after instantiation (i.e. at the meta-class __call__() method) but I find that not only wasteful but also potentially inefficient as the initialization step could be a time-consuming one.

In short, what's required for sub-classes of ABCs is to override the relevant descriptor (be it a property or a method), it doesn't matter how, and documenting to your users that it's possible to use __slots__ as implementation for abstract properties seems to me as the more adequate approach.


1 In any case, at the very least, ABCs should always define an empty __slots__ class attribute because otherwise sub-classes are forced to have __dict__ (dynamic attribute access) and __weakref__ (weak reference support) when instantiated. See the abc or collections.abc modules for examples of this being the case within the standard library.