An idea for method_missing

M

mrstevegross

I was exploring techniques for implementing method_missing in Python.
I've seen a few posts out there on the subject... One tricky aspect is
if it's possible to not just intercept a method_missing call, but
actually dynamically add a new function to an existing class. I
realized you can modify the Class.__dict__ variable to actually add a
new function. Here's how to do it:

class Foo:
def __init__(self):
self.foo = 3

def method_missing(self, attr, *args):
print attr, "is called with %d args:" % len(args),
print args

def __getattr__(self, attr):
print "Adding a new attribute called:", attr
def callable(*args):
self.method_missing(attr, *args[1:])
Foo.__dict__[attr] = callable
return lambda *args :callable(self, *args)

In the sample client code below, you'll see that the first time
go_home gets invoked, the __getattr__ call reports that it is adding a
new function. On subsequent calls, the actual method gets used:

f = Foo()
f.go_home(1,2,3)
f.go_home(1,2,3)
f.go_home(1,2,3)
f.go_away('boo')
f.go_home(1,2,3)

--Steve
 
G

Gabriel Genellina

I was exploring techniques for implementing method_missing in Python.
I've seen a few posts out there on the subject... One tricky aspect is
if it's possible to not just intercept a method_missing call, but
actually dynamically add a new function to an existing class. I
realized you can modify the Class.__dict__ variable to actually add a
new function. Here's how to do it:

class Foo:
def __init__(self):
self.foo = 3

def method_missing(self, attr, *args):
print attr, "is called with %d args:" % len(args),
print args

def __getattr__(self, attr):
print "Adding a new attribute called:", attr
def callable(*args):
self.method_missing(attr, *args[1:])
Foo.__dict__[attr] = callable
return lambda *args :callable(self, *args)

You're adding a function (a closure, actually) to some *class* that refers
to a specific *instance* of it. This doesn't work as expected; let's add
some verbosity to show the issue:

class Foo:
def __init__(self, foo):
self.foo = foo

def method_missing(self, attr, *args):
print 'Foo(%r)' % self.foo, attr, "is called with %d args:" %
len(args),
print args
# __getattr__ stays the same

f = Foo('f')
f.go_home(1,2,3)
g = Foo('g')
g.go_home('boo')

# output:
Adding a new attribute called: go_home
Foo('f') go_home is called with 3 args: (1, 2, 3)
Foo('f') go_home is called with 1 args: ('boo',)

One would expect Foo('g') on the last line. Worse:

py> print f
Adding a new attribute called: __str__
Foo('f') __str__ is called with 0 args: ()

Traceback (most recent call last):
File "<pyshell#21>", line 1, in <module>
print f
TypeError: __str__ returned non-string (type NoneType)
py> if f: print "Ok!"

Adding a new attribute called: __nonzero__
Foo('f') __nonzero__ is called with 0 args: ()

Traceback (most recent call last):
File "<pyshell#28>", line 1, in <module>
if f: print "Ok!"
TypeError: __nonzero__ should return an int

Almost anything you want to do with a Foo instance will fail, because
Python relies on the existence or not of some __magic__ methods to
implement lots of things.

The code below tries to overcome this problems. Still, in Python, unlike
Ruby, there is no way to distinguish between a "method" and an "attribute"
-- both are looked up exactly the same way. __getattr__ already is,
roughly, the equivalent of method_missing (well, attribute_missing, I'd
say).

class Foo(object):
def __init__(self, foo):
self.foo = foo

def method_missing(self, attr, *args, **kw):
print 'Foo(%r)' % self.foo, attr, "is called with %d args:" %
len(args),
print args

def __getattr__(self, attr):
if attr[:2]==attr[-2:]=='__':
raise AttributeError(attr)
print "Adding a new attribute called:", attr
def missing(self, *args, **kw):
return self.method_missing(attr, *args, **kw)
setattr(type(self), attr, missing)
return missing.__get__(self, type(self))

f = Foo('f')
f.go_home(1,2,3)
f.go_home(2,3,4)
g = Foo('g')
g.go_home('boo')
f.go_home('f again')
print f # works fine now
print f.foox # oops! typos have unexpected side effects :(
print len(f) # should raise TypeError
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,767
Messages
2,569,572
Members
45,045
Latest member
DRCM

Latest Threads

Top