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
Before describing my solution, let me introduce you how Python class instances are created:
Figure 1: Python Instance creation [1]
Given the above description, you can see that in Python class instances are actually created by a 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.With all that said, we can simply try to do our checking if the instance created by
__init__
actually defines those "required" attributes.Metaclass
As you can see in
__call__
what we do is to create the class object, and then call itscheck_required_attributes()
method which will check if the required attributes have been defined. In there if the required attributes are not defined we should simply throw an error.Superclass
Python 2
Python 3
Here we define the actual superclass. Three things:
None
seestarting_day_of_week = None
check_required_attributes
method that checks if the required attributes areNone
and if they are to throw aNotImplementedError
with a reasonable error message to the user.Example of a working and non-working subclass
Output
As you can see, the first instance created successfully since was defining the required attribute, where the second one raised a
NotImplementedError
straightaway.This works. Will prevent subclasses from even being defined, let alone instantiated.
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.
Trying to instantiate the first implementing class:
Trying to instantiate the second implementing class:
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. 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.
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.
Now if you try to define a subclass, but don't define the attributes:
You get:
N.B. 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 thantype
.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.