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

新手上路,请多包涵

假设您正在编写一个抽象类,并且它的一个或多个非抽象类方法要求具体类具有特定的类属性;例如,如果可以通过匹配不同的正则表达式来构造每个具体类的实例,您可能希望为 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` 应该是实例属性而不是类属性。
  1. 装饰塔
   @property
   @classmethod
   @abc.abstractmethod
   def PATTERN(cls):
       pass

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

  1. 虚拟值
   PATTERN = ''

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

  1. 错误诱导虚拟值
   PATTERN = None

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

  1. 没做什么。 基本上是#4 的更硬核变体。 ABC 的文档字符串中某处可能有注释,但 ABC 本身不应有任何妨碍 PATTERN 属性的方式。

  2. 其他???

原文由 jwodder 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 730
1 个回答

您可以使用 Python 3.6 中引入的 __init_subclass__ 方法来 更轻松地创建自定义类,而无需借助元类。当定义一个新类时,它被称为创建类对象之前的最后一步。

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

原文由 SethMMorton 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
logo
Stack Overflow 翻译
子站问答
访问
宣传栏