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()) |
(也许这可以用自定义元类更好地实现,但是为了示例的缘故,请尝试忽略这一点。)
现在,因为抽象方法和属性的重写是在实例创建时检查的,而不是在子类创建时检查的,所以尝试使用
一堆装饰工
1 2 3 4 | @property @abc.abstractmethod def PATTERN(self): pass |
号
(顺便说一句,假设使用python 3.4或更高版本。)这可能会误导读者,因为它意味着
装饰塔
1 2 3 4 5 | @property @classmethod @abc.abstractmethod def PATTERN(cls): pass |
这对读者来说可能非常混乱,因为
虚拟值
1 | PATTERN = '' |
。
如果一个具体类不能定义它自己的
错误诱导假值
1 | PATTERN = None |
如果一个具体的类不能定义它自己的
什么都不做。基本上是4的一个更硬核的变体。在ABC的docstring中的某个地方可以有一个注释,但是ABC本身不应该有任何与
其他????
python>=3.6版本
(向下滚动查看适用于python的版本<=3.5)。
如果您幸运地只使用了python 3.6而不必担心向后兼容性,那么您可以使用python 3.6中引入的新
在我看来,使用它的最卑鄙的方法是创建一个类修饰器,它接受属性使其成为抽象的,从而向用户明确地说明他们需要定义什么。
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的情况下实现它是很有指导意义的。这里的好处是,如果需要,您可以使您的基类成为一个抽象的基类,而不必做任何工作(只需从
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
与
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. |
。