【问题标题】:Abstract attribute (not property)?抽象属性(不是属性)?
【发布时间】:2014-07-12 22:45:09
【问题描述】:

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

我想写这样的东西:

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

Abstract attributes in Python 问题建议作为使用@property@abstractmethod 的唯一答案:它没有回答我的问题。

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

【问题讨论】:

  • 为什么你需要强制某人在他们的类上具有特定属性?
  • 这不就是ABC的全部吗?如果您想要我的具体示例,我希望人们为他们的传感器编写一个类,并且该类应该有一个 self.port 属性。
  • 经过反思,是的,我想是的;虽然我认为这与鸭式打字相悖......
  • 也许我只是要求太多复杂性,但是在做抽象类时不使用 ABC 会打扰我(我想我只会使用普通的基类)...
  • anentropics 的解决方案很简单,效果很好。为什么它不是公认的答案?

标签: python abstract


【解决方案1】:

与公认的答案相比,一个可能更好的解决方案:

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__ 代码来提高(在类声明时),但现在看来有点矫枉过正(除非有人愿意实现)。

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 在 cmets 中所建议的那样:

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


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

【讨论】:

  • 非常简洁,这确实是我怀疑存在的那种解决方案(我想我会改变接受的答案)。事后看来,声明的语法真的很奇怪。从长远来看,python 中起作用的是鸭子类型:ABC 和注释只会让代码看起来像糟糕的 java(语法和结构)。
  • 我希望我能更多地支持这个。希望这个解决方案会在某个时候集成到标准库中。谢谢。
  • 我已经开始打包此代码 sn-p:pip install better-abc。如果您想贡献一些测试或附加功能,请打开拉取请求here
  • 如果其他人对这个优雅的解决方案破坏 pylint 类型提示感到沮丧,您可以更进一步,将 bar = abstract_attribute() 设置为 bar: int = abstract_attribute()。可能是常识,但我花了几个小时才意识到这一点。
  • 这是一个很好的评论@SMiller,谢谢!我用打字支持所需的草稿更新了答案(它可能不会涵盖所有边缘情况,但我希望它会有所帮助)。
【解决方案2】:

如果你真的想强制一个子类定义一个给定的属性,你可以使用元类:

 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__ 以确保在实例的 init 之后调用 check_bar

GoodFoo()  # ok
BadFoo ()  # yield NotImplementedError

【讨论】:

    【解决方案3】:

    仅仅因为您在抽象基类上将其定义为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 abstractpropertydeprecated。所以 Python 3 用户应该使用以下代码:

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

    【讨论】:

    • 遗憾的是,ABC 检查没有发生在实例初始化而不是类定义时,这样抽象属性是可能的。这将允许我们拥有一个 abstractattribute_or_property - 这就是 op 想要的
    • init 中添加属性对我不起作用。 python3.6
    • @ArtemZhukov 上面的示例是 IPython 中的 Python 3.6 会话,它可以工作。不过,如上所示,您还需要类主体中的required = None
    • @chris,请参阅我的stackoverflow.com/a/50381071/6646912,了解在初始化时进行检查的 ABC 子类。
    • @krassowski 除了上面的@abstractproperty 定义之外什么也没有,但更糟糕的是,它不处理类主体中定义的属性,而不是__init__
    【解决方案4】:

    问题不在于什么,而在于何时

    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,所以只关心是否是class,而不关心instance,有属性。


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

    【讨论】:

    • 是的,这就是我从现在开始一直在做的事情(但是将其定义为抽象的方法很奇怪)。但它并没有解决实例属性的问题。
    • 我认为this 是一个更好(而且更符合pythonic)的解决方案。
    【解决方案5】:

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

    但是,所有答案似乎都忽略了一件事情是 Python 的成员槽(__slots__ 类属性)。需要实现抽象属性的 ABC 用户只需在 __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 或类似的)。1

    一些答案​​建议进行“抽象”属性检查之后 实例化(即在元类__call__() 方法),但我发现这不仅浪费而且作为初始化步骤可能效率低下可能会很耗时。

    简而言之,ABC 的子类需要的是覆盖相关的描述符(无论是属性还是方法),不管如何,并向您的用户记录可以使用 __slots__在我看来,抽象属性的实现是更合适的方法。


    1无论如何,至少,ABC 应该总是定义一个空的__slots__ 类属性,否则子类将被强制拥有__dict__(动态属性访问)和__weakref__(弱引用支持)在实例化时。请参阅abccollections.abc 模块以了解标准库中的示例。

    【讨论】:

    • 当 abc.abstractproperty 被弃用时,这如何站得住脚?
    【解决方案6】:

    我已经搜索了一段时间,但没有看到任何我喜欢的东西。你可能知道,如果你这样做:

    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,但如果你只是把抽象属性放在一起:

    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"
    【解决方案7】:

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

    【讨论】:

      猜你喜欢
      • 2011-11-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-02-23
      • 1970-01-01
      相关资源
      最近更新 更多