jwo*_*der 18 python abstract-class python-3.x
假设您正在编写一个抽象类,并且其一个或多个非抽象类方法要求具体类具有特定的类属性; 例如,如果每个具体类的实例可以通过匹配不同的正则表达式来构造,那么您可能希望为您的ABC提供以下内容:
@classmethod
def parse(cls, s):
m = re.fullmatch(cls.PATTERN, s)
if not m:
raise ValueError(s)
return cls(**m.groupdict())
Run Code Online (Sandbox Code Playgroud)
(也许这可以通过自定义元类更好地实现,但是为了示例,请尝试忽略它.)
现在,因为在实例创建时检查抽象方法和属性的覆盖,而不是子类创建时间,尝试使用abc.abstractmethod
以确保具体类具有PATTERN
属性将无法工作 - 但肯定应该有一些东西可以告诉任何人查看您的代码"我没有忘记PATTERN
在ABC 上定义;具体的类应该定义自己的." 问题是:哪些东西是最Pythonic?
堆装饰员
@property
@abc.abstractmethod
def PATTERN(self):
pass
Run Code Online (Sandbox Code Playgroud)
(顺便说一下,假设Python 3.4或更高版本.)这可能会误导读者,因为它暗示PATTERN
应该是一个实例属性而不是类属性.
装饰塔
@property
@classmethod
@abc.abstractmethod
def PATTERN(cls):
pass
Run Code Online (Sandbox Code Playgroud)
这可以是非常混乱的读者,因为@property
和@classmethod
通常不能结合; 它们只在这里一起工作(对于给定的"工作"值),因为一旦被覆盖,该方法就会被忽略.
虚拟价值
PATTERN = ''
Run Code Online (Sandbox Code Playgroud)
如果具体类无法定义自己的类PATTERN
,parse
则只接受空输入.此选项不是广泛适用的,因为并非所有用例都具有适当的虚拟值.
引起误差的虚拟值
PATTERN = None
Run Code Online (Sandbox Code Playgroud)
如果具体类无法定义自己的类PATTERN
,parse
将引发错误,程序员得到他们应得的.
没做什么. 基本上是#4的更硬核变体.在ABC的文档字符串中可以有一个注释,但ABC本身不应该有任何PATTERN
属性的方式.
其他???
Set*_*ton 15
Python> = 3.6版本
(向下滚动以查找适用于Python <= 3.5的版本).
如果您有幸只使用Python 3.6并且不必担心向后兼容性,那么您可以使用__init_subclass__
Python 3.6中引入的新方法来更轻松地自定义类创建,而无需使用元类.定义新类时,它被称为创建类对象之前的最后一步.
在我看来,使用它的最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
Run Code Online (Sandbox Code Playgroud)
回溯可能如下所示,并且发生在子类创建时,而不是实例化时.
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!!!
Run Code Online (Sandbox Code Playgroud)
在展示装饰器的实现方式之前,展示如何在没有装饰器的情况下实现它是有益的.这里的好处是,如果需要,你可以使你的基类成为一个抽象的基类,而不必做任何工作(只是继承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'
Run Code Online (Sandbox Code Playgroud)
以下是装饰器的实现方式.
# 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)
Run Code Online (Sandbox Code Playgroud)
Python <= 3.5版本
如果你不幸只使用Python 3.6而不必担心向后兼容性,那么你将不得不使用元类.即使这是完全有效的Python,也可以讨论解决方案是如何pythonic的,因为元类很难包裹你的大脑,但我认为它击中了Python的Zen的大部分内容,所以我认为它并不是那么糟糕.
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
Run Code Online (Sandbox Code Playgroud)
这与__init_subclass__
上面显示的Python> = 3.6 方法完全相同(除了回溯看起来有点不同,因为它在失败之前通过一组不同的方法路由).
与__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
Run Code Online (Sandbox Code Playgroud)
输出正如您所期望的那样.
5
TypeError: Can't instantiate abstract class IllegalPatternChild1 with abstract methods abstract
# Then the NotImplementedError if it kept on going.
Run Code Online (Sandbox Code Playgroud)
我一直在寻找这样的东西很长一段时间,直到昨天我决定深入研究它。我非常喜欢@SethMMorton 的回复,但是缺少两件事:允许一个抽象类有一个本身是抽象的子类,并与 typehints 和静态类型工具(例如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__(...): ...
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
3962 次 |
最近记录: |