大多数Pythonic方式声明一个抽象类属性

jwo*_*der 18 python abstract-class python-3.x

假设您正在编写一个抽象类,并且其一个或多个非抽象类方法要求具体类具有特定的类属性; 例如,如果每个具体类的实例可以通过匹配不同的正则表达式来构造,那么您可能希望为您的ABC提供以下内容:

@classmethod
def parse(cls, s):
    m = re.fullmatch(cls.PATTERN, s)
    if not m:
        raise ValueError(s)
    return cls(**m.groupdict())
Run Code Online (Sandbox Code Playgroud)

(也许这可以通过自定义元类更好地实现,但是为了示例,请尝试忽略它.)

现在,因为在实例创建时检查抽象方法和属性的覆盖,而不是子类创建时间,尝试使用abc.abstractmethod以确保具体类具有PATTERN属性将无法工作 - 但肯定应该有一些东西可以告诉任何人查看您的代码"我没有忘记PATTERN在ABC 上定义;具体的类应该定义自己的." 问题是:哪些东西是最Pythonic?

  1. 堆装饰员

    @property
    @abc.abstractmethod
    def PATTERN(self):
        pass
    
    Run Code Online (Sandbox Code Playgroud)

    (顺便说一下,假设Python 3.4或更高版本.)这可能会误导读者,因为它暗示PATTERN应该是一个实例属性而不是类属性.

  2. 装饰塔

    @property
    @classmethod
    @abc.abstractmethod
    def PATTERN(cls):
        pass
    
    Run Code Online (Sandbox Code Playgroud)

    这可以是非常混乱的读者,因为@property@classmethod通常不能结合; 它们只在这里一起工作(对于给定的"工作"值),因为一旦被覆盖,该方法就会被忽略.

  3. 虚拟价值

    PATTERN = ''
    
    Run Code Online (Sandbox Code Playgroud)

    如果具体类无法定义自己的类PATTERN,parse则只接受空输入.此选项不是广泛适用的,因为并非所有用例都具有适当的虚拟值.

  4. 引起误差的虚拟值

    PATTERN = None
    
    Run Code Online (Sandbox Code Playgroud)

    如果具体类无法定义自己的类PATTERN,parse将引发错误,程序员得到他们应得的.

  5. 没做什么. 基本上是#4的更硬核变体.在ABC的文档字符串中可以有一个注释,但ABC本身不应该有任何PATTERN属性的方式.

  6. 其他???

Set*_*ton 15

Python> = 3.6版本

(向下滚动以查找适用于Python <= 3.5的版本).

如果您有幸只使用Python 3.6并且不必担心向后兼容性,那么您可以使用__init_subclass__Python 3.6中引入的新方法来更轻松地自定义类创建,而无需使用元类.定义新类时,它被称为创建类对象之前的最后一步.

在我看来,使用它的最pythonic方法是创建一个接受属性的类装饰器来制作抽象,从而使用户明确他们需要定义的内容.

from custom_decorators import abstract_class_attributes

@abstract_class_attributes('PATTERN')
class PatternDefiningBase:
    pass

class LegalPatternChild(PatternDefiningBase):
    PATTERN = r'foo\s+bar'

class IllegalPatternChild(PatternDefiningBase):
    pass
Run Code Online (Sandbox Code Playgroud)

回溯可能如下所示,并且发生在子类创建时,而不是实例化时.

NotImplementedError                       Traceback (most recent call last)
...
     18     PATTERN = r'foo\s+bar'
     19 
---> 20 class IllegalPatternChild(PatternDefiningBase):
     21     pass

...

<ipython-input-11-44089d753ec1> in __init_subclass__(cls, **kwargs)
      9         if cls.PATTERN is NotImplemented:
     10             # Choose your favorite exception.
---> 11             raise NotImplementedError('You forgot to define PATTERN!!!')
     12 
     13     @classmethod

NotImplementedError: You forgot to define PATTERN!!!
Run Code Online (Sandbox Code Playgroud)

在展示装饰器的实现方式之前,展示如何在没有装饰器的情况下实现它是有益的.这里的好处是,如果需要,你可以使你的基类成为一个抽象的基类,而不必做任何工作(只是继承abc.ABC或创建元类abc.ABCMeta).

class PatternDefiningBase:
    # Dear programmer: implement this in a subclass OR YOU'LL BE SORRY!
    PATTERN = NotImplemented

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)

        # If the new class did not redefine PATTERN, fail *hard*.
        if cls.PATTERN is NotImplemented:
            # Choose your favorite exception.
            raise NotImplementedError('You forgot to define PATTERN!!!')

    @classmethod
    def sample(cls):
        print(cls.PATTERN)

class LegalPatternChild(PatternDefiningBase):
    PATTERN = r'foo\s+bar'
Run Code Online (Sandbox Code Playgroud)

以下是装饰器的实现方式.

# custom_decorators.py

