Python变量覆盖陷阱


已经好几次碰到这样的错误了,每次碰到都花费我大量的时间,下面总结下我所犯的错误,希望对大家也有帮助。

闭包

我想抓取一系列的网页,抓取网页很慢,然后对网页内容进行处理,为了提高速度,我启动了多个线程去抓。以下是其代码:

  1. import sys   
  2. import threading   
  3. import time   
  4. urls = [ 'http://www.6688.cc''http://www.bkjia.com''http://www.bkjia.net' ]   
  5.   
  6. for url in urls:   
  7.     def _fetch():   
  8.         sys.stdout.write('fetch from: %s\n' % url)   
  9.         time.sleep(1)       # 模拟获取网页   
  10.         sys.stdout.write('process content from: %s\n' % url)   
  11.     threading.Thread(target=_fetch).start()  

代码很简单,对每个url都启动一个线程,线程启动时运行_fetch函数,_fetch函数是个闭包,因为它引用了循环变量url,我使用time.sleep()来模拟抓取网页的过程。我们期望url在每个_fetch函数内有不同值,且在函数内保持不变,但实际上不是,上述程序输出如下结果:

  1. fetch from: http://www.6688.cc   
  2. fetch from: http://www.bkjia.com   
  3. fetch from: http://www.bkjia.net   
  4. process content from: http://www.bkjia.net   
  5. process content from: http://www.bkjia.net   
  6. process content from: http://www.bkjia.net  

注意到process content全部输出的都是baidu。为什么会有这个结果?这是因为实际上每个_fetch引用的是同一个变量,随着循环的进行,url的值在不断的变化。线程刚启动时url值还没来得及改变,但是抓取网页完成后,循环已经结束了,url保持为最后一次循环的值,即http://www.bkjia.net。怎么解决这个问题呢?在_fetch函数内部先将url赋给一个局部变量的方式是有问题的:

  1. for url in urls:   
  2.     def _fetch():   
  3.         _url = url   
  4.         sys.stdout.write('fetch from: %s\n' % _url)   
  5.         time.sleep(1)       # 模拟获取网页   
  6.         sys.stdout.write('process content from: %s\n' % _url)   
  7.     threading.Thread(target=_fetch).start()  

虽然它的输出结果在我的机器上是正确的,但却有可能在其它机器上失败,这是因为线程的启动可能在这次循环体结束之后,这样有可能会抓取重复的url。一种方式是利用命名参数来保持当前循环时url值: 

  1. for url in urls:   
  2.     def _fetch(url=url):   
  3.         sys.stdout.write('fetch from: %s\n' % url)   
  4.         time.sleep(1)       # 模拟获取网页   
  5.         sys.stdout.write('process content from: %s\n' % url)   
  6.     threading.Thread(target=_fetch).start()  

这种方式起作用是因为每个函数会保持命令参数的默认值,每次循环时的url被保持在_fetch函数内,不带参数调用它时,url为函数本身保持的url默认值,而不是循环变量url。仅仅对这个示例,更简单的方式是直接将url传给线程的构造函数,但这种方式并不总有效。

  1. for url in urls:   
  2.     def _fetch(url):   
  3.         sys.stdout.write('fetch from: %s\n' % url)   
  4.         time.sleep(1)       # 模拟获取网页   
  5.         sys.stdout.write('process content from: %s\n' % url)   
  6.     threading.Thread(target=_fetch, args=(url,)).start()  

变量覆盖方法

我要写一个让用户输入验证码的引擎,验证码内容从一个url处获得,两次让用户输入验证码。以下是其代码:

  1. class Engine(object):   
  2.     def captcha(self, url):   
  3.         '''''从url处获得验证码'''  
  4.         # get captcha from url   
  5.         # ...   
  6.         self.captcha = raw_input('Enter the captcha: ')   
  7.         return self  
  8.   
  9. e = Engine()   
  10. e.captcha('http://website/captcha')     # first time   
  11. print 'You entered captcha: %s' % e.captcha   
  12. e.captcha('http://website/captcha')     # second time   
  13. print 'You entered captcha: %s' % e.captcha  

代码看起来很正常,但运行时却显示错误:

  1. Enter the captcha: <<ea859>>        # 输入验证码   
  2. You entered captcha: <<ea859>>   
  3. Traceback (most recent call last):   
  4.   File "test.py", line 11in <module>   
  5.     e.captcha('http://website/captcha')     # second time   
  6. TypeError: 'str' object is not callable  

异常是在第二次调用e.captcha()方法时出现的,错误很令人莫名其妙。问题原因是我们在第6行将用户输入结果保存在self.captcha中了,它和方法名字一样,所以第一次调用完成之后,e.capcha就变成用户输入的字符串了,所以在第11行再次调用e.captcha()方法时,它实际上调用str.__call__()方法,而str没有这个方法,所以就出现上面的异常。有Java,C++或者C#背景的人比较容易上犯这个错误,在这些语言中变量和方法属于不同的命名空间,一方不会覆盖另一方。而在python等函数编程语言中,函数就是第一类对象,不再区分方法和变量,同名的变量会覆盖同名的方法,反之亦然。解决方法很简单,将captcha方法重命名为get_captcha就可以了。

  1. class Engine(object):   
  2.     def get_captcha(self, url):   
  3.         '''''从url处获得验证码'''  
  4.         # get captcha from url   
  5.         # ...   
  6.         self.captcha = raw_input('Enter the captcha: ')   
  7.         return self  
  8.   
  9. e = Engine()   
  10. e.get_captcha('http://website/captcha')     # first time   
  11. print 'You entered captcha: %s' % e.captcha   
  12. e.get_captcha('http://website/captcha')     # second time   
  13. print 'You entered captcha: %s' % e.captcha  
  • 1
  • 2
  • 下一页

相关内容