Programmatically adding methods to classes and objects: more Ruby/Python comparisons

In Dynamically extending object behaviour in Ruby and Python we explored techniques for extending object behaviour dynamically by catching calls to undefined methods and having the target objects respond in some interesting way. In this sequel we achieve similar results by a very different (put perhaps more straightforward) approach, adding methods to existing classes and objects ahead of time programmatically.

I’ve avoided here approaches that simply involve generating program source as strings and then eval-ing it in some way. This  can be effective, but it’s uninteresting and perhaps a bit inelegant! Instead, we use closures, in the form of blocks, lambdas and nested functions.

Straight then to code. First comes Ruby, which I’ve done for 5 or so years on and off, then Python – for 3 weeks maybe? With that admission out there I’ll be as fair as I can!

Ruby

class Hello
  attr_reader :name

  def initialize(name)
    @name = name
  end

  # define a method named x that says hi to x
  def self.def_hi(x)
    define_method(x) { "Hi #{x} from #{name}" }
  end

  # handy helper - see http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html
  def metaclass; class << self; self; end; end

  # uniquely for the object, define a method named x that says hi to x
  def def_obj_yo(x)
    metaclass.send(:define_method, x) { "Hi #{x} from #{name}" }
  end

  def_hi "everyone"
end

h1 = Hello.new("h1")
h2 = Hello.new("h2")

Hello.def_hi("foo")
h1.def_obj_yo("baz")

puts h1.everyone      #=> "Hi everyone from h1"
puts h2.everyone      #=> "Hi everyone from h2"
puts h1.foo           #=> "Hi foo from h1"
puts h2.foo           #=> "Hi foo from h2"
puts h1.foo           #=> "Hi bar from h1"
puts h2.foo           #=> "Hi bar from h2"
puts h1.baz           #=> "Hi Baz from h1"
puts h2.baz           #=> NoMethodError: undefined method ‘baz’ for #

Here, the everyone and foo methods are defined on the class as normal instance methods, and baz is defined as a method on h1‘s “metaclass” (thanks to _why for the helper), making it specific to the h1 instance.

In each case, define_method does the job of creating and installing the method on a class; the subtlety is in the way the blocks (the {...} bits) capture the x parameters for later use.

Newbie Python

class Hello(object):
    def __init__(self, name):
        self.name = name

    @classmethod
    def def_hi(cls, x):
        'define a method named x that says hi to x'
        setattr(cls, x, lambda self: "Hi %s from %s" % (x, self.name))

    @classmethod
    def def_hello(cls, x):
        'define a method x that says hi to x'
        def say_hello(self):
            return "Hello %s from %s" % (x, self.name)
        setattr(cls, x, say_hello)

    def def_obj_yo(self, x):
        'uniqely for the object, add a callable attribute named x that says hi to x'
        setattr(self, x, lambda: "Yo %s from %s" % (x, self.name))

h1 = Hello("h1")
h2 = Hello("h2")

Hello.def_hi("foo")
Hello.def_hello("bar")
h1.def_obj_yo("baz")

print h1.foo()           #=> "Hi foo from h1"
print h2.foo()           #=> "Hi foo from h2"
print h1.baz()           #=> "Yo baz from h1"
print h2.baz()           #=> AttributeError: 'Hello' object has no attribute 'baz'

To be honest, I’m not sure how idiomatic this Python code is, but it certainly works in Python 2.6.2. The reason I mention the version is that older documentation refers to a now-deprecated module “new”, that contains tools similar to Ruby’s define_method. My code is remarkably simple for what it achieves: it just adds lambda objects (in def_hi and def_obj_yo) or function objects (in def_hello) and sets them as attributes of the class or object. Again, we’re relying on those closures to capture the x parameters indefinitely.  The “hi” and “hello” versions aren’t very different (except in length);  sometimes a lambda is too restrictive and a nested function is more appropriate.

I’m 99.9% sure satisfied now (I was rather less sure as I posted the first drafts of this article; I’ve since read the relevant parts of the Language Reference) that the foo objects are proper methods in the strictest sense, though they definitely do behave like them. It’s probably fair to say though that though baz isn’t really a method; self isn’t set as a result of calling the method – it’s just another variable captured in the lambda. Sneaky! If we copied it onto another object, self would still refer to h1.

Comparison

These two lines of Ruby:

  attr_reader :name
  def_hi "everyone"

are executed as the class definition is created. They are very simple examples of how we can define and use what look like new keywords but are in reality just calls to class methods. More complicated examples accept blocks, providing nice API-defined extension points for subclasses to add custom behaviour. Ruby excels in its ability to define APIs not just for their functionality but for their sheer prettiness from the user’s (especially the extender’s) perspective.

Even disregarding cosmetic details such as the optionality of parentheses in Ruby, I was unable to execute similar code within the scope of the Python class definition. Please do correct if me if I’ve got this wrong – having commented this morning (perhaps prematurely) on Yehuda Katz’s interesting article The Importance of Executable Class Bodies it would be good to clear this up!

[UPDATE: yes you can execute arbitrary code within the scope of the class definition, but it’s not quite as simple as calling class methods – in fact I still haven’t found a way to access the class object as it’s being defined (perhaps it doesn’t exist yet!).  The gory details of something that looks pretty close are in 157768: Automating simple property creation (from 2002, perhaps superseded since)]

There is however still a certain elegance to the Python idea (explored more fully last time) of methods being just function-valued (or callable) attributes of objects. No special syntax needed (I don’t think I can ever learn to love Ruby’s class << self idiom) and I love the ease with which function objects can be obtained, installed and invoked, and how, nested within other functions, they can be closures.

On balance? Ruby does seem capable of supporting the nicest APIs (DSLs even), but there’s a simplicity to Python that perhaps makes for more understandable code internally. There’s an interesting balance there to be struck, though neither one wins on a knockout. We’re down now to the level of personal preference, though even without choosing a winner it has definitely been a beneficial experience to have taken on a new language in a serious way. It changes you!

2 thoughts on “Programmatically adding methods to classes and objects: more Ruby/Python comparisons

Comments are closed.