def abstract_class_attributes(*names):
    """Class decorator to add one or more abstract attribute."""

    def _func(cls, *names):
        """ Function that extends the __init_subclass__ method of a class."""

        # Add each attribute to the class with the value of NotImplemented
        for name in names:
            setattr(cls, name, NotImplemented)

        # Save the original __init_subclass__ implementation, then wrap
        # it with our new implementation.
        orig_init_subclass = cls.__init_subclass__

        def new_init_subclass(cls, **kwargs):
            """
            New definition of __init_subclass__ that checks that
            attributes are implemented.
            """

            # The default implementation of __init_subclass__ takes no
            # positional arguments, but a custom implementation does.
            # If the user has not reimplemented __init_subclass__ then
            # the first signature will fail and we try the second.
            try:
                orig_init_subclass(cls, **kwargs)
            except TypeError:
                orig_init_subclass(**kwargs)

            # Check that each attribute is defined.
            for name in names:
                if getattr(cls, name, NotImplemented) is NotImplemented:
                    raise NotImplementedError(f'You forgot to define {name}!!!')

        # Bind this new function to the __init_subclass__.
        # For reasons beyond the scope here, it we must manually
        # declare it as a classmethod because it is not done automatically
        # as it would be if declared in the standard way.
        cls.__init_subclass__ = classmethod(new_init_subclass)

        return cls

    return lambda cls: _func(cls, *names)
Run Code Online (Sandbox Code Playgroud)

Python <= 3.5版本

如果你不幸只使用Python 3.6而不必担心向后兼容性,那么你将不得不使用元类.即使这是完全有效的Python,也可以讨论解决方案是如何pythonic的,因为元类很难包裹你的大脑,但我认为它击中了Python的Zen的大部分内容,所以我认为它并不是那么糟糕.

class RequirePatternMeta(type):
    """Metaclass that enforces child classes define PATTERN."""

    def __init__(cls, name, bases, attrs):
        # Skip the check if there are no parent classes,
        # which allows base classes to not define PATTERN.
        if not bases:
            return
        if attrs.get('PATTERN', NotImplemented) is NotImplemented:
            # Choose your favorite exception.
            raise NotImplementedError('You forgot to define PATTERN!!!')

class PatternDefiningBase(metaclass=RequirePatternMeta):
    # Dear programmer: implement this in a subclass OR YOU'LL BE SORRY!
    PATTERN = NotImplemented

    @classmethod
    def sample(cls):
        print(cls.PATTERN)

class LegalPatternChild(PatternDefiningBase):
    PATTERN = r'foo\s+bar'

class IllegalPatternChild(PatternDefiningBase):
    pass
Run Code Online (Sandbox Code Playgroud)

这与__init_subclass__上面显示的Python> = 3.6 方法完全相同(除了回溯看起来有点不同,因为它在失败之前通过一组不同的方法路由).

__init_subclass__方法不同,如果要将子类设为抽象基类,则必须执行一些额外的工作(您必须使用组合元类ABCMeta).

from abs import ABCMeta, abstractmethod

ABCRequirePatternMeta = type('ABCRequirePatternMeta', (ABCMeta, RequirePatternMeta), {})

class PatternDefiningBase(metaclass=ABCRequirePatternMeta):
    # Dear programmer: implement this in a subclass OR YOU'LL BE SORRY!
    PATTERN = NotImplemented

    @classmethod
    def sample(cls):
        print(cls.PATTERN)

    @abstractmethod
    def abstract(self):
        return 6

class LegalPatternChild(PatternDefiningBase):
    PATTERN = r'foo\s+bar'

    def abstract(self):
        return 5

class IllegalPatternChild1(PatternDefiningBase):
    PATTERN = r'foo\s+bar'

print(LegalPatternChild().abstract())
print(IllegalPatternChild1().abstract())

class IllegalPatternChild2(PatternDefiningBase):
    pass
Run Code Online (Sandbox Code Playgroud)

输出正如您所期望的那样.

5
TypeError: Can't instantiate abstract class IllegalPatternChild1 with abstract methods abstract
# Then the NotImplementedError if it kept on going.
Run Code Online (Sandbox Code Playgroud)

  • 由于`NotImplemented`是一个单例,通常最好与`is`进行比较 (2认同)

Cla*_*ude 7

我一直在寻找这样的东西很长一段时间,直到昨天我决定深入研究它。我非常喜欢@SethMMorton 的回复,但是缺少两件事:允许一个抽象类有一个本身是抽象的子类,并与 typehints 和静态类型工具(例如mypy)很好地配合(这是有道理的,因为早在 2017 年这些几乎不是一回事)。

我开始打算用我自己的解决方案在这里写一个回复,但是我意识到我需要大量的测试和文档,所以我将它做成了一个合适的 python 模块:abstractcp

使用(从0.9.5版本开始):

class Parser(acp.Abstract):
    PATTERN: str = acp.abstract_class_property(str)

    @classmethod
    def parse(cls, s):
        m = re.fullmatch(cls.PATTERN, s)
        if not m:
            raise ValueError(s)
        return cls(**m.groupdict())

class FooBarParser(Parser):
    PATTERN = r"foo\s+bar"

    def __init__(...): ...

class SpamParser(Parser):
    PATTERN = r"(spam)+eggs"

    def __init__(...): ...
Run Code Online (Sandbox Code Playgroud)

如需完整使用,请参阅pypigithub上的页面。