Python培训
400-996-5531
本篇文章会以简单代码来描述我们在写Python代码时经常会写的一些语法,而没有思考过它们会带来的麻烦(bug),它们不算是语法错误,只是因为对Python机制的理解不够而导致的问题,文章会提供一些更妥善的写法,但不一定是最好的解决方案,这要具体情况具体分析,有时候不好的写法也是一种需求。
引用式变量
>>> a = [1, 2, 3]>>> b = a>>> a.append(4)>>> b[1, 2, 3, 4]>>> id(a), id(b)(34789128L, 34789128L)>>> b is aTrue>>> b == aTrue>>>
Python变量是一种引用式变量,对引用式变量来说,说把变量分配给对象更合理,反过来说就有问题。因为对象在赋值之前就已经创建了。
对于例子程序,是先创建了[1, 2, 3]列表对象,然后才将变量a绑定到这个列表对象上,简单的变量赋值b = a不会创建这个列表对象的副本,而是直接将b也绑定到了这个列表对象上,所以当我对变量a进行操作的时候就会改变变量b的值。
我们可以通过is运算符和id函数来确定变量是否是同一个引用,==运算符仅仅是用来比较两个变量的值是否相等。
相对不可变性
>>> t1 = (1, 2, [30, 40])>>> id(t1[-1])35898568L>>> t1[-1].append(99)>>> t1(1, 2, [30, 40, 99])>>> id(t1[-1])35898568L>>>
相对不可变性是对元组数据类型而言的,元组是不可变的数据类型指的是元组中每个对象的数值标识(ID)不会变,而不是对象本身不可变。这也是有些元组不可散列的原因。
对于例子程序,t1[-1]的id是不可变的,但它存储的是一个可变的列表,元组的值会随着引用的可变对象的变化而变化。
浅复制
>>> l1 = [3, [55, 44], (7, 8, 9)]>>> l2 = list(l1)>>> l2[3, [55, 44], (7, 8, 9)]>>> l3 = l1[:]>>> l3[3, [55, 44], (7, 8, 9)]>>> id(l1), id(l2), id(l3)(35916296L, 35916040L, 35899720L)>>> id(l1[-1]), id(l2[-1]), id(l3[-1])(35056376L, 35056376L, 35056376L)>>>
复制列表有两种简单的浅复制的方法,一种通过构造方法list(),一种通过分片的方式[:],例子程序中给出了这两种浅复制的方法,但什么叫浅复制呢?对应的应该有深复制?对的,有浅必有深,你懂的。所谓的浅复制,从例子程序可以看出来,只是复制了列表的引用,而列表中对象的引用依然未变。如果所有元素都是不可变的,那么这样没有问题,还能节省内存。但是如果有可变的元素,可能会导致意想不到的问题。
大家思考一下下面的代码输出结果,如果你的思考结果和注释结果相同,那么相信你就应该理解浅复制的概念了。
l1 = [3, [66, 55, 44], (7, 8, 9)]l2 = list(l1)l1.append(100)l1[1].remove(55)print("l1:", l1)print("l2:", l2)l2[1] += [33, 22]l2[2] += (10, 11)print("l1:", l1)print("l2:", l2)# l1: [3, [66, 44], (7, 8, 9), 100])# l2: [3, [66, 44], (7, 8, 9)])# l1: [3, [66, 44, 33, 22], (7, 8, 9), 100])# l2: [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)])
因为浅复制会带来的问题,下面就来说明深复制的技巧。副本不共享内部对象的引用就叫深复制。copy模块提供的deepcopy和copy函数能为任意对象做深复制和浅复制。
0>>> import copy>>> l1 = [3, [55, 44], (7, 8, 9)]>>> l2 = copy.copy(l1)>>> l2[3, [55, 44], (7, 8, 9)]>>> l3 = copy.deepcopy(l1)>>> l3[3, [55, 44], (7, 8, 9)]>>> id(l1), id(l2), id(l3)(35898376L, 35963784L, 35916040L)>>> id(l1[-1]), id(l2[-1]), id(l3[-1])(35910208L, 35910208L, 35910208L)>>> id(l1[1]), id(l2[1]), id(l3[1])(35364552L, 35364552L, 35914824L)>>>
对于例子程序中,如果列表中对象是可变数据类型,通过deepcopy函数会将可变对象的引用复制成副本。
共享传参
>>> def f(a, b):... a += b... return a...>>> a = [1, 2]>>> b = [3, 4]>>> f(a, b)[1, 2, 3, 4]>>> a, b([1, 2, 3, 4], [3, 4])>>>
共享传参是指函数的形参将获得实参中引用的副本,如果实参传入的是可变对象,函数将可能会修改这个可变对象。例子程序中演示了这一结果。这不能算错误,这取决于你的需求,如果你确实想通过这个方法来修改通过参数传入的对象,但一定要三思。
可变默认值
通过共享传参就引申到可变默认值这个话题,记住一句Python金句:不要使用可变类型作为参数的默认值。最经典一个案例就是将空列表[]作为默认值传给形参带来的诡异结果。来看例子程序,这里自定义一个PublicBus的类,模拟乘客上公交车的情形,提供上车和下车的功能,默认车是一辆空车。
class PublicBus(object): def __init__(self, passengers=[]): self.passengers = passengers def pick(self, name): self.passengers.append(name) def drop(self, ame): self.passengers.remove(name)bus1 = PublicBus()bus1.pick("Andy")print(bus1.passengers)bus2 = PublicBus()print(bus2.passengers)print(id(bus1.passengers), id(bus2.passengers))bus2.pick("Joseph")print(bus1.passengers)print(bus2.passengers)# ['Andy']# ['Andy']# (32797832L, 32797832L)# ['Andy', 'Joseph']
可怕的事情像预期的一样发生,我定义了两辆空车bus1和bus2,而bus1上车的乘客和bus2上车的乘客共享了,也就是说两辆车的乘客永远是相同的。
这显然是不符合逻辑的,其中的原因解释起来也很简单,没有指定初始乘客的PublicBus实例会共享同一个乘客列表,这是因为不指定乘客的时候,self.passengers变成了passengers参数默认值的引用了。
出现这个问题的根源是,默认值在定义函数时计算(通常在加载模块时),如果默认值是可变对象,修改它的值会对后续函数调用都会受到影响。
正确的做法是以None作为默认值,让公交车自己维护乘客列表。
class PublicBus(object): def __init__(self, passengers=None): if passengers is None: self.passengers = [] else: self.passengers = list(passengers) def pick(self, name): self.passengers.append(name) def drop(self, name): self.passengers.remove(name)bus1 = PublicBus()bus1.pick("Andy")print(bus1.passengers)bus2 = PublicBus()print(bus2.passengers)print(id(bus1.passengers), id(bus2.passengers))bus2.pick("Joseph")print(bus1.passengers)print(bus2.passengers)# ['Andy']# []# (31134216L, 31094024L)# ['Andy']# ['Joseph']
填写下面表单即可预约申请免费试听! 怕学不会?助教全程陪读,随时解惑!担心就业?一地学习,可全国推荐就业!
Copyright © 京ICP备08000853号-56 京公网安备 11010802029508号 达内时代科技集团有限公司 版权所有
Tedu.cn All Rights Reserved