0

Consider the following code:

class Lockable(object):

    def __init__(self):
        self._lock = None

    def is_locked(self):
       return self._lock is None


class LockableDict(dict, Lockable):
     pass

And now:

In [2]: l = example.LockableDict(a=1, b=2, c=3)

In [3]: l.is_locked()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-3-4344e3948b9c> in <module>()
----> 1 l.is_locked()

/home/sven/utils/example.py in is_locked(self)
      8 
      9     def is_locked(self):
---> 10         return self._lock is None

AttributeError: 'LockableDict' object has no attribute '_lock'

It looks as though Lockable.__init__ weren't called at all. Is that so? Why?

To make things more interesting, it turns out that it's sufficient to change the LockableDict class header like this:

class LockableDict(Lockable, dict):

to make it work. Why?

Sventimir
  • 1,996
  • 3
  • 14
  • 25
  • If you don't call it, it's not called. – laike9m May 18 '15 at 07:55
  • What do you mean? I don't override `__init__` in `LockableDict`, so it should generate an `__init__` making automatic calls to base classes' `__init__`s, shouldn't it? Also it doesn't explain why the second variant works... – Sventimir May 18 '15 at 07:57
  • You should explicit call `__init__` in the `__init__` of LockableDict – RvdK May 18 '15 at 08:00
  • No, that's the way C++ does, but not Python. – laike9m May 18 '15 at 08:00
  • Then why does it work after the modification I described? – Sventimir May 18 '15 at 08:01
  • 1
    @RvdK: I think this is a different question; that OP understands how Python works, and wanted to know why it was designed that way; this one doesn't understand, and wants to know what's happening. – abarnert May 18 '15 at 08:03
  • If the subclass inherits from only ONE parent and if the subclass does not define its own constructor, the parent's constructor will be inherited and used when instantiating the subclass. From your experiments with subclassing from multiple parents, it appears that Python takes the first parent. – Kevin Lee Dec 14 '15 at 08:54

1 Answers1

4

Your misunderstanding is clear from this comment:

I don't override __init__ in LockableDict, so it should generate an __init__ making automatic calls to base classes' __init__s, shouldn't it?

No!

First, nothing gets automatically generated; the method resolution happens at call time.

And at call time, Python won't call every base class's __init__, it will only call the first one it finds.*

This is why super exists. If you don't explicitly call super in an override, no base classes or even sibling classes will get their implementations called. And dict.__init__ doesn't call its super.

Neither does your Lockable.__init__. So, this means that reversing the order makes sure Lockable.__init__ gets called… but dict.__init__ now doesn't get called.


So, what do you actually want to do? Well, Python's MRO algorithm is designed to allow maximum flexibility for hierarchies of cooperating classes, with any non-cooperating classes thrown in at the end. So, you could do this:

class Lockable(object):

    def __init__(self):
        super().__init__() # super(Lockable, self).__init__() for 2.x
        self._lock = None

    def is_locked(self):
       return self._lock is None


class LockableDict(Lockable, dict):
     pass

Also notice that this allows you to pass initializer arguments along the chain through all your cooperating classes, and then just call the unfriendly class's __init__ with the arguments you know it needs.

In a few cases, you have to put an unfriendly class first.** In that case, you have to explicitly call around them:

class LockableDict(dict, Lockable):
    def __init__(self):
        dict.__init__(self)
        Lockable.__init__(self)

But fortunately, this doesn't come up very often.


* In standard method resolution order, which is slightly more complicated than you'd expect. The same MRO rule is applied when you call super to find the "next" class—which may be a base class, or a sibling, or even a sibling of a subclass. You may find the Wikipedia article on C3 linearization more approachable.

** Especially for built-in classes, because they're special in a few ways that aren't worth getting into here. Then again, many built-in classes don't actually do anything in their __init__, and instead do all the initialization inside the __new__ constructor.

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • Is there any gain from Python not calling *all* `__init__`s but only the first one it finds? I see none... – Sventimir May 18 '15 at 08:21
  • 1
    @Sventimir Yes. For just one advantage: imagine that Lockable is later changed to need an extra argument that dict won't accept. If Python automatically called every base class, your code could no longer work. For more details, read the links I posted in the footnotes and the answer to the related question RvdK linked in a comment on your question. (Because this followup basically is a dup of that related question.) – abarnert May 18 '15 at 08:23