【问题标题】:Abstract attributes in Python [duplicate]Python中的抽象属性[重复]
【发布时间】:2011-02-13 17:48:28
【问题描述】:

在 Python 中用抽象属性实现以下 Scala 代码的最短/最优雅的方法是什么?

abstract class Controller {

    val path: String

}

Scala 编译器强制使用Controller 的子类来定义“路径”。子类如下所示:

class MyController extends Controller {

    override val path = "/home"

}

【问题讨论】:

  • 你试过什么?如果您对解决方案有任何问题或疑问,请发布您的 Python 代码。
  • “Scala 编译器强制使用 Controller 的子类来定义“路径”。” ... 什么时候强制执行?如果是编译时间,那你就不走运了。如果它是运行时的,那么您究竟希望它如何“强制执行”?换句话说,引发 AttributeError 和 NotImplementedError 有区别吗?为什么?
  • 我知道 Python 是一种动态语言,python 解释器不能强制执行静态类型。对我来说重要的是,它尽可能早地失败,并且很容易找到错误发生的位置和原因。
  • 对于较新的副本有一些相关答案:stackoverflow.com/questions/23831510/…,但基本上得出的结论是,从 python 3.8 开始,没有好的解决方案。

标签: python oop scala abstract-class


【解决方案1】:

Python 3.3+

from abc import ABCMeta, abstractmethod


class A(metaclass=ABCMeta):
    def __init__(self):
        # ...
        pass

    @property
    @abstractmethod
    def a(self):
        pass

    @abstractmethod
    def b(self):
        pass


class B(A):
    a = 1

    def b(self):
        pass

未能在派生类B 中声明ab 将引发TypeError 例如:

TypeError:无法用抽象方法a实例化抽象类B

Python 2.7

为此有一个 @abstractproperty 装饰器:

from abc import ABCMeta, abstractmethod, abstractproperty


class A:
    __metaclass__ = ABCMeta

    def __init__(self):
        # ...
        pass

    @abstractproperty
    def a(self):
        pass

    @abstractmethod
    def b(self):
        pass


class B(A):
    a = 1

    def b(self):
        pass

【讨论】:

  • 我的“A”类是 Exception 的子类,这似乎破坏了示例。
  • @Chris2048 它是如何破坏它的?你遇到了什么错误?
  • 有没有办法以类似于this question 的与 Python 2/3 无关的方式编写它?
  • 您的 python 3 解决方案将a 实现为B 的类属性,而不是对象属性。如果你改为 ``` class B(A): def __init__(self): self.a=1 ... ``` 实例化B 时会出现错误:“Can't instantiate abstract class B with abstract方法一"
  • @Janus 提出了一个重要的观点......能够class MyController(val path: String) extends Controller 是 scala 版本中提供的一个有价值的选项,但这里没有
【解决方案2】:

Python 对此有一个内置的异常,但直到运行时才会遇到该异常。

class Base(object):
    @property
    def path(self):
        raise NotImplementedError


class SubClass(Base):
    path = 'blah'

【讨论】:

  • 具体来说,在访问属性之前,您不会遇到异常,在这种情况下,无论如何您都会遇到 AttributeError。
  • 我认为提出NotImplementedError 更明确,因此可能比将其留给AttributeError 更好。
  • 并不是说这仅在 path 直接设置在 SubClass 上时才有效。给定一个实例sc = SubClass(),如果您尝试设置sc.path = 'blah' 或有一个包含self.path = 'blah' 之类的方法而不直接在SubClass 上定义path,您将得到一个AttributeError: can't set attribute
  • 这不是答案!!这不是实例字段。类中的Scala文件是实例字段,因为Scala将静态和实例分开。
【解决方案3】:

自从最初提出这个问题以来,python 已经改变了抽象类的实现方式。我在 python 3.6 中使用了 abc.ABC 形式,使用了稍微不同的方法。这里我将常量定义为必须在每个子类中定义的属性。

from abc import ABC, abstractmethod


class Base(ABC):

    @property
    @classmethod
    @abstractmethod
    def CONSTANT(cls):
        return NotImplementedError

    def print_constant(self):
        print(type(self).CONSTANT)


class Derived(Base):
    CONSTANT = 42

这会强制派生类定义常量,否则当您尝试实例化子类时会引发TypeError 异常。当您想将该常量用于抽象类中实现的任何功能时,您必须通过type(self).CONSTANT 而不是仅通过CONSTANT 访问子类常量,因为该值在基类中未定义。

