简体   繁体   English

在子类中强制执行类变量

[英]Enforcing Class Variables in a Subclass

I'm working on extending the Python webapp2 web framework for App Engine to bring in some missing features (in order to make creating apps a little quicker and easier).我正在为 App Engine 扩展 Python webapp2 网络框架,以引入一些缺失的功能(以便更快、更轻松地创建应用程序)。

One of the requirements here is that each subclass needs to have some specific static class variables.这里的要求之一是每个子类都需要有一些特定的静态类变量。 Is the best way to achieve this to simply throw an exception if they are missing when I go to utilise them or is there a better way?如果在我使用它们时缺少它们​​,那么最好的方法是简单地抛出异常还是有更好的方法?

Example (not real code):示例(不是真正的代码):

Subclass:子类:

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

page_name needs to be present in order to be processed here: 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

Python will already throw an exception if you try to use an attribute that doesn't exist.如果您尝试使用不存在的属性,Python 将已经抛出异常。 That's a perfectly reasonable approach, as the error message will make it clear that the attribute needs to be there.这是一种完全合理的方法,因为错误消息将明确指出该属性需要存在。 It is also common practice to provide reasonable defaults for these attributes in the base class, where possible.在可能的情况下,为基类中的这些属性提供合理的默认值也是一种常见的做法。 Abstract base classes are good if you need to require properties or methods, but they don't work with data attributes, and they don't raise an error until the class is instantiated.如果您需要需要属性或方法,抽象基类是很好的,但它们不适用于数据属性,并且在实例化类之前它们不会引发错误。

If you want to fail as quickly as possible, a metaclass can prevent the user from even defining the class without including the attributes.如果您想尽快失败,元类甚至可以阻止用户在不包含属性的情况下定义类。 The nice thing about a metaclass is that it's inheritable, so if you define it on a base class it is automatically used on any class derived on it.元类的好处是它是可继承的,所以如果你在基类上定义它,它会自动用于任何派生在它上面的类。

Here's such a metaclass;这是一个元类; in fact, here's a metaclass factory that lets you easily pass in the attribute names you wish to require.事实上,这里有一个元类工厂,可以让您轻松地传入您希望需要的属性名称。

def RequiredAttributes(*required_attrs):

    class RequiredAttributesMeta(type):
        def __init__(cls, name, bases, attrs):
            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

Now to actually define a base class using this metaclass is a bit tricky.现在使用这个元类实际定义一个基类有点棘手。 You have to define the attributes to define the class, that being the entire point of the metaclass, but if the attributes are defined on the base class they are also defined on any class derived from it, defeating the purpose.您必须定义属性来定义类,即元类的整个点,但如果属性是在基类上定义的,它们也会在从它派生的任何类上定义,这与目的不符。 So what we'll do is define them (using a dummy value) then delete them from the class afterward.所以我们要做的是定义它们(使用一个虚拟值)然后从类中删除它们。

class Base(object):
    __metaclass__ = RequiredAttributes("a", "b" ,"c")
    a = b = c = 0

del Base.a, Base.b, Base.c

Now if you try to define a subclass, but don't define the attributes:现在,如果您尝试定义子类,但不定义属性:

class Child(Base):
    pass

You get:你得到:

AttributeError: class 'Child' requires attributes 'a', 'b', 'c'

NB I don't have any experience with Google App Engine, so it's possible it already uses a metaclass.注意,我对 Google App Engine 没有任何经验,因此它可能已经使用了元类。 In this case, you want your RequiredAttributesMeta to derive from that metaclass, rather than type .在这种情况下,您希望您的RequiredAttributesMeta派生自该元类,而不是type

Before describing my solution, let me introduce you how Python class instances are created:在描述我的解决方案之前,让我向您介绍一下 Python 类实例是如何创建的:

在 Python 中创建实例

Figure 1: Python Instance creation [1]图 1:Python 实例创建[1]

