/ python

Implementing a Generic Constructor in Python

A few posts ago I presented a neat ruby trick that defines a generic initialize method. The same idea can be implemented in python but I'm not sure the result is as satisfactory. Here's the code for you to judge.

What We're Building

Python already provides a namedtuple class with which one can build generic containers. But without monkey patching, named tuples are not that easy to extend. I'd like to be able to write:

rexy = Dog(age=10, name='Rexy')
print(rexy)

And be able to control the Dog class and extend it.

Take 1: A Simple Class

A first take is to simply define the Dog class:

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age  = age

    def __str__(self):
        return f"Hi I'm {self.name} and I'm {self.age} years old"

Python's built in support for named arguments is a life saver here and already provides a great deal of simplicity. Of course if we had multiple Dog like classes it may feel tedious to define __init__ for each of them.

Take 2: Inheritance

Using inheritance we can define __init__ only once and use it for multiple inheriting classes:

class NamedThing:
    def __init__(self, **opts):
        for key in opts:
            setattr(self, key, opts[key])

class Dog(NamedThing):
    def __str__(self):
        return f"Hi I'm {self.name} and I'm {self.age} years old"
        
class Cat(NamedThing):
    pass

rexy = Dog(age=10, name='rexy')
print(rexy)

Both Can and Dog can now be created using named arguments, and each will get their own attributes on self. For a final touch I'd add validations:

class NamedThing:
    def __init__(self, **opts):
        for key in opts:
            if key not in self.attr_accessor:
                raise Exception(f'Invalid Prop: {key}')
            setattr(self, key, opts[key])

class Dog(NamedThing):
    attr_accessor = ['name', 'age']

    def __str__(self):
        return f"Hi I'm {self.name} and I'm {self.age} years old"


rexy = Dog(age=10, name='rexy')
print(rexy)

Now trying to pass properties not defined on the class itself fails, and so we have better control what gets created and some protection from spelling mistakes.

Compared With Ruby

Initially the above looks nice but it has one main drawbacks: It's hard to customize. Let me clarify that by comparing with ruby.

First a reminder of the parallel ruby code:

module NamedThing
  def initialize(**args)
    args.each do |k, v|
      send("#{k}=", v)
    end
  end
end

class Dog
  include NamedThing
  attr_accessor :name, :age

  def to_s
    "Hi! My name is #{name} and I am #{age} years old"
  end
end

d = Dog.new(age: 40, name: 'rexy')
puts d

Now what happens when we need to add some logic to the dog, for example to have its name lowercased after creation (and after each modification). Well in ruby all we have to do is to add a new setter method and everything else just works:

module NamedThing
  def initialize(**args)
    args.each do |k, v|
      send("#{k}=", v)
    end
  end
end

class Dog
  include NamedThing
  attr_accessor :name, :age

  def name=(val)
    @name = val.downcase
  end

  def to_s
    "Hi! My name is #{name} and I am #{age} years old"
  end
end

d = Dog.new(age: 40, name: 'Rexy')
puts d
d.name = 'Fuzzy'
puts d

The new setter is used by both the generic code and by normal ruby code which follows. Python, due to its different treatment of properties, won't allow the same reuse. To define a setter we need to select a different name for the actual attribute, which renders the entire __init__ logic written above useless.