A_Porcupine
A_Porcupine

Reputation: 1038

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

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_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

Upvotes: 31

Views: 17732

Answers (6)

Rafael
Rafael

Reputation: 7242

In Python, under the hood, an object is created by a metaclass. That metaclass has a __call__ method which is triggered when an instance of the class is created. This method is calling the __new__ and __init__ of your class and then finally returns the object instance back to the caller. See The figure below for a visualisation of this process (figure reference [1]).

Instance creation in Python

With all that said, we can simply check for "required" attributes in the __call__.

Metaclass

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

We hijack the __call__, we create the class object, and then call its check_required_attributes() method which will check if the required attributes have been defined. If they are not defined we simply throw an exception.

Superclass

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

Three things:

  • Should make use of our metaclass.
  • Should define the required attributes as None see 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.

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.

The first instance is created successfully, since it defines the required attribute, whereas the second one raised a NotImplementedError error.

Upvotes: 16

kindall
kindall

Reputation: 184280

Python will already throw an exception if you try to use an attribute that doesn't exist. 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. Metaclasses are inheritable, so if you define a metaclass on a base class it is automatically used on any class derived from 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 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

Now we can define a base class:

class Base(metaclass=build_required_attributes_metaclass("a", "b" ,"c")):
    pass

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'

I don't have any experience with Google App Engine, so it's possible it already uses a metaclass. In this case, you want your RequiredAttributesMeta to derive from that metaclass, rather than type.

Upvotes: 13

jarekwg
jarekwg

Reputation: 385

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. Can add a similar check to skip over 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

And then use like so:

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

Upvotes: 1

eric.frederich
eric.frederich

Reputation: 1668

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'

Upvotes: 14

rdodev
rdodev

Reputation: 3192

Generally speaking, in Python it's widely accepted that the best way to deal with this sort of scenario, as you correctly suggested, is to wrap whatever operation needs this class variable with a try-except block.

Upvotes: 1

mbatchkarov
mbatchkarov

Reputation: 16069

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.

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

Upvotes: 11

Related Questions