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'])
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
.