Self-Initializing Classes

Published Feb 21, 2018Last updated Feb 22, 2018
Self-Initializing Classes

Python is famous for its high productivity, mainly due to a clean and concise syntax. Still, some tasks require to write boilerplate code. For example, any class constructor will receive the necessary values to initialize the state of the instantiated object through a sequence of self.fieldname = value.

It is a common pattern that the name of the constructor's formal parameters coincide with the fields to initialize (i.e., each line of the constructor will read as self.fieldname = fieldname).

In this article, I'll show you how avoid writing boilerplate code to initialize the fields of a class using Python metaclasses and decorators.

Requirements

  • Python Version 3.*

RoadMap

I will create a class called Person with some attributes in the most OOP common way. Then, I'll introduce the code step by step with metaclasses and decorators with the "auto-init" logic, also using the magic module inspect.

Base Code

I have created a main.py and at the beginning, my code looks like this:

  class Person ():
    def __init__(self, first_name, last_name, birth_date, sex, address):
      self.first_name = first_name
      self.last_name  = last_name
      self.birth_date = birth_date
      self.sex        = sex
      self.address     = address
    def __repr__(self):
      return "{} {} {} {} {}"\
      .format(self.first_name, self.last_name, self.birth_date, self.sex, self.address)
  
  if __name__ == '__main__':
    john = Person('Jonh', 'Doe', '21/06/1990', 'male', '216 Caledonia Street')
    print(john)

Autoinit Module

I will create a new file called autoinit.py, where I will place all of my logic.

class AutoInit(type):
  def __new__(meta, classname, supers, classdict):
    return type.__new__(meta, classname, supers, classdict)

I have created a class AutoInit that at the moment does nothing. It's just a simple metaclass. So I can immediatly use my metaclass on the Person class, like this:

#main.py
from autoinit import AutoInit

class Person (metaclass=AutoInit):
  ...
  ...
  ...

So this is the first change to the person class that I had to do. We will conclude the refactor later, once the autoinit module has finished.

Decore the '__init__' class method

The __init__ method is the constructor method of a class. In order to archieve our goal, we have to place a decorator around this method to power up the normal behavior.

#autoinit.py
class AutoInit(type):
  def __new__(meta, classname, supers, classdict):
    classdict['__init__'] = autoInitDecorator(classdict['__init__'])
    return type.__new__(meta, classname, supers, classdict)

def autoInitDecorator (toDecoreFun):
  def wrapper(*args):
    print("Hello from autoinit decorator")
    toDecoreFun(*args)
  return wrapper

The 'inspect' module

From the inspect module, I will use the getargspec function. From the Python documentation:

Get the names and default values of a function's arguments.
A tuple of four things is returned: (args, varargs, varkw, defaults).
'args' is a list of the argument names (it may contain nested lists).
'varargs' and 'varkw' are the names of the * and ** arguments or None.
'defaults' is an n-tuple of the default values of the last n arguments.

So with this powerful function, my code will look like this:

#autoinit.py
class AutoInit(type):
  def __new__(meta, classname, supers, classdict):
    classdict['__init__'] = autoInitDecorator(classdict['__init__'])
    return type.__new__(meta, classname, supers, classdict)

def autoInitDecorator (toDecoreFun):
  def wrapper(*args):
    
    # ['self', 'first_name', 'last_name', 'birth_date', 'sex', 'address']
    argsnames = getargspec(toDecoreFun)[0]
    
    # the values provided when a new instance is created minus the 'self' reference
    # ['Jonh', 'Doe', '21/06/1990', 'male', '216 Caledonia Street']
    argsvalues = [x for x in args[1:]]
    
    # 'self' -> the reference to the instance
    objref = args[0]
    
    # setting the attribute with the corrisponding values to the instance
    # note I am skipping the 'self' reference
    for x in argsnames[1:]:
     	objref.__setattr__(x,argsvalues.pop(0))
    
  return wrapper

Code refactoring

So my main.py code will look like this:

  from autoinit import AutoInit

  class Person (metaclass=AutoInit):
    def __init__(self, first_name, last_name, birth_date, sex, address):
      pass
    def __repr__(self):
      return "{} {} {} {} {}"\
      .format(self.first_name, self.last_name, self.birth_date, self.sex, self.address)
  
  if __name__ == '__main__':
    john = Person('Jonh', 'Doe', '21/06/1990', 'male', '216 Caledonia Street')
    print(john)

That's all folks

Note that this example is not designed to work also with key arguments (maybe in a future article I could made an extension of this exercise).

Metaclasses and decorators are very powerful tools in Python. I very quickly do amazing things in the other languages. But is also easy to fall into code nightmares when the source code grows up.

Discover and read more posts from Marco Predari
get started