Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

__init__ not being called for double inherited class

Tags:

python

django

I have the following code:

from form_utils import forms as betterforms
from django.db import models

class FilterForm(betterforms.BetterForm):
    def __init__(self, *args, **kwargs):
        super(FilterForm, self).__init__(*args, **kwargs)
        print('filter form __init__')

    ...

class NewEntityForm(FilterForm, FileFormMixin):
    def __init__(self, *args, **kwargs):
        super(NewEntityForm, self).__init__(*args, **kwargs)
        # super(FileFormMixin, self).__init__() <-- really does not matter
        print('newentityform __init__')

FileForMixin defined as follows:

class FileFormMixin(object):
    def __init__(self, *args, **kwargs):
        super(FileFormMixin, self).__init__(*args, **kwargs)
        print('file form mixin __init__')

FileFormMixin is provided by https://github.com/mbraak/django-file-form, betterforms by https://github.com/carljm/django-form-utils.

The problem is, FileFormMixin's __init__ is never getting called. How can I fix that? I really need all of them. Right now it's executing only FilterForm and NewEntityForm constructors.

UPDATE

So, I looked at all mentioned classes __init__'s, and they're calling super()!

FileFormMixin:

class FileFormMixin(object):
    def __init__(self, *args, **kwargs):
        super(FileFormMixin, self).__init__(*args, **kwargs)

BetterForm:

class BetterBaseForm(object):
    ...
        def __init__(self, *args, **kwargs):
        self._fieldsets = deepcopy(self.base_fieldsets)
        self._row_attrs = deepcopy(self.base_row_attrs)
        self._fieldset_collection = None
        super(BetterBaseForm, self).__init__(*args, **kwargs)

class BetterForm(with_metaclass(BetterFormMetaclass, BetterBaseForm),
                 forms.Form):
    __doc__ = BetterBaseForm.__doc__

More of that, printing class' mro as @elwin-arens proposed, gives following output:

filter form __init__ NewEntityForm.__mro__ (<class 'myapp.forms.NewEntityForm'>, <class 'myapp.forms.FilterForm'>, <class 'form_utils.forms.BetterForm'>, <class 'django.forms.widgets.NewBase'>, <class 'form_utils.forms.BetterBaseForm'>, <class 'django.forms.forms.Form'>, <class 'django.forms.forms.BaseForm'>, <class 'django_file_form.forms.FileFormMixin'>, <class 'object'>) newsiteform __init__

But __init__ for FileFormMixin is executed only if I call it explicitly as @tom-karzes advised

like image 294
user37741 Avatar asked Jun 12 '16 06:06

user37741


2 Answers

tl;dr

From your posted MRO of NewEntityForm you see the class BaseForm. In the source of django.forms.forms.BaseForm you can also see that this class does not call super(BaseForm, self).__init__() and therefore is responsible for breaking the chain to FileFormMixin.

In this case you can work around this by changing the order of NewEntityForm's base classes like so:

class NewEntityForm(FileFormMixin, FilterForm):
    def __init__(self, *args, **kwargs):
        super(NewEntityForm, self).__init__(*args, **kwargs)

Explanation

What you're probably thinking is that the classes FilterForm and FileFormMixin exists as base classes side-by-side.

In reality inheriting from multiple classes creates a Method Resolution Order (MRO) that is a list of classes that is traversed linear, in-order. This order determines what super means in a given context.

A simplified example to demonstrate:

class A(object):
  def __init__(self):
    super(A, self).__init__()
    print('A', A.__mro__)

class B(object):
  def __init__(self):
    super(B, self).__init__()
    print('B', B.__mro__)

class C(B, A):
  def __init__(self):
    super(C, self).__init__()
    print('C', C.__mro__)


c = C() 

super takes a class and moves one to the right in the method resolution order. So super(C, self).__init__() calls B.__init__, which should be obvious, but super(B, self).__init__() in this context calls A.__init__ and not object.__init__, which probably is less obvious.

This is because in the above code example C has the following method resolution order:

C (<class 'C'>, <class 'B'>, <class 'A'>, <class 'object'>)

To put it in pictures, the left if what you might expect, the right is what's actually happening:

    Mental model             Actual model
        C                        C
        |                        |
       / \                       B
      B   A                      |
      |   |                      A
       \ /                       |   
      object                   object

So if B doesn't call it's super:

class A(object):
  def __init__(self):
    super(A, self).__init__()
    print('A')

class B(object):
  def __init__(self):
    print('B')

class C(B, A):
  def __init__(self):
    super(C, self).__init__()
    print('C')


c = C()

This results in A's __init__() not being called after calling super(C, self).__init__().

This then begs the question what the MRO of NewEntityForm looks like. You can print the mro like so:

print('NewEntityForm.__mro__', NewEntityForm.__mro__)

This will probably show a list where FileFormMixin comes after a class that does not call it's super init method, as Daniel Roseman already said.

A possible solution might be to explicitly call the init of the class you want to apply like so:

class NewEntityForm(FilterForm):
    def __init__(self, *args, **kwargs):
        super(NewEntityForm, self).__init__(*args, **kwargs)
        FileFormMixin.__init__(self)
        print('newentityform __init__')

Or you could flip the base classes of NewEntityForm around.

like image 56
Elwin Arens Avatar answered Nov 03 '22 00:11

Elwin Arens


Presumably, BetterForm does not call super, so the chain stops there.

The only thing you can do is to swap the order of classes in the declaration:

class NewEntityForm(FileFormMixin, FilterForm):

Although of course this might affect any other methods that are defined in both classes.

like image 6
Daniel Roseman Avatar answered Nov 03 '22 02:11

Daniel Roseman