声明抽象类属性的最 Pythonic 方式 [英] Most Pythonic way to declare an abstract class property

查看:38
本文介绍了声明抽象类属性的最 Pythonic 方式的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Assume you're writing an abstract class and one or more of its non-abstract class methods require the concrete class to have a specific class attribute; e.g., if instances of each concrete class can be constructed by matching against a different regular expression, you might want to give your ABC the following:

@classmethod
def parse(cls, s):
    m = re.fullmatch(cls.PATTERN, s)
    if not m:
        raise ValueError(s)
    return cls(**m.groupdict())

(也许使用自定义元类可以更好地实现这一点,但为了示例,请尝试忽略它.)

(Maybe this could be better implemented with a custom metaclass, but try to ignore that for the sake of the example.)

现在,因为覆盖抽象方法 &在实例创建时检查属性,而不是子类创建时,尝试使用 abc.abstractmethod 来确保具有 PATTERN 属性的具体类不起作用 - 但肯定应该有某事告诉任何正在查看您的代码的人我没有忘记在 ABC 上定义 PATTERN;具体的类应该定义它们自己的."问题是:哪个东西是最 Pythonic 的?

Now, because overriding of abstract methods & properties is checked at instance creation time, not subclass creation time, trying to use abc.abstractmethod to ensure concrete classes have PATTERN attributes won't work — but surely there should be something there to tell anyone looking at your code "I didn't forget to define PATTERN on the ABC; the concrete classes are supposed to define their own." The question is: Which something is the most Pythonic?

  1. 一堆装饰器

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

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

(Assume Python 3.4 or higher, by the way.) This can be very misleading to readers, as it implies that PATTERN should be an instance property instead of a class attribute.

装饰之塔

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

这会让读者很困惑,因为@property@classmethod 通常不能组合在一起;它们仅在此处协同工作(对于给定的work"值),因为该方法一旦被覆盖就会被忽略.

This can be very confusing to readers, as @property and @classmethod normally can't be combined; they only work together here (for a given value of "work") because the method is ignored once it's overridden.

虚拟值

PATTERN = ''

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

If a concrete class fails to define its own PATTERN, parse will only accept empty input. This option isn't widely applicable, as not all use cases will have an appropriate dummy value.

错误诱导虚拟值

PATTERN = None

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

If a concrete class fails to define its own PATTERN, parse will raise an error, and the programmer gets what they deserve.

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

Do nothing. Basically a more hardcore variant of #4. There can be a note in the ABC's docstring somewhere, but the ABC itself shouldn't have anything in the way of a PATTERN attribute.

其他???

推荐答案

Python >= 3.6 版本

(向下滚动以获得适用于 Python <= 3.5 的版本).

(Scroll down for a version that works for Python <= 3.5).

如果您有幸只使用 Python 3.6 而不必担心向后兼容性,您可以使用新的 __init_subclass__ 方法,该方法在 Python 3.6 中引入 无需借助元类即可更轻松地创建自定义类.定义新类时,在类对象创建前的最后一步调用.

If you are fortunate enough to only be using Python 3.6 and not have to worry about backwards compatibility, you can use the new __init_subclass__ method which was introduced in Python 3.6 to make customizing class creation easier without resorting to metaclasses. When defining a new class, it is called as the last step before the class object is created.

在我看来,使用它的最 Pythonic 的方法是制作一个类装饰器,该装饰器接受要抽象的属性,从而使用户明确知道他们需要定义什么.

In my opinion, the most pythonic way to use this would be to make a class decorator that accepts the attributes to make abstract, thus making it explicit to the user what they need to define.

from custom_decorators import abstract_class_attributes

@abstract_class_attributes('PATTERN')
class PatternDefiningBase:
    pass

class LegalPatternChild(PatternDefiningBase):
    PATTERN = r'foos+bar'

class IllegalPatternChild(PatternDefiningBase):
    pass

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

The traceback might be as follows, and occurs at subclass creation time, not instantiation time.

NotImplementedError                       Traceback (most recent call last)
...
     18     PATTERN = r'foos+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).

Before showing how the decorator is implemented, it is instructive to show how you could implement this without the decorator. The nice thing here is that if needed you could make your base class an abstract base class without having to do any work (just inherit from abc.ABC or make the metaclass 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'foos+bar'

这是装饰器的实现方式.

Here is how the decorator could be implemented.

# 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 版本

如果你不够幸运只使用 Python 3.6 并且不必担心向后兼容性,你将不得不使用元类.尽管这是完全有效的 Python,但人们可能会争论解决方案是如何pythonic 因为元类很难让你的大脑围绕它,但我认为它达到了 Python 之禅 所以我觉得还不错.

If you are not fortunate enough to only be using Python 3.6 and not have to worry about backwards compatibility, you will have to use a metaclass. Even though this is perfectly valid Python, one could debate how pythonic the solution is because metaclasses are hard to wrap your brain around, but I think it hits most of the points of The Zen of Python so I think it's not so bad.

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'foos+bar'

class IllegalPatternChild(PatternDefiningBase):
    pass

这与上面显示的 Python >= 3.6 __init_subclass__ 方法完全一样(除了回溯看起来有点不同,因为它在失败之前通过不同的方法集路由).

This behaves exactly like the Python >= 3.6 __init_subclass__ method shown above (except the traceback will look bit different because it is routed through a different set of methods before failing).

__init_subclass__ 方法不同,如果你想让子类成为抽象基类,你只需要做一些额外的工作(你必须用 组合元类ABCMeta).

Unlike the __init_subclass__ method, if you want to make a subclass an abstract base class you will have to do just a bit of extra work (you'll have to compose the metaclass with 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'foos+bar'

    def abstract(self):
        return 5

class IllegalPatternChild1(PatternDefiningBase):
    PATTERN = r'foos+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.

这篇关于声明抽象类属性的最 Pythonic 方式的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