设为首页 收藏本站
查看: 787|回复: 0

[经验分享] python变量覆盖陷阱

[复制链接]

尚未签到

发表于 2017-4-25 11:40:16 | 显示全部楼层 |阅读模式
我已经好几次碰到这样的错误了,每次碰到都花费我大量的时间,下面总结下我所犯的错误,希望对大家也有帮助。
闭包
我想抓取一系列的网页,抓取网页很慢,然后对网页内容进行处理,为了提高速度,我启动了多个线程去抓。以下是其代码:

import sys
import threading
import time
urls = [ 'http://www.google.com', 'http://www.sina.com.cn', 'http://www.baidu.com' ]
for url in urls:
def _fetch():
sys.stdout.write('fetch from: %s\n' % url)
time.sleep(1)       # 模拟获取网页
sys.stdout.write('process content from: %s\n' % url)
threading.Thread(target=_fetch).start()

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

fetch from: http://www.google.com
fetch from: http://www.sina.com.cn
fetch from: http://www.baidu.com
process content from: http://www.baidu.com
process content from: http://www.baidu.com
process content from: http://www.baidu.com

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

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

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

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

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

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


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

class Engine(object):
def captcha(self, url):
'''从url处获得验证码'''
# get captcha from url
# ...
self.captcha = raw_input('Enter the captcha: ')
return self
e = Engine()
e.captcha('http://website/captcha')     # first time
print 'You entered captcha: %s' % e.captcha
e.captcha('http://website/captcha')     # second time
print 'You entered captcha: %s' % e.captcha

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

Enter the captcha: <<ea859>># 输入验证码
You entered captcha: <<ea859>>
Traceback (most recent call last):
File "test.py", line 11, in <module>
e.captcha('http://website/captcha')     # second time
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就可以了。

class Engine(object):
def get_captcha(self, url):
'''从url处获得验证码'''
# get captcha from url
# ...
self.captcha = raw_input('Enter the captcha: ')
return self
e = Engine()
e.get_captcha('http://website/captcha')     # first time
print 'You entered captcha: %s' % e.captcha
e.get_captcha('http://website/captcha')     # second time
print 'You entered captcha: %s' % e.captcha


list comprehension
我要导入一个联系人列表,每个联系人有name, mobile, address, im, 并且可能有多个备份mobile,假设每个字段以空格分开,多个备份mobile之前以逗号分开。以下是其代码:

from StringIO import StringIO
def is_mobile(mobile):
return len(mobile) == 11
def import_contacts(file):
for line in file.readlines():
parts = line.strip().split()
name = parts[0]
mobile = parts[1]
address = parts[2]
im = parts[3]
backup_mobiles = [ mobile for mobile in parts[4].split(',') if is_mobile(mobile) ]
print 'importing contact: %s, mobile=%s' % (name, mobile)
import_contacts(StringIO('''marlon 13511002222 beijing marlon@gmail.com 13711112222,13822224444'''))

你可能会想结果会输出:

importing contact: marlon, mobile=13511002222

但实际输出:

importing contact: marlon, mobile=13822224444

这是因为第12行使用了list comprehension,其中使用变量名称和第9行使用的变量名称相同,都为mobile,在python中这两个是同一个变量,使用list comprehension会改变mobile,最终结果为backup_mobiles最后一个元素的值。有Java, C++等背景的人也容易犯这个错误,因为很容易将list comprehension理解成:

List<String> backup_mobiles = new ArrayList<String>();
for (String mobile: parts[4].split(",")) {
backup_mobiles.add(mobile);
}

而在这些语言中,mobile属于for循环里的局部变量,不会覆盖外面的同名变量(只是会隐藏)。但在python中不同,两者是同一个变量,解决方法很简单将list comprehension中的mobile变量改成短名m,就像循环变量通常使用短名一样,list comprehension也最好使用短名,表示它们的作用域很小,不容易覆盖外围变量(或者覆盖了也没事)。

from StringIO import StringIO
def is_mobile(mobile):
return len(mobile) == 11
def import_contacts(file):
for line in file.readlines():
parts = line.strip().split()
name = parts[0]
mobile = parts[1]
address = parts[2]
im = parts[3]
backup_mobiles = [ m for m in parts[4].split(',') if is_mobile(m) ]
print 'importing contact: %s, mobile=%s' % (name, mobile)
import_contacts(StringIO('''marlon 13511002222 beijing marlon@gmail.com 13711112222,13822224444'''))


(完)

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-369075-1-1.html 上篇帖子: 转 可爱的 Python:Python中的文本处理 下篇帖子: Python 学习入门(7)—— lambda
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表