【问题标题】:Most Pythonic way to declare an abstract class property声明抽象类属性的大多数 Pythonic 方式
【发布时间】:2017-07-21 23:47:56
【问题描述】:

假设您正在编写一个抽象类,并且它的一个或多个非抽象类方法要求具体类具有特定的类属性;例如,如果每个具体类的实例可以通过匹配不同的正则表达式来构建,您可能希望为您的 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 属性是行不通的——但肯定应该有 something 告诉任何查看您的代码的人“我没有忘记在 ABC 上定义 PATTERN;具体的类应该定义它们自己的。”问题是:something 是最 Pythonic 的?

  1. 装饰器堆

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

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

  2. 装饰塔

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

    这可能会让读者非常困惑,因为@property@classmethod 通常不能组合;它们只在这里一起工作(对于给定的“work”值),因为该方法一旦被覆盖就会被忽略。

  3. 虚拟值

    PATTERN = ''
    

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

  4. 引发错误的虚拟值

    PATTERN = None
    

    如果具体类未能定义自己的PATTERNparse 将引发错误,程序员得到应有的回报。

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

  6. 其他???

【问题讨论】:

  • pythonic 是主观的吗?

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


【解决方案1】:

Python >= 3.6 版本

(向下滚动查看适用于 Python

如果您有幸只使用 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

如果您不够幸运,只能使用 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 >= 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.

【讨论】:

  • 我已经读过,这是元类的优点之一 - 强化 lookfeel使用它的对象。
  • @wwii 是的,它们也可以用于此目的,但在这种情况下,这不仅仅是外观和感觉。与抽象基类一样,它强制在定义时定义特定的类属性,而不仅仅是外观或感觉。
  • .. 这就是我所说的外观和感觉——我应该更准确。谢谢
  • 由于NotImplemented 是一个单例,通常更好地与is 进行比较
【解决方案2】:

我一直在寻找类似的东西,直到昨天我才决定深入研究它。我非常喜欢@SethMMorton's reply,但是缺少两件事:允许一个抽象类有一个本身是抽象的子类,并且可以很好地使用类型提示和静态类型工具,例如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__(...): ...

请参阅page on pypigithub 以充分使用。

【讨论】:

    猜你喜欢
    • 2016-03-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-28
    • 2016-05-10
    • 2011-01-23
    • 2015-02-28
    • 2011-01-13
    相关资源
    最近更新 更多