Search code examples
pythonconstructorconstructor-overloading

How to implement multiple constructors in python?


In python it's not possible to define the init function more than once, which, knowing how the language works, it's rather fair. When an object is created, the init is called, so, having two of them would create uncertainty. However in some designs such property is desirable. For example:

class Triangle(object):
  def __init__(self,vertices):
    self.v1 = vertices[0]
    self.v2 = vertices[1]
    self.v3 = vertices[2]

 def area(self):
   # calculate the are using vertices
   ...
   return result

class Triangle(object):
  def __init__(self,sides):
    self.v1 = sides[0]
    self.v2 = sides[1]
    self.v3 = sides[2]

 def area(self):
   # calculate the are using sides
   ...
   return result

in such cases we have the same number of attribute to initialize, and also they are correlated so that from one, you can obtain the other.True, in this specific example one could work with the fact that vertices are tuples whilst sides may be floats (or strings or something else) but of course it's not always the case.

One possible solution would be to delegate the initialization process to other functions like:

class Triangle(object):
  def __init__(self):
    self.v1 = None
    self.v2 = None
    self.v3 = None

  def byVertices(self,vertices):
    self.v1 = vertices[0]
    self.v2 = vertices[1]
    self.v3 = vertices[2]

 def sidesToVertices(self,sides):
    # converts sides to vertices
    ...
    return vertices

 def bySides(self,sides):
    vertices = sidesToVertices(sides)
    self.v1 = vertices[0]
    self.v2 = vertices[1]
    self.v3 = vertices[2]

 def area(self):
   # calculate the are using vertices
   ...
   return result

but it doesn't look very clean, and all the functionalities like "area" would have to check that the attributes are correctly instanciated (or adopt a try/catch) which is a lot of code, plus it undermines the readability of the project. Overall it looks like a cheap trick for the purpose.

Another option would be to tell the instance, what type of attributes you are going to initialize:

class Triangle(object):
  def __init__(self, data, type = "vertices"):
    if type == "sides":
      data = sideToVertices(self,sides)
    else if type == "vertices":
      pass
    else:
      raise(Exception)

   self.v1 = data[0]
   self.v2 = data[1]
   self.v3 = data[3]

 def sidesToVertices(self,sides):
    # converts sides to vertices
    ...
    return vertices



 def area(self):
   # calculate the are using vertices

This other approach seems preferrable, however i'm not sure how much "pythonic" is to introduce logic in the init. What are your tought on the matter ? Is there a better way to orchestrate the situation ?


Solution

  • Alternate constructors are the most common use case for class methods. Your "real" __init__ is often then the lowest common denominator for the various class methods.

    class Triangle(object):
        def __init__(self, v1, v2, v3):
            self.v1 = v1
            self.v2 = v2
            self.v3 = v3
    
        # This is just here to demonstrate, since it is just
        # wrapping the built-in __new__ for no good reason.
        @classmethod
        def by_vertices(cls, vertices):
            # Make sure there are exactly three vertices, though :)
            return cls(*vertices)
    
        @staticmethod
        def sidesToVertices(sides):
            # converts sides to vertices
            return v1, v2, v3 
    
        @classmethod
        def by_sides(cls, sides):
            return cls(*sides_to_vertices(sides)) 
    
        def area(self):
            # calculate the are using vertices
            ...
            return result
    

    The, to get an instance of Triangle, you can write any of the following:

    t = Triangle(p1, p2, p3)
    t = Triangle.by_vertices(p1, p2, p3)  # Same as the "direct" method
    t = Triangle.by_sides(s1, s2, s3)
    

    The only difference here is that Triangle(p1, p2, p3) hides the implicit call to Triangle.__new__, which is a class method just like by_vertices and by_sides. (In fact, you could simply define by_vertices = __new__.) In all three cases, Python implicitly calls Triangle.__init__ on whatever cls returns.

    (Note that by_vertices generates a specific triangle, while by_sides could generate any number of "equivalent" triangles that differ only by position and rotation relative to the origin. Conversely, by_sides could be thought to generate the "real" triangle, with by_vertices specifying a triangle in a particular position. None of this is particularly relevant to the question at hand.)


    Tangential background.

    t = Triangle(v1, v2, v3) is the "normal" method, but what does this mean? Triangle is a class, not a function. To answer this, you need to know about metaclasses and the __call__ method.

    __call__ is used to make an instance callable. my_obj(args) becomes syntactic sugar for my_obj.__call__(args), which itself, like all instance methods, is syntactic sugar (to a first approximation) for type(my_obj).__call__(my_obj, args).

    You've heard that everything in Python is an object. This is true for classes as well; every class object is an instance of its metaclass, the type of types. The default is type, so Triangle(v1, v2, v3) would desugar to Triangle.__call__(v1, v2, v3) or type.__call__(Triangle, v1, v2, v3).

    With that out of the way, what does type.__call__ do? Not much. It just calls the appropriate class method __new__ for its first argument. That is, type.__call__(Triangle, v1, v2, v3) == Triangle.__new__(v1, v2, v3). Python then implicitly calls __init__ on the return value of Triangle.__new__ if it is, in fact, an instance of Triangle. Thus, you can think of type.__call__ as being defined like

    def __call__(cls, *args, **kwargs):
        obj = cls.__new__(*args, **kwargs)
        if isinstance(obj, cls):
           cls.__init__(obj, *args, **kwargs)
        return obj