Enforcing Function Implementation in Subclasses
This is going to get very weird, very quickly. When you create a class in Python, it looks about like the following:
class MyClass:
pass
Now, let’s say I create some really cool class, with a set of cool functions, but I expect my users to implement some of the functions:
from abc import abstractmethod
class BaseClass:
@abstractmethod
def foo(self,):
raise NotImplementedError
So the intention is, when my user inherits the above class, they do the following:
class UserClass(BaseClass):
def foo(self, *args, **kwargs):
# actual functionality
pass
That’s all well and good, but what happens if my user forgets to implement foo
? The above ran just fine, and even instantiation works!
class BaseClass:
@abstractmethod
def foo(self,):
raise NotImplementedError
class UserClass(BaseClass):
pass
user_instance = UserClass()
Now, this is a problem. Suppose this class were deployed to some production system, which attempts to call foo
…
user_instance.foo()
---------------------------------------------------------------------------
NotImplementedError Traceback (most recent call last)
/storage/projects/notes/metaprogramming/metaclasses.ipynb Cell 72 in <cell line: 1>()
----> <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/metaclasses.ipynb#Y141sZmlsZQ%3D%3D?line=0'>1</a> user_instance.foo()
/storage/projects/notes/metaprogramming/metaclasses.ipynb Cell 72 in BaseClass.foo(self)
<a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/metaclasses.ipynb#Y141sZmlsZQ%3D%3D?line=1'>2</a> @abstractmethod
<a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/metaclasses.ipynb#Y141sZmlsZQ%3D%3D?line=2'>3</a> def foo(self,):
----> <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/metaclasses.ipynb#Y141sZmlsZQ%3D%3D?line=3'>4</a> raise NotImplementedError
NotImplementedError:
That’s a problem! Any code that will fail should fail at compile time, NOT only after it’s deployed. So how do you ensure that, given you write a class, users of your class actually implement the function?
PEP 487#
Enter PEP 487: this PEP proposed a hook (Python’s runtime is quite rich, an a hook is a concrete method in an abstract class that can be overridden by subclasses) for easing the customization of class creation:
from dis import dis
class Base:
def __init_subclass__(cls, **kwargs):
print('__init_subclass__ run', cls)
super().__init_subclass__(**kwargs)
class MyClass(Base):
def __init__(self, ):
return
__init_subclass__ run <class '__main__.MyClass'>
From the above, we can see the __init_subclass__
is run at time of class creation. This is going to be useful to check for whether or not a user overrides my abstract function.
So let’s try this again, in the __init_subclass__
, we check whether or not the method foo
is still abstract or not. In this case, methods decorated with @abstractmethod
have an attribute __isabstractmethod__
which can be pulled:
class BaseClass: # this is the class I would write
def __init_subclass__(cls, **kwargs):
# if attribute foo of the class cls is still abstract, raise an error
if getattr(cls().foo, '__isabstractmethod__', False):
raise NotImplementedError('Function foo must be implemented')
super().__init_subclass__(**kwargs)
@abstractmethod
def foo(self, ):
raise NotImplementedError
Now if the above was set up correctly, any classes inheriting from BaseClass
should fail to be created at all at time of class creation, NOT instance creation!
class MyGoodUserClass(BaseClass):
def foo(self, x):
return x**2
user_instance = MyGoodUserClass()
user_instance.foo(x=3)
9
The above works fine, the method foo
was successfully overridden and implemented; but the best-case scenario is fairly uninteresting. What happens when a user forgets to implement/override foo
?
class MyBadUserClass(BaseClass):
pass
---------------------------------------------------------------------------
NotImplementedError Traceback (most recent call last)
/storage/projects/notes/metaprogramming/metaclasses.ipynb Cell 80 in <cell line: 1>()
----> <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/metaclasses.ipynb#Y154sZmlsZQ%3D%3D?line=0'>1</a> class MyBadUserClass(BaseClass):
<a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/metaclasses.ipynb#Y154sZmlsZQ%3D%3D?line=1'>2</a> pass
/storage/projects/notes/metaprogramming/metaclasses.ipynb Cell 80 in BaseClass.__init_subclass__(cls, **kwargs)
<a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/metaclasses.ipynb#Y154sZmlsZQ%3D%3D?line=1'>2</a> def __init_subclass__(cls, **kwargs):
<a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/metaclasses.ipynb#Y154sZmlsZQ%3D%3D?line=2'>3</a> # if attribute foo of the class cls is still abstract, raise an error
<a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/metaclasses.ipynb#Y154sZmlsZQ%3D%3D?line=3'>4</a> if getattr(cls().foo, '__isabstractmethod__', False):
----> <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/metaclasses.ipynb#Y154sZmlsZQ%3D%3D?line=4'>5</a> raise NotImplementedError('Function foo must be implemented')
<a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/metaclasses.ipynb#Y154sZmlsZQ%3D%3D?line=6'>7</a> super().__init_subclass__(**kwargs)
NotImplementedError: Function foo must be implemented
That’s right, class creation fails up-front, exactly where it’s supposed to fail!
An Actual Example#
Okay that was quite meta (pun intended), let’s see an example; Let’s say, I have a parent class that does data transformations, but I expect the user to implement their own cost function, so the function should take two inputs and return the similarity between them:
import math
from abc import abstractmethod
class TransformData:
def __init_subclass__(cls, **kwargs):
if getattr(cls().cost , '__isabstractmethod__', False):
raise NotImplementedError('Implement cost function!')
super().__init_subclass__(**kwargs)
# assume some useful functions here
def exponent(self, x):
return math.exp(x)
def factorial(self, x):
return math.factorial(x)
@abstractmethod
def cost(self, a, b):
raise NotImplementedError
Now, my user, by means of subclassing TransformData
, must implement their own cost function. If they don’t:
class UserTransforms(TransformData):
pass
---------------------------------------------------------------------------
NotImplementedError Traceback (most recent call last)
/storage/projects/notes/metaprogramming/metaclasses.ipynb Cell 85 in <cell line: 1>()
----> <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/metaclasses.ipynb#Y161sZmlsZQ%3D%3D?line=0'>1</a> class UserTransforms(TransformData):
<a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/metaclasses.ipynb#Y161sZmlsZQ%3D%3D?line=1'>2</a> pass
/storage/projects/notes/metaprogramming/metaclasses.ipynb Cell 85 in TransformData.__init_subclass__(cls, **kwargs)
<a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/metaclasses.ipynb#Y161sZmlsZQ%3D%3D?line=4'>5</a> def __init_subclass__(cls, **kwargs):
<a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/metaclasses.ipynb#Y161sZmlsZQ%3D%3D?line=5'>6</a> if getattr(cls().cost , '__isabstractmethod__', False):
----> <a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/metaclasses.ipynb#Y161sZmlsZQ%3D%3D?line=6'>7</a> raise NotImplementedError('Implement cost function!')
<a href='vscode-notebook-cell:/storage/projects/notes/metaprogramming/metaclasses.ipynb#Y161sZmlsZQ%3D%3D?line=8'>9</a> super().__init_subclass__(**kwargs)
NotImplementedError: Implement cost function!
And if they do:
class UserTransforms(TransformData):
def cost(self, a, b):
return a - b
It goes without saying, this is for sake of example, and not every abstract method need necessarily be implemented. This is for mission-critical functionality where the entire purpose of the class is negated without implementation.