Python中的抽象属性[重复]


91

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

abstract class Controller {

    val path: String

}

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

class MyController extends Controller {

    override val path = "/home"

}

1
你尝试了什么?如果您对解决方案有任何问题或疑问,请发布您的Python代码。
S.Lott

“ Scala编译器强制执行Controller的子类来定义“路径”。...何时强制执行?如果是编译时间,那么您就不走运了。如果是运行时,那么您究竟希望它如何“强制执行”?换句话说,引发AttributeError和NotImplementedError有区别吗?为什么?
不错,2010年

2
我知道Python是一种动态语言,并且python解释器无法强制使用静态类型。对我而言,重要的是,它应尽早失败,并且很容易找到导致错误的原因和原因所在。
迪蒙


对于较新的副本有一些相关的答案:stackoverflow.com/questions/23831510/…,但基本上,要点是,从python 3.8开始,还没有很好的解决方案。
Janus,

Answers:


92

Python有一个内置的异常,尽管直到运行时您都不会遇到该异常。

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


class SubClass(Base):
    path = 'blah'

13
具体来说,在访问attrtibute之前,您不会遇到异常,在这种情况下,无论如何您都会遇到AttributeError。
本·詹姆斯

5
我认为提出aNotImplementedError更为明确,因此可能比将其交给a更好AttributeError
blokeley

您也可以在自己引发异常时添加诸如“无法实例化抽象类Base”之类的消息。
巴斯蒂安·莱纳德(BastienLéonard)2010年

12
并非仅当path直接在上设置时才有效SubClass。给定一个实例sc = SubClass(),如果您尝试设置sc.path = 'blah'或有一个包含诸如此类的方法self.path = 'blah'而没有path直接定义的方法SubClass,您将得到一个AttributeError: can't set attribute
erik

3
这不是答案!!这不是实例字段。类中提交的Scala是实例字段,因为Scala将静态和实例分开。
WeiChing林炜清

101

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

未声明a或未b在派生类中B引发以下内容TypeError

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

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 '18

1
@ Chris2048如何打破它?你遇到了什么错误?
乔尔,

有没有办法像这个问题一样以Python 2/3不可知的方式编写此代码?
bluenote10 '19

7
您的python 3解决方案实现a为的类属性B,而不是对象属性。如果改为使用类B(A):def __init __(self):self.a = 1 ...```实例化时会出现错误B:“无法使用抽象方法a实例化抽象类B”
Janus

@Janus提出了一个重要的观点...能够class MyController(val path: String) extends Controller成为Scala版本中可用的有价值的选择,但在此缺失
joel

46

自从最初提出这个问题以来,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而不是just访问子类常量CONSTANT,因为该值在基类中未定义。

还有其他方法可以实现此功能,但是我喜欢这种语法,因为对我来说,这似乎对读者来说是最简单和显而易见的。

先前的答案都触及了有用的观点,但是我认为被接受的答案并不能直接回答问题,因为

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

这不是对原始答案的错。自发布以来,对抽象类语法进行了重大更改,在这种情况下,它可以实现更整洁,更实用的实现。


1
这似乎也是唯一令人安抚的正确答案mypy
Anthony

1
感谢您提供实际解决该问题的唯一答案之一。请问该print_constant方法的目的是什么?据我所知,它什么都没做?
布赖恩·约瑟夫

1
在这里,必须实例化Derived类才能调用它。因此,如果您删除@classmethod装饰器
zhukovgreen

1
@BrianJoseph-print_constant那里的方法说明了如何在父类中访问常量,因为该常量仅在子类中定义
James

1
如果您在的定义中添加返回类型声明,Base.CONSTANT则mypy还将检查子类是否使用正确的类型来定义CONSTANT
Florian Brucker

23

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

class Controller:
    path: str

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

这样就可以很明显地看到属性是抽象的代码。如果尚未被覆盖,则尝试访问该属性的代码将引发一个AttributeError


21
c = Controller()仍然有效。控制器类不是抽象的。
isilpekel '19

16

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

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

import abc


class Controller(abc.ABC):
    path: str = NotImplemented

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

1
请注意,这不是实例字段。Scala字段始终是类定义中的实例成员。因为Scala将静态和实例分开。
WeiChing林炜清

11

对于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,
# This approach provides more clarity
class AnotherBaseController(ABC):
    @property
    @abstractmethod
    def path(self) -> str:
    """
    :return: the url path of this controller
    """

与其他答案有何不同:

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

  2. ...比投掷更建议NotImplementedError(...)。如果子类中缺少抽象字段的实现,则会自动提示一个极其冗长的错误。相反,NotImplementedError它本身并不能说明为什么缺少实现。此外,它需要体力劳动才能真正提高它。


1
您的第二点是不正确的。尝试使用未实现的方法实例化抽象类(或子类)时,无论键入...还是引发,NotImplementedError或者根本pass不影响引发的错误。
杰弗隆

当您引发NotImplementedError时,您可以传递字符串消息,以将更详细的信息告知调用方为何引发错误。
DtechNet

最好至少为abstract方法声明一个文档字符串,而不是passelipsis(...)。皮林特告诉我。
sausix

谢谢sausix,这是一个好点。我将其包含在答案中
Sergei Voitovich

6

从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

2
@notnami是的。但是,该问题要求提供类变量,而不是在__init__方法期间设置的实例变量。
jonathan.scholbach

1
哼...是我还是您唯一一个严格解决这个问题的人?:) +1。
keepAlive

5

您的基类可以实现一种__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),这将不起作用。
szeitlin

4

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的类型检查
Allen Wang

3

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


1

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

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


12
请详细说明:abc模块在这种情况下有何帮助?
guettli

1

BastienLéonard的答案提到了抽象基类模块,而Brendan Abel的答案涉及未实现的属性,从而引起错误。为确保未在模块外部实现该类,可以在基本名称前添加下划线,以表示该名称对模块是私有的(即,未导入)。

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

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

1
如果子类不重新定义属性,是否有可能引发一些错误?方法很容易,但是属性呢?
Mario F

_Controller类中省略路径声明会更好吗?如果已经存在(无效)值,则“鸭子输入”将不会生效。否则,在某个时候,我需要path定义该字段,就不会有错误,因为已经有一个值。
deamon

@Mario-是的,Brendan Abel的回答提供了一个很好的方法
Brendan 2010年

1
class AbstractStuff:
    @property
    @abc.abstractmethod
    def some_property(self):
        pass

abc.abstractproperty我认为自3.3版本起已弃用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.