声明抽象类属性的大多数Python方法 [英] Most Pythonic way to declare an abstract class property

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

问题描述

假设您正在编写一个抽象类,并且其一个或多个非抽象类方法要求具体类具有特定的类属性;例如,如果可以通过与不同的正则表达式进行匹配来构造每个具体类的实例,则可能需要为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属性不会起作用-但可以肯定的是,这里应该有 something 来告诉任何人查看您的代码我没有忘记在ABC上定义PATTERN;具体的类应该定义自己的类."问题是:哪个东西最适合Python?

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通常不能结合使用.它们仅在此处一起工作(对于给定的工作"值),因为一旦重写该方法,该方法就会被忽略.

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 = ''

如果具体类未能定义其自己的PATTERN,则parse将仅接受空输入.此选项并不广泛适用,因为并非所有用例都将具有适当的虚拟值.

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

如果具体类未能定义自己的PATTERN,则parse将引发错误,程序员将得到应有的报酬.

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,而不必担心向后兼容性,则可以使用新的使自定义类的创建更加容易,而无需使用元类.定义新类时,它被称为创建类对象之前的最后一步.

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.

我认为,使用此功能最有效的方法是制作一个类装饰器,该类装饰器接受要创建抽象的属性,从而使用户可以清楚地知道他们需要定义的内容.

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'foo\s+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'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).

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'foo\s+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 的解决方案是如何解决的,因为元类很难缠住您的大脑,但我认为它符合

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'foo\s+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'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

输出与您期望的一样.

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

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

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