python随机抽样 bug_解决Python程序不易察觉的几个Bug

本篇文章会以简单代码来描述我们在写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']

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值