Python中HttpParallelHandler并行请求回调索引始终为49的问题排查
你遇到的这个问题是Python中lambda延迟绑定特性导致的典型坑,我来一步步帮你拆解原因和解决办法。
首先回顾你的场景:在for循环里给每个HTTP请求注册回调lambda,期望每个回调能拿到对应的循环索引,但结果所有回调都输出了最后一个索引值49——哪怕你尝试用copy.copy(index)也没用。
问题根源:Lambda的延迟绑定
Python中的lambda表达式对变量的绑定是延迟的:它不会在定义lambda的时候就捕获变量的当前值,而是在lambda被调用的时候,才会去查找这个变量的当前值。
在你的循环里,index是一个循环变量,每次循环它都会被更新为新的值。当所有的HTTP请求完成、回调lambda被执行时,循环已经结束了,此时index的值已经变成了49(循环最后一次的索引)。所以所有lambda在调用时,都会去取这个已经变成49的变量值,导致输出全是49。
至于你尝试的copy.copy(index)没用,是因为index是整数——Python的整数是不可变类型,copy.copy对于不可变对象来说,返回的还是同一个对象的引用,本质上和直接用index没有区别。循环中index被重新赋值为新的整数对象,但lambda里引用的还是变量index本身,而不是当时的数值。
解决方案:提前捕获变量值
有几种简单的方法可以让lambda在定义时就捕获当前的index值,避免延迟绑定的问题:
方法1:利用lambda的默认参数
这是最常用的解决方案,通过给lambda添加一个默认参数,让它在定义时就把当前的index值保存下来:
if __name__ == '__main__': def onsuccess(html_code, index): print "Success: html received. index: " + str(index) def onerror(e): print "Error: " + str(e) http_handler = ParallelHttpHandler() urls = ["https://google.com?count=%d" % n for n in range(50)] for index, url in enumerate(urls): # 通过默认参数idx=index,在lambda定义时捕获当前index的值 http_handler.http_get(url, None, None, lambda html_code, idx=index: onsuccess(html_code, idx), onerror) http_handler.wait_all()
默认参数的求值时机是在lambda定义的时候,所以每个lambda实例都会持有自己的idx参数,对应循环当时的index值,不会被后续循环的变量更新影响。
方法2:使用functools.partial绑定参数
另一种方法是用functools.partial来提前绑定onsuccess函数的index参数:
import functools if __name__ == '__main__': def onsuccess(html_code, index): print "Success: html received. index: " + str(index) def onerror(e): print "Error: " + str(e) http_handler = ParallelHttpHandler() urls = ["https://google.com?count=%d" % n for n in range(50)] for index, url in enumerate(urls): # 用partial提前绑定index参数 success_callback = functools.partial(onsuccess, index=index) http_handler.http_get(url, None, None, success_callback, onerror) http_handler.wait_all()
functools.partial会在创建时就把指定的参数值固定下来,生成一个新的可调用对象,这样每个回调都会持有当时的index值。
方法3:封装循环体为辅助函数
你也可以把循环里的逻辑封装成一个小函数,利用函数的局部变量来捕获index值:
if __name__ == '__main__': def onsuccess(html_code, index): print "Success: html received. index: " + str(index) def onerror(e): print "Error: " + str(e) def register_request(http_handler, url, index): # 这里的index是函数的局部变量,每个调用都会保存当前的循环值 http_handler.http_get(url, None, None, lambda html_code: onsuccess(html_code, index), onerror) http_handler = ParallelHttpHandler() urls = ["https://google.com?count=%d" % n for n in range(50)] for index, url in enumerate(urls): register_request(http_handler, url, index) http_handler.wait_all()
每次调用register_request时,函数内部的index是独立的局部变量,lambda引用的是这个局部变量,不会被循环后续的index更新影响。
以上几种方法都能解决你的问题,其中方法1是最简洁直观的,推荐优先使用。
内容的提问来源于stack exchange,提问作者VanDir