还有其他方法可以实现这一点,但我喜欢这种语法,因为在我看来,它对读者来说是最简单明了的。

前面的答案都触及了有用的点,但我觉得接受的答案并没有直接回答问题,因为

  • 问题要求在抽象类中实现,但接受的答案不遵循抽象形式。
  • 问题要求强制执行。我认为在这个答案中执行更严格,因为如果未定义CONSTANT,则在实例化子类时会导致运行时错误。接受的答案允许实例化对象,并且仅在访问CONSTANT 时抛出错误,从而使执行不那么严格。

这不是对原始答案的错误。抽象类语法自发布以来发生了重大变化,在这种情况下允许更整洁和更实用的实现。

【讨论】:

  • 这似乎是唯一正确的答案,也让mypy
  • 感谢您实际上解决问题的唯一答案之一。请问print_constant方法的目的是什么?据我所知,它什么也没做?
  • 这里不实例化Derived类是不能调用的。因此,如果您删除 @classmethod 装饰器,则没有区别
  • 我以前查过这个问题,但从未注意到您的回答。它应该是公认的,如果我知道可以堆叠 @property@classmethod 装饰器,我早就使用过。谢谢
  • @BrianJoseph - print_constant 方法用于说明如何访问父类中的常量,因为它只在子类中定义
【解决方案4】:

In Python 3.6+,您可以注释抽象类(或任何变量)的属性,而无需为该属性提供值。

from abc import ABC

class Controller(ABC):
    path: str

class MyController(Controller):
    path = "/home"

这使得代码非常干净,很明显属性是抽象的。在 if 未被覆盖时尝试访问该属性的代码将引发 AttributeError

【讨论】:

  • 在 Python 3.7 中,如果 MyController 是在没有路径属性的情况下定义的,我不会收到任何错误。为什么?
  • @DaniTeba 此解决方案在访问时引发错误,而不是在定义时。如果在类定义时引发错误对您很重要,您将不得不使用装饰器解决方案之一。
【解决方案5】:

您可以在abc.ABC 抽象基类中创建一个属性,其值例如为NotImplemented,这样如果该属性未被覆盖然后使用,则在运行时会显示错误。

以下代码使用PEP 484 类型提示来帮助PyCharm 正确地静态分析path 属性的类型。

from abc import ABC


class Controller(ABC):
    path: str = NotImplemented

class MyController(Controller):
    path = "/home"

【讨论】:

  • 注意这不是实例字段。 Scala 字段始终是类定义中的实例成员。因为 Scala 将静态和实例分开。
  • 请注意,这是出于不同目的而滥用NotImplemented。这也是我的想法,所以我有点赞同 ^^ 我认为@drhagen 的答案更正确,但使用= NotImplemented 更明显。
  • 如果我错了,请纠正我,但即使没有这个,如果使用了一个属性并且没有覆盖或定义,它会抛出一个错误。这是任何类的基本行为。
  • @Corel 的想法是,这使得意图非常明确和可发现。
【解决方案6】:

对于 Python 3.3+ 有一个优雅的解决方案

from abc import ABC, abstractmethod

class BaseController(ABC):
    @property
    @abstractmethod
    def path(self) -> str:
        ...

class Controller(BaseController):
    path = "/home"


# Instead of an elipsis, you can add a docstring for clarity
class AnotherBaseController(ABC):
    @property
    @abstractmethod
    def path(self) -> str:
    """
    :return: the url path of this controller
    """

尽管已经给出了一些很好的答案,但我认为这个答案仍然会增加一些价值。这种方法有两个优点:

  1. ... 在抽象方法的主体中比pass 更可取。与pass 不同,... 表示无操作,而pass 仅表示没有实际实现

  2. ... 比抛出 NotImplementedError(...) 更推荐。如果子类中缺少抽象字段的实现,这会自动提示极其冗长的错误。相比之下,NotImplementedError 本身并没有说明为什么缺少实现。此外,它需要手工劳动才能真正抬起它。

【讨论】:

  • 您的第 2 点不正确。无论您键入 ... 还是 raise NotImplementedError 或只是 pass 都不会影响尝试使用未实现的方法实例化抽象类(或子类)时引发的错误。
  • 当你引发 NotImplementedError 时,你可以传递一个字符串消息来告诉调用者关于为什么抛出错误的更详细的信息。
  • 最好至少为抽象方法声明一个文档字符串,而不是pass 或省略号(...)。 pylint 告诉我的。
  • 谢谢 sausix,这是一个很好的观点。我会把它包含在答案中