Given the above description, you can see that in Python class instances are actually created by a Metaclass.通过上面的描述,你可以看到 Python 中的类实例实际上是由一个 Metaclass 创建的。 As we can see, when the caller is creating an instance of our class, first the __call__ magic method is called which in turn is calling the __new__ and __init__ of the class and then __cal__ is returning the object instance back to the caller.正如我们所见,当调用者创建我们类的实例时,首先__call__魔术方法,该方法依次调用类的__new____init__ ,然后__cal__将对象实例返回给调用者。

With all that said, we can simply try to do our checking if the instance created by __init__ actually defines those "required" attributes.尽管如此,我们可以简单地尝试检查__init__创建的实例是否确实定义了那些“必需”属性。

Metaclass元类

class ForceRequiredAttributeDefinitionMeta(type):
    def __call__(cls, *args, **kwargs):
        class_object = type.__call__(cls, *args, **kwargs)
        class_object.check_required_attributes()
        return class_object

As you can see in __call__ what we do is to create the class object, and then call its check_required_attributes() method which will check if the required attributes have been defined.正如您在__call__中看到的,我们所做的是创建类对象,然后调用其check_required_attributes()方法,该方法将检查是否已定义所需的属性。 In there if the required attributes are not defined we should simply throw an error.在那里,如果未定义所需的属性,我们应该简单地抛出一个错误。

Superclass超类

Python 2蟒蛇 2

class ForceRequiredAttributeDefinition(object):
    __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.')

Python 3蟒蛇 3

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.')

Here we define the actual superclass.这里我们定义了实际的超类。 Three things:三件事:

  • Should make use of our metaclass.应该利用我们的元类。
  • Should define the required attributes as None see starting_day_of_week = None应将所需的属性定义为None请参阅starting_day_of_week = None
  • Should implement the check_required_attributes method that checks if the required attributes are None and if they are to throw a NotImplementedError with a reasonable error message to the user.应该实现check_required_attributes方法,该方法检查所需的属性是否为None以及它们是否要向用户抛出NotImplementedError和合理的错误消息。

Example of a working and non-working subclass工作和非工作子类的示例

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

Output输出

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.

As you can see, the first instance created successfully since was defining the required attribute, where the second one raised a NotImplementedError straightaway.如您所见,第一个实例成功创建,因为它定义了 required 属性,第二个实例立即引发了NotImplementedError

Abstract Base Classes allow to declare a property abstract, which will force all implementing classes to have the property.抽象基类允许声明属性抽象,这将强制所有实现类具有该属性。 I am only providing this example for completeness, many pythonistas think your proposed solution is more pythonic.我只是为了完整性提供这个例子,许多 pythonistas 认为你提出的解决方案更 pythonic。

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

Trying to instantiate the first implementing class:尝试实例化第一个实现类:

print Implementation1()
Out[6]: <__main__.Implementation1 at 0x105c41d90>

Trying to instantiate the second implementing class:尝试实例化第二个实现类:

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

This works.这有效。 Will prevent subclasses from even being defined, let alone instantiated.甚至会阻止子类被定义,更不用说实例化了。

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'

一般来说,在 Python 中,人们普遍认为,正如您正确建议的那样,处理这种情况的最佳方法是用 try-except 块包装需要此类变量的任何操作。

I love this answer .我喜欢这个答案 Best approach as a once off.最好的方法是一次性的。 Much less scary to other readers than metaclasses.与元类相比,对于其他读者来说,可怕得多。

However, metaclasses are great if you want this as a general util to plug into lots of places.但是,如果您希望将其作为通用工具插入到很多地方,元类非常有用。 I've borrow from some of the other answers, but also added a bases check, so that you can use this in a mixin and the mixin itself won't trigger it.我借鉴了其他一些答案,但还添加了bases检查,以便您可以在 mixin 中使用它,并且 mixin 本身不会触发它。 Can add a similar check to skip over ABCs.可以添加类似的检查来跳过 ABC。

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

And then use like so:然后像这样使用:

class LicenseAccessMixin(metaclass=RequiredAttributes('access_control')):
    ...  # define methods that safely refer to `self.access_control`.

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM