4

I'm writing a client for a group of RESTful services. The body of the REST calls have the same XML structure, given parameters. There are several dozen calls, and I will not be implementing all of them. As such, I want to make them easy to specify and easy to use. The REST methods are grouped by functionality in separate modules and will need to share the same urllib2 opener for authentication and cookies. Here's an example of how a method is declared:

@rest_method('POST', '/document')
def createDocument(id, title, body):
    # possibly some validation on the arguments
    pass

All the developer has to care about is validation. The format of the XML (for POST and PUT) or the URL (for GET and DELETE) and the deserialization of the response is done in helper methods. The decorated methods are collected in a client object from which they will be executed and processed. For example:

c = RESTClient('http://foo.com', username, password)
c.createDocument(1, 'title', 'body')

The code is done. The only issue is in attaching the decorated methods to the client class. Although all the decorated methods can be seen in the client instance, they all share the same definition, namely the last one to be bound. Here's a brief example which duplicates the behaviour I'm seeing:

import types

class C(object): pass
def one(a): return a
def two(a, b): return a+b
def bracketit(t): return '(%s)' % t

c = C()

for m in (one, two):
    new_method = lambda self, *args, **kwargs:\
            bracketit(m(*args, **kwargs))
    method = types.MethodType(new_method, c, C)
    setattr(C, m.__name__, method)

print c.one 
print c.two
print c.two(1, 2)
print c.one(1)

When I run this, I get the following output:

<bound method C.<lambda> of <__main__.C object at 0x1003b0d90>>
<bound method C.<lambda> of <__main__.C object at 0x1003b0d90>>
(3)
Traceback (most recent call last):
  File "/tmp/test.py", line 19, in <module>
    print c.one(1)
  File "/tmp/test.py", line 12, in <lambda>
    bracketit(m(*args, **kwargs))
TypeError: two() takes exactly 2 arguments (1 given)

I'm not sure why the two methods are bound in the same way. I haven't been able to find much documentation on how instancemethod binds methods to instances. What is going on underneath the hood, and how would I fix the above code so that the second call prints '(1)'?

Ian Stevens
  • 794
  • 4
  • 19

2 Answers2

3

The lambda is calling m, pulling it from the local scope. After the end of the for loop, m is set to two. Calling c.one or c.two will result in two being called.

You can tell that two is being called by looking at the last line of your traceback:

TypeError: two() takes exactly 2 arguments (1 given)

A good demonstration of what is going on can be found here.

This should do what you expect, but is a kinda messy:

class C(object): pass
def one(a): return a
def two(a, b): return a+b
def bracketit(t): return '(%s)' % t

c = C()

for m in (one, two):
    def build_method(m):
        return (lambda self, *args, **kwargs:
            bracketit(m(*args, **kwargs)))
    method = build_method(m)
    setattr(C, m.__name__, method)

print c.one 
print c.two
print c.two(1, 2)
print c.one(1)

I also removed the explicit creation of an unbound method, as it is unnessesary.

Mike Boers
  • 6,665
  • 3
  • 31
  • 40
3

The problem is the variable m is left as two at the end of the loop, and that affects the definitions made during the loop. You can fix it by creating closures with nested functions:

for m in (one, two):
    def make_method(m):
      def new_method(self, *args, **kwargs):
          return bracketit(m(*args, **kwargs))
      return new_method
    method = types.MethodType(make_method(m), c, C)
    setattr(C, m.__name__, method)

when run in your test code produces:

<bound method C.new_method of <__main__.C object at 0x0135EF30>>
<bound method C.new_method of <__main__.C object at 0x0135EF30>>
(3)
(1)
Ned Batchelder
  • 364,293
  • 75
  • 561
  • 662