动态绑定Python方法到实例时正确绑定了方法名,但方法未绑定

4 投票
2 回答
2060 浏览
提问于 2025-04-15 16:27

我正在为一组RESTful服务编写一个客户端。这些REST调用的请求体有相同的XML结构,具体取决于参数。总共有几十个调用,我并不会实现所有的功能。因此,我想让它们更容易指定和使用。这些REST方法根据功能分成不同的模块,并且需要共享同一个urllib2的打开器来进行身份验证和处理Cookies。下面是一个方法声明的例子:

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

开发者只需要关注验证部分。XML的格式(对于POST和PUT)或URL(对于GET和DELETE)以及响应的反序列化都是通过辅助方法来完成的。被装饰的方法会被收集到一个客户端对象中,然后从这个对象中执行和处理。例如:

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

代码已经写好了。唯一的问题是在将被装饰的方法附加到客户端类时。虽然在客户端实例中可以看到所有被装饰的方法,但它们都共享同一个定义,也就是最后一个被绑定的方法。下面是一个简短的例子,展示了我所遇到的行为:

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)

当我运行这个时,我得到了以下输出:

<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)

我不太明白为什么这两个方法的绑定方式是一样的。我没有找到很多关于实例方法如何将方法绑定到实例的文档。到底发生了什么,我该如何修复上面的代码,以便第二个调用能打印出'(1)'?

2 个回答

3

问题在于,变量 m 在循环结束时仍然是 two,这会影响在循环中做出的定义。你可以通过创建闭包和嵌套函数来解决这个问题:

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)

在你的测试代码中运行时,会产生:

<bound method C.new_method of <__main__.C object at 0x0135EF30>>
<bound method C.new_method of <__main__.C object at 0x0135EF30>>
(3)
(1)
3

这个 lambda 表达式在调用 m,它是从本地的作用域中获取的。在 for 循环结束后,m 被设置为 two。所以当你调用 c.onec.two 时,实际上是调用了 two

你可以通过查看错误信息的最后一行来确认 two 被调用了:

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

这里有一个很好的示例,能帮助你理解发生了什么。

这个代码应该能按你的预期工作,但看起来有点乱:

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)

我还去掉了不必要的未绑定方法的创建,因为那样做其实没必要。

撰写回答