在子类中强制实现类变量

29

我正在扩展Python的webapp2网络框架,使其具备一些缺失的功能(以便快速、简便地创建应用程序)。

其中一个要求是每个子类需要有一些特定的静态类变量。最好的实现方法是,在使用这些变量时,如果它们缺失,就抛出异常,还是有更好的方法?

示例(非真实代码):

子类:

class Bar(Foo):
  page_name = 'New Page'

为了在此处进行处理,page_name 必须存在:

page_names = process_pages(list_of_pages)

def process_pages(list_of_pages)
  page_names = []

  for page in list_of_pages:
    page_names.append(page.page_name)

  return page_names

2
这更多是一个Python问题,而不是一个App Engine问题。 - rdodev
你能提供一个SSCCE/MVCE来支持你的问题吗? - zmo
3
顺便提一下,def process_pages(list_of_pages): return [ page.page_name for page in list_of_pages ]的意思是将给定页面列表中每个页面的名称提取出来并返回一个名称列表。 - chepner
6个回答

13
在Python中,底层是通过元类来创建对象的。这个元类有一个`__call__`方法,在创建类的实例时被触发。这个方法会调用你的类的`__new__`和`__init__`,然后最终将对象实例返回给调用者。请参考下面的图示以了解这个过程(图示参考[1])。

Instance creation in Python

说了这么多,我们只需在__call__中检查"required"属性即可。 元类
class ForceRequiredAttributeDefinitionMeta(type):
    def __call__(cls, *args, **kwargs):
        class_object = type.__call__(cls, *args, **kwargs)
        class_object.check_required_attributes()
        return class_object

我们在这里使用了__call__,我们创建了类对象,然后调用它的check_required_attributes()方法来检查是否定义了必需的属性。如果没有定义,我们就简单地抛出一个异常。 超类
class ForceRequiredAttributeDefinition(metaclass=ForceRequiredAttributeDefinitionMeta):
    starting_day_of_week = None

    def check_required_attributes(self):
        if self.starting_day_of_week is None:
            raise NotImplementedError('Subclass must define self.starting_day_of_week attribute. \n This attribute should define the first day of the week.')

三件事:

  • 应该利用我们的元类。
  • 应该将所需属性定义为None,参见starting_day_of_week = None
  • 应该实现check_required_attributes方法,检查所需属性是否为None,如果是,则抛出一个带有合理错误消息的NotImplementedError给用户。

工作和不工作子类的示例

class ConcereteValidExample(ForceRequiredAttributeDefinition):
    def __init__(self):
        self.starting_day_of_week = "Monday"


class ConcereteInvalidExample(ForceRequiredAttributeDefinition):
    def __init__(self):
        # This will throw an error because self.starting_day_of_week is not defined.
        pass

输出

Traceback (most recent call last):
  File "test.py", line 50, in <module>
    ConcereteInvalidExample()  # This will throw an NotImplementedError straightaway
  File "test.py", line 18, in __call__
    obj.check_required_attributes()
  File "test.py", line 36, in check_required_attributes
    raise NotImplementedError('Subclass must define self.starting_day_of_week attribute. \n This attribute should define the first day of the week.')
NotImplementedError: Subclass must define self.starting_day_of_week attribute.
 This attribute should define the first day of the week.

第一个实例成功创建,因为它定义了所需的属性,而第二个实例引发了一个NotImplementedError错误。

13
Python如果尝试使用不存在的属性,会立即抛出异常。这是一个非常合理的做法,因为错误信息会明确指出需要存在该属性。在基类中为这些属性提供合理的默认值也是常见的做法。抽象基类适用于需要要求属性或方法的情况,但它们无法处理数据属性,并且直到实例化类之后才会引发错误。
如果您希望尽快失败,元类可以阻止用户在定义类时不包含属性。元类是可继承的,因此如果在基类上定义了元类,则任何从它派生的类都会自动使用该元类。
下面是一个这样的元类;事实上,这是一个元类工厂,可以轻松地传入您希望要求的属性名称。
def build_required_attributes_metaclass(*required_attrs):

    class RequiredAttributesMeta(type):
        def __init__(cls, name, bases, attrs):
            if cls.mro() == [cls, object]:
                return   # don't require attrs on our base class
            missing_attrs = ["'%s'" % attr for attr in required_attrs 
                             if not hasattr(cls, attr)]
            if missing_attrs:
                raise AttributeError("class '%s' requires attribute%s %s" %
                                     (name, "s" * (len(missing_attrs) > 1), 
                                      ", ".join(missing_attrs)))
    return RequiredAttributesMeta

