Reputation: 1038
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
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]).
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:
None
see starting_day_of_week = None
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
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
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
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
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
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