Search code examples
pythoninheritancemultiple-inheritanceinitsuper

mutiple inheritance: use __init__ method of two superclasses


I want to make multiple inheritance, making a subclass inherit from two different super classes. This subclass should have an __init__ method in which the __init__ methods of the super classes are called with the expected arguments.

Here is an example that can illustrate my problem:

class Place:
   def __init__(self, country, city, **attr):
      self.country = country
      self.city = city
      self.attributes = attr
   
   def show_location(self):
      print(self.country, self.city)

class Product:
   def __init__(self, price, currency = '$'):
      self.price = price
      self.currency = currency

   def show_price(self):
      print(self.price, self.currency)

class Flat(Place, Product):
   def __init__(self, country, city, street, number, price, currency = '$', **attr):
      super(Place).__init__(country, city, **attr)
      super(Product).__init__(price, currency)
      self.street = street
      self.number = number

   def show_address(self):
      print(self.number, self.street)
      
myflat = Flat('Mozambique', 'Nampula', 'Rua dos Combatentes', 4, 150000, '$')

But I don't really know how to use the super() method, and this code throws the following error:

TypeError: super() argument 1 must be type, not str

I wonder how can I initialize the Flat object both as a Place and as a Product...

Is it appropriate to use super() in this context?

Or is it better to call __init__ directly this way: SuperClass.__init__(self, ...) ?


Solution

  • TL;DR The article Python’s super() considered super! explains in detail how to use super correctly.


    super is intended to implement cooperative multiple inheritance, where the classes involved are designed together to support such inheritance. This requires redesigning both Place and Product slightly; the linked article discusses the rationale for the redesign shown below. (If you can't redesign them, the linked article also explains how to define adaptor classes to wrap them.)

    class Place:
       def __init__(self, *, country, city, **kwargs):
          super().__init__(**kwargs)
          self.country = country
          self.city = city
       
       def show_location(self):
          print(self.country, self.city)
    
    class Product:
       def __init__(self, *, price, currency='$', **kwargs):
          super().__init__(**kwargs)
          self.price = price
          self.currency = currency
    
       def show_price(self):
          print(self.price, self.currency)
    
    class Flat(Place, Product):
       def __init__(self, *, street, number, **kwargs):
          super().__init__(**kwargs)
          self.street = street
          self.number = number
    
       def show_address(self):
          print(self.number, self.street)
          
    myflat = Flat(country='Mozambique', city='Nampula', street='Rua dos Combatentes', number=4, price=150000)
    

    The key is that super refers to the next class in the method resolution order (MRO), a list of classes constructed from the inheritance tree. Only one call to super is required in each method. Both Place and Product use it as well, because neither class knows if it will be the last class in the MRO of the object being initialized. (More precisely, both know they won't, because object itself is always the final class.)

    Each __init__ method accepts a certain number of keyword-only arguments that it knows what to do with, along with arbitrary keyword arguments that it will pass on to one of the ancestor class to handle. Ultimately, if the classes are defined correctly, one of the calls to super().__init__(**kwargs) will invoke object.__init__ with no keyword arguments, ending the chain.