blog

Photo by Inbetween Architects on Unsplash

Adding Dynamically Named Methods to Python Classes

by

I recently wanted to dynamically add methods to a Python class based on a list of relationships. The model class, Animal, was related to a number of description classes like Color and Sound. For a given animal, I wanted to be able to easily get the values for its descriptions, such as:

elephant.color_values()
# => ['gray', 'silver', 'taupe']

Although I could have simply written non-dynamic methods like color_values, noise_values, etc. in the Animal class, I had already defined a list of the relevant class names and I prefer not repeating code. So instead I dove into Python’s dynamic pool.

Animal class

For simplicity, I’ve swapped out the code that traverses the class relationships and instead stored the descriptions directly in the Animal class. Here’s the class simplified for this blog post, along with the list of descriptions available:

class Animal:
   descriptions = {}
   def __init__(self, name, colors, sounds):
      self.name = name
      self.descriptions['color'] = colors
      self.descriptions['sound'] = sounds
description_names = ['color', 'sound']

And a cat to play with:

cat = Animal('cat', colors=['red', 'orange'], sounds=['purr', 'meow'])
Image of Cat, by Kerri Lee Smith

[Photo by Kerri Lee Smith](http:/https://www.flickr.com/photos/77654185@N07/8231178508/ “Photo by Kerri Lee Smith”)

Add method with known name

If I knew the description names ahead of time and wasn’t too lazy to write them out again, I could easily add a method to Animal outside of the class definition:

def color_values(self):
   print "{0}: {1}".format(self.name, self.descriptions['color'])
Animal.color_values = color_values
cat.color_values()
# => cat: ['red', 'orange']

Add methods with unknown names

But if the method names are unknown, or in this case I’m trying to avoid repeating them, things get a little more complicated. Instead of explicitly typing out the method name, the name comes from a variable, and is set on the class using the setattr method.

def add_description_fn(description):
   fn_name = description + '_values'
   def fn(self):
      print "{0}: {1}".format(self.name, self.descriptions[description])
   setattr(Animal, fn_name, fn)
for description in description_names:
   add_description_fn(description)
cat.color_values()
# => cat: ['red', 'orange']
cat.sound_values()
# => cat: ['purr', 'meow']

Why add_description_fn was necessary

In my first pass at this problem, I created the dynamic methods directly in the for-loop and was baffled why .color_values() was returning sounds rather than colors. The problem was caused by Python’s unusual for-loop variable scoping (described in some detail in this scope resolution guide, and also by Fran Bull in an earlier post on Python gotchas that I clearly didn’t take to heart). As I learned from this exercise, the loop variable defined in a for-loop exists at the block level that contains the for-loop. Here’s code to illustrate the issue:

fns = {}
for i in ['a', 'b']:
   fns[i] = lambda: i
print i
# => b
print fns['a']()
# => b
print fns['b']()
# => b

The loop variable i is local to this entire block, and so the a and b functions end up referencing the exact same variable, rather than separate temporary variable set to ‘a’ and ‘b’.

Moving the description accessor method definition into add_description_fn avoids this problem by using a variable local to the method definition scope that’s not shared with the next iteration of the loop.

Documenting a dynamic method

Just because a method is dynamic doesn’t mean it can’t be properly documented. Besides code comments around the definition of the method, a dynamically added methods should have its name and doc attributes set. The name attribute is particularly important for readable stack traces.

Here’s the updated add_description_fn function:

def add_description_fn(description):
   fn_name = description + '_values'
   def fn(self):
       print "{0}: {1}".format(self.name, self.descriptions[description])
   setattr(Animal, fn_name, fn)
   fn.__name__ = fn_name
   fn.__doc__ = "Return values for the {0} description".format(description)

Calling help(cat.color_values)now prints useful information about the new method:

Help on method color_values in module __main__:
color_values(self) method of __main__.Animal instance
    Return values for the color description

And thus the best of both worlds: well-documented, easy-to-call methods defined without lots of copy-and-paste. As long as new description names are added to description_names (which can be used for other dynamic processing of descriptions, like displaying them all through a web interface), corresponding description accessor methods will be added to Animal.

+ more