抽象属性(非属性)?

124 投票
8 回答
41855 浏览
提问于 2025-04-18 07:24

如何定义一个抽象的实例属性,但不使用属性装饰器,最好的做法是什么?

我想写成这样:

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 个回答

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,所以它只关注类本身是否有这个属性,而不是实例。


我建议你不要强行去解决这个问题,因为这很难。另一种选择是让他们提前定义这个属性,但这样只会留下无用的属性,反而只是让错误静默不报。

9

正如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__(弱引用支持)。可以查看abccollections.abc模块,了解标准库中这种情况的例子。

41

仅仅因为你在抽象基类里定义了一个 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
76

这里有一个可能比接受的答案更好的解决方案:

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")
33

如果你真的想要确保一个子类必须定义某个属性,你可以使用元类:

 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

撰写回答