关于python:大多数Pythonic方式声明一个抽象类属性

Most Pythonic way to declare an abstract class property

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

1
2
3
4
5
6
@classmethod
def parse(cls, s):
    m = re.fullmatch(cls.PATTERN, s)
    if not m:
        raise ValueError(s)
    return cls(**m.groupdict())

(也许这可以用自定义元类更好地实现,但是为了示例的缘故,请尝试忽略这一点。)

现在,因为抽象方法和属性的重写是在实例创建时检查的,而不是在子类创建时检查的,所以尝试使用abc.abstractmethod来确保具体类具有PATTERN属性是行不通的,但肯定有一些东西可以告诉任何查看您的代码的人"我没有忘记在上面定义PATTERN"具体的类应该定义它们自己的类。"问题是:哪种东西最像Python?

  • 一堆装饰工

    1
    2
    3
    4
    @property
    @abc.abstractmethod
    def PATTERN(self):
        pass

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

  • 装饰塔

    1
    2
    3
    4
    5
    @property
    @classmethod
    @abc.abstractmethod
    def PATTERN(cls):
        pass

    这对读者来说可能非常混乱,因为@property@classmethod通常不能组合在一起;它们只能在这里一起工作(对于给定的值"work"),因为一旦方法被重写,它就会被忽略。

  • 虚拟值

    1
    PATTERN = ''

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

  • 错误诱导假值

    1
    PATTERN = None

    如果一个具体的类不能定义它自己的PATTERNparse将产生一个错误,程序员将得到他们应得的。

  • 什么都不做。基本上是4的一个更硬核的变体。在ABC的docstring中的某个地方可以有一个注释,但是ABC本身不应该有任何与PATTERN属性不同的地方。

  • 其他????


  • python>=3.6版本

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

    如果您幸运地只使用了python 3.6而不必担心向后兼容性,那么您可以使用python 3.6中引入的新__init_subclass__方法来简化自定义类的创建,而无需使用元类。定义新类时,它被调用为创建类对象之前的最后一步。

    在我看来,使用它的最卑鄙的方法是创建一个类修饰器,它接受属性使其成为抽象的,从而向用户明确地说明他们需要定义什么。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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!!!

    在演示如何实现decorator之前,演示如何在不使用decorator的情况下实现它是很有指导意义的。这里的好处是,如果需要,您可以使您的基类成为一个抽象的基类,而不必做任何工作(只需从abc.ABC继承或使元类abc.ABCMeta)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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'

    下面是如何实现这个修饰器。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    # 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<=3.5版本

    如果您不够幸运,只使用Python3.6,不必担心向后兼容性,那么必须使用元类。尽管这是一个完全有效的python,但是有人可能会争论这个解决方案是怎样的python,因为元类很难将你的大脑包围起来,但是我认为它触及了python禅宗的大部分要点,所以我认为它没有那么糟糕。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    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组成元类)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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

    输出正如您所期望的。

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