【解决方案7】:

从 Python 3.6 开始,您可以使用 __init_subclass__ 在初始化时检查子类的类变量:

from abc import ABC

class A(ABC):
    @classmethod
    def __init_subclass__(cls):
        required_class_variables = [
            'foo',
            'bar',
        ]
        for var in required_class_variables:
            if not hasattr(cls, var):
                raise NotImplementedError(
                    f'Class {cls} lacks required `{var}` class attribute'
                )

如果未定义缺少的类变量,这会在子类初始化时引发错误,因此您不必等到缺少的类变量被访问。

【讨论】:

  • __init_subclass__ 在子类初始化之前调用父类。所以如果你想检查子类的__init__ 中设置的属性,这是行不通的。
  • @notnami 这是真的。但是,该问题要求提供类变量,而不是在 __init__ 方法期间设置的实例变量。
  • 哼...是我还是你唯一一个严格解决这个问题的人? :) +1。
【解决方案8】:

我只修改了一点@James 答案,这样所有这些装饰器就不会占据太多位置。如果你有多个这样的抽象属性要定义,这很方便:

from abc import ABC, abstractmethod

def abstractproperty(func):
   return property(classmethod(abstractmethod(func)))

class Base(ABC):

    @abstractproperty
    def CONSTANT(cls): ...

    def print_constant(self):
        print(type(self).CONSTANT)


class Derived(Base):
    CONSTANT = 42

class BadDerived(Base):
    BAD_CONSTANT = 42

Derived()       # -> Fine
BadDerived()    # -> Error

【讨论】:

    【解决方案9】:

    Python3.6 的实现可能如下所示:

    In [20]: class X:
        ...:     def __init_subclass__(cls):
        ...:         if not hasattr(cls, 'required'):
        ...:             raise NotImplementedError
    
    In [21]: class Y(X):
        ...:     required = 5
        ...:     
    
    In [22]: Y()
    Out[22]: <__main__.Y at 0x7f08408c9a20>
    

    【讨论】:

    • 不确定这是否会引发类型错误,但如果您的子类未实现该属性,以防您使用来自 IDE 的类型检查
    【解决方案10】:

    您的基类可以实现一个检查类属性的__new__ 方法:

    class Controller(object):
        def __new__(cls, *args, **kargs):
            if not hasattr(cls,'path'): 
                raise NotImplementedError("'Controller' subclasses should have a 'path' attribute")
            return object.__new__(cls)
    
    class C1(Controller):
        path = 42
    
    class C2(Controller):
        pass
    
    
    c1 = C1() 
    # ok
    
    c2 = C2()  
    # NotImplementedError: 'Controller' subclasses should have a 'path' attribute
    

    这样在实例化时会引发错误

    【讨论】:

    • 有趣的是,如果您需要使用 def __init__(self),这将不起作用。
    【解决方案11】:

    Bastien Léonard 的回答提到了抽象基类模块,而 Brendan Abel 的回答涉及引发错误的未实现属性。为了确保该类不在模块之外实现,您可以在基名称前加上下划线,表示它是模块私有的(即它不被导入)。

    class _Controller(object):
        path = '' # There are better ways to declare attributes - see other answers
    
    class MyController(_Controller):
        path = '/Home'

    【讨论】:

    • 如果子类没有重新定义属性,是否可能引发一些错误?方法很容易,但是属性呢?
    • _Controller类中省略路径声明不是更好吗?如果已经存在(无效)值,则 Duck Typing 将不会生效。否则在某些时候,我需要定义 path 字段,不会有错误,因为已经有一个值。
    • @Mario - 是的,Brendan Abel 的回答提供了一个很好的方法来做到这一点
    【解决方案12】:
    class AbstractStuff:
        @property
        @abc.abstractmethod
        def some_property(self):
            pass
    

    我认为,从 3.3 开始,abc.abstractproperty 已被弃用。

    【讨论】:

      【解决方案13】:

      看看 abc(抽象基类)模块:http://docs.python.org/library/abc.html

      但是,在我看来,最简单和最常见的解决方案是在创建基类的实例或访问其属性时引发异常。

      【讨论】:

      • 请详细说明:abc 模块在这种情况下有何帮助?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-05-21
      • 2014-10-11
      • 2012-07-23
      • 1970-01-01
      • 1970-01-01
      • 2014-09-28
      相关资源
      最近更新 更多