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

时间:2017-07-21 23:47:56

标签: python python-3.x abstract-class

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

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

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

现在,因为重写抽象方法&在实例创建时检查属性,而不是子类创建时间,尝试使用abc.abstractmethod来确保具有PATTERN属性的具体类不起作用 - 但肯定应该有某些东西告诉任何人看你的代码“我没有忘记在ABC上定义PATTERN;具体的类应该定义自己的代码。”问题是:哪个某些东西是最Pythonic?

  1. 一堆装饰者

    @property
    @abc.abstractmethod
    def PATTERN(self):
        pass
    

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

  2. 装饰塔

    @property
    @classmethod
    @abc.abstractmethod
    def PATTERN(cls):
        pass
    

    这对读者来说非常困惑,因为@property@classmethod通常无法合并;他们只在这里一起工作(对于给定的“工作”值),因为一旦被覆盖,该方法就会被忽略。

  3. 虚拟价值

    PATTERN = ''
    

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

  4. 错误诱导虚拟值

    PATTERN = None
    

    如果具体类无法定义自己的PATTERNparse将引发错误,并且程序员得到他们应得的。

  5. 什么都不做。基本上是#4的硬核变体。 ABC的文档字符串中可能有一个注释,但ABC本身不应该有PATTERN属性的任何内容。

  6. 其他???

2 个答案:

答案 0 :(得分:9)

Python> = 3.6版本

(向下滚动查看适用于Python的版本< = 3.5)。

如果您有幸只使用Python 3.6而不必担心向后兼容性,则可以使用Python 3.6中引入的新__init_subclass__方法make customizing class creation easier without resorting to metaclasses。定义新类时,它被称为创建类对象之前的最后一步。

在我看来,使用它的最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

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

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!!!

在展示装饰器的实现方式之前,展示如何在没有装饰器的情况下实现它是有益的。这里的好处是,如果需要,你可以使你的基类成为一个抽象的基类而不必做任何工作(只需从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'

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

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

Python&lt; = 3.5版本

如果您不幸仅仅使用Python 3.6而不必担心向后兼容性,则必须使用元类。虽然这是完全有效的Python,但人们可以讨论解决方案是如何 pythonic 因为元类很难包裹你的大脑,但我认为它会触及The Zen of Python的大多数点,所以我认为这不是那么糟糕。

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

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

__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

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

5
TypeError: Can't instantiate abstract class IllegalPatternChild1 with abstract methods abstract
# Then the NotImplementedError if it kept on going.

答案 1 :(得分:3)

我一直在寻找类似的东西,直到昨天我决定深入研究它。我非常喜欢@SethMMorton's reply,但是缺少两件事:允许抽象类具有一个本身是抽象的子类,并与类型提示和诸如mypy之类的静态类型输入工具一起很好地使用(这很有意义,自2017年以来,这些都不是问题。

我开始着手用自己的解决方案在此处写一个回复,但是我意识到我需要大量的测试和文档,因此我将其设置为合适的python模块:abstractcp

使用(从0.9.5版开始):

console.log("responseGETHtml2", responseGETHtml2.html());

请充分使用page on pypigithub