抽象属性(非属性)?
如何定义一个抽象的实例属性,但不使用属性装饰器,最好的做法是什么?
我想写成这样:
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
,但这并没有回答我的问题。
ActiveState 提供的一个关于抽象类属性的做法,通过 AbstractAttribute
可能是个不错的办法,但我不太确定。这个方法也只适用于类属性,而不适用于实例属性。
8 个回答
问题不在于是什么,而在于什么时候:
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
,所以它只关注类本身是否有这个属性,而不是实例。
我建议你不要强行去解决这个问题,因为这很难。另一种选择是让他们提前定义这个属性,但这样只会留下无用的属性,反而只是让错误静默不报。
正如Anentropic所说,你不需要把一个abstractproperty
实现成另一个property
。
不过,所有的回答似乎都忽略了Python的成员槽(也就是__slots__
这个类属性)。如果你的抽象基类(ABCs)需要实现抽象属性,用户可以简单地在__slots__
里定义它们,只要需要的是一个数据属性就行。
比如说,
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
这样的中间变量。
有些回答建议在实例化后进行“抽象”属性检查(也就是在元类的__call__()
方法中),但我觉得这样不仅浪费,还可能效率低下,因为初始化步骤可能会耗时。
总之,对于ABC的子类来说,所需要做的就是重写相关的描述符(无论是属性还是方法),具体怎么做并不重要,并且向用户说明可以使用__slots__
来实现抽象属性,这似乎是更合适的方法。
1 无论如何,ABC至少应该始终定义一个空的__slots__
类属性,因为否则子类在实例化时会被迫拥有__dict__
(动态属性访问)和__weakref__
(弱引用支持)。可以查看abc
或collections.abc
模块,了解标准库中这种情况的例子。
仅仅因为你在抽象基类里定义了一个 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.3 以后,abstractproperty
已经被 弃用。所以 Python 3 的用户应该使用以下方式:
from abc import ABCMeta, abstractmethod
class X(metaclass=ABCMeta):
@property
@abstractmethod
def required(self):
raise NotImplementedError
这里有一个可能比接受的答案更好的解决方案:
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 辅助函数。顺便说一下,我认为这段代码是 CC0 的。
如果使用 AST 解析器来提前检查(在类声明时)并扫描 __init__
代码,这个方案可以进一步改进,但目前看来这有点过于复杂(除非有人愿意去实现)。
2021: 类型支持
你可以使用:
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)
这将让 mypy 突出一些类型问题
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")
对于简写的表示法,正如 @SMiller 在评论中提到的:
class AbstractFooTypedShorthand(metaclass=ABCMeta):
bar: int = abstract_attribute()
AbstractFooTypedShorthand.bar += 'test' # Mypy: Unsupported operand types for + ("int" and "str")
如果你真的想要确保一个子类必须定义某个属性,你可以使用元类:
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
。
GoodFoo() # ok
BadFoo () # yield NotImplementedError