- wto 26 lipca 2022
- Programming
- #python, #metaclasses, #mro
When I first learned about metaclasses I wondered when exactly I should use them. This is generally an issue in programming for me that you can tackle a problem in many ways, each way has pros and cons, and you never really know what the best way is until you have tried it.
Back to metaclasses. I had an issue that one endpoint in an application I was working on
was timing out. After profiling with cProfiler (and visualizing with snakeviz - I highly
recommend this tool!)
I found out that the issue was with a django.models.Model
subclass that also inherited from a
bunch of other superclasses and mixins. I will call this class MyModel
.
One of these mixins (let's call it CulpritMixin
) did some calculations when MyModel
instance
was initialized
and these calculations were the reason why the endpoint timed-out.
I was pretty sure we didn't need these calculations in this endpoint. So my thinking was:
since we don't need the calculations, why not just remove the mixin?
Of course, I couldn't do just that because MyModel
was being used in a lot of other places in
the system where the mixin was actually useful. So I had to remove the mixin only for the
one endpoint where MyModel
was used. How do you do that? I figured I can just remove the mixin
from the MRO of MyModel
and that would solve the issue. One way to remove a class from an MRO
is to use a metaclass!
class NoCulpritMixinMetaClass(type):
def mro(cls) -> list[type]:
tmp_mro = super().mro()
tmp_mro.remove(CulpritMixin)
return tmp_mro
class MyModelNoCulpritMixin(
MyModel, metaclass=NoCulpritMixinMetaClass
):
class Meta:
proxy = True
Ok, so that was my first attempt. I run unit tests, and Django had some issues. For some reason there was a discrepancy in class attributes in Django machinery when it was loading the models.
After some debugging I was able to get it work:
class NoCulpritMixinMetaClass(type):
def __init__(cls, *args, **kwargs):
super().__init__(*args, **kwargs)
cls._to_delete_attributes_ = set()
for name in dir(cls):
try:
inspect.getattr_static(cls, name)
except AttributeError:
cls._to_delete_attributes_.add(name)
def __dir__(cls):
"""There is some discrepancy with inspect.getattr_static and dir(model) in
django guts after we remove CulpritMixin from mro which throws errors,
so this fixes it."""
original_dir = super().__dir__()
for attr_to_delete in cls._to_delete_attributes_:
original_dir.remove(attr_to_delete)
return original_dir
def mro(cls) -> list[type]:
tmp_mro = super().mro()
tmp_mro.remove(CulpritMixin)
return tmp_mro
Ok, so now this solution is messy. At this point I was hesitant to show this code to
colleagues for a code review. Firstly, I guess, using metaclass could be described as controversial.
But in my first solution I had only modified def mro(cls)
so I had thought
it had been straightforward, and maybe it had justified the use of the metaclass.
But now I have been also getting mixed up with Django inner workings. Well, lets see if unit tests pass.
100% run successfully. Ok, I guess this maybe is not that bad. I've created an MR and wrote a detailed description of the problem.
Next day nobody really said anything about using the metaclass - the solution was to simply not
initialize MyModel
instance at all, and use MyModel.objects.filter(...).values_list(...)
instead of MyModel.objects.filter(...)
! This way the code of CulpritMixin
is not called at all, and
there are no heavy computations! Much simpler than my solution!
So in the end I haven't yet had an opportunity to use metaclasses "in the real world". I was actually pretty happy about that because it wasn't comfortable to tamper with Django guts.
To sum up, it turned out not to be the best use case for a metaclass, but I am still happy I have it in my toolbox!