Python 中的抽象属性

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

abstract class Controller {


val path: String


}

一个 Controller的子类被 Scala 编译器强制定义“ path”:

class MyController extends Controller {


override val path = "/home"


}
134868 次浏览

Bastien Léonard 的答案提到了抽象基类模块,Brendan Abel 的答案处理了引发错误的未实现属性。为了确保类不在模块之外实现,可以在基名前加一个下划线,表示它是模块的私有类(也就是说,它没有被导入)。

也就是说。

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


class MyController(_Controller):
path = '/Home'

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

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

Python 对此有一个内置的异常,不过在运行时之前不会遇到这个异常。

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




class SubClass(Base):
path = 'blah'

基类可以实现检查 class 属性的 __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

这样,在实例化时错误就会产生

您可以在 ABC抽象基类中创建一个带有值(如 NotImplemented)的属性,这样,如果该属性未被重写并随后使用,则在运行时会显示一个表示意图的明确错误。

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

from abc import ABC


class Controller(ABC):
path: str = NotImplemented


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

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,如:

不能用抽象方法 a实例化抽象类 B

Python 2.7

有一个 @ 抽象属性的装饰工程师:

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

Python 3.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>
class AbstractStuff:
@property
@abc.abstractmethod
def some_property(self):
pass

我认为,从3.3版本开始,abc.abstractproperty已经被废弃了。

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

from abc import ABC, abstractmethod




class Base(ABC):


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


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




class Derived(Base):
CONSTANT = 42

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

还有其他的实现方法,但我喜欢这种语法,因为对于读者来说,它似乎是最简单明了的。

以前的答案都很有用,但我觉得公认的答案并不直接回答这个问题,因为

  • 这个问题要求在抽象类中实现,但是公认的答案并不遵循抽象形式主义。
  • 这个问题要求强制执行。在这个答案中,我认为实现更为严格,因为如果没有定义 CONSTANT,实例化子类时会导致运行时错误。接受的答案允许对象被实例化,并且只在访问 CONSTANT时抛出一个错误,从而降低了执行的严格程度。

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

在 Python 3.6 + 中,可以对抽象类(或任何变量)的属性进行注释,而不需要为该属性提供值。

from abc import ABC


class Controller(ABC):
path: str


class MyController(Controller):
def __init__(self, path: str):
self.path = path

这使得代码非常简洁,很明显,属性是抽象的。如果没有被覆盖,则尝试访问该属性的代码将引发 AttributeError

在 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'
)

如果没有定义缺少的类变量,这将在子类的初始化时引发一个错误,因此您不必等到访问缺少的类变量时再进行操作。

对于 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本身并没有说明为什么缺少实现。此外,它实际上需要体力劳动来养育它。

我只是修改了一点 @ 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