现在我们可以定义一个基类:
class Base(metaclass=build_required_attributes_metaclass("a", "b" ,"c")):
    pass

现在如果你尝试定义一个子类,但没有定义属性:
class Child(Base):
    pass

你得到:
AttributeError: class 'Child' requires attributes 'a', 'b', 'c'

我对Google App Engine没有任何经验,所以它可能已经使用了元类。在这种情况下,你希望你的RequiredAttributesMeta从那个元类派生,而不是type

13

这个方法有效。它可以防止子类被定义,更不用说实例化了。

class Foo:

    page_name = None
    author = None

    def __init_subclass__(cls, **kwargs):
        for required in ('page_name', 'author',):
            if not getattr(cls, required):
                raise TypeError(f"Can't instantiate abstract class {cls.__name__} without {required} attribute defined")
        return super().__init_subclass__(**kwargs)


class Bar(Foo):
    page_name = 'New Page'
    author = 'eric'

除非您为getattr提供默认值,否则永远不会遇到TypeError,因为不存在的属性将引发AttributeError,即if not getattr(cls, required, None) - Jürgen Gmach

10

抽象基类允许声明一个属性为抽象的,这将强制所有实现该属性的子类都必须拥有该属性。我只提供这个例子是为了完整性,许多 Python 爱好者认为您提出的解决方案更符合 Python 的风格。

import abc

class Base(object):
    __metaclass__ = abc.ABCMeta

    @abc.abstractproperty
    def value(self):
        return 'Should never get here'


class Implementation1(Base):

    @property
    def value(self):
        return 'concrete property'


class Implementation2(Base):
    pass # doesn't have the required property

尝试实例化第一个实现类:
print Implementation1()
Out[6]: <__main__.Implementation1 at 0x105c41d90>

尝试实例化第二个实现类:

print Implementation2()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-4-bbaeae6b17a6> in <module>()
----> 1 Implementation2()

TypeError: Can't instantiate abstract class Implementation2 with abstract methods value

1
似乎是一个过度的杀伤力,特别是因为你仍然需要在使用它的任何内容周围添加try-except块。最好遵循Python的常规惯例,"寻求原谅而非事先获得许可",至少在我看来是最好的选择。ABC还有其他出色的用途,但我认为对于这种情况来说有点过头了。 - rdodev
虽然我同意这可能有些过度,但这是一种替代方法,并且还提到其他人可能认为我提到的解决方案更符合Pythonic。谢谢! - A_Porcupine
3
"'abc.abstractproperty'自Python 3.3起已被弃用。现在应使用带有'abc.abstractmethod'的'property'。因此,现在可以使用@property @abstractmethod装饰器。" - pdaawr

1
通常来说,在Python中,广泛认为处理这种情况的最佳方式是像你正确建议的那样,用try-except块包装需要使用该类变量的任何操作。

1
我喜欢这个答案。 一次性的最佳方法。比元类对其他读者来说不那么可怕。
然而,如果您想要将其作为通用工具插入许多地方,元类非常好。我借鉴了其他答案的一些内容,但还添加了一个bases检查,以便您可以在mixin中使用它,而mixin本身不会触发它。可以添加类似的检查以跳过ABCs。
def RequiredAttributes(*required_attrs):
    class RequiredAttributesMeta(type):
        def __init__(cls, name, bases, attrs):
            if not bases:
                return  # No bases implies mixin. Mixins aren't the final class, so they're exempt.
            if missing_attrs := [attr for attr in required_attrs if not hasattr(cls, attr)]:
                raise AttributeError(f"{name!r} requires attributes: {missing_attrs}")
    return RequiredAttributesMeta

然后就像这样使用:
class LicenseAccessMixin(metaclass=RequiredAttributes('access_control')):
    ...  # define methods that safely refer to `self.access_control`.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接