那些年在使用python过程中踩的一些坑。
Python是一门功能非常强大,语法也比较简单的编程语言。在使用Python的过程中本人深深地感受到这门语言的魅力。
即便如此,本人在编程的过程中依旧踩到了一些坑。这里将它们简单总结起来,希望可以帮助一些新人规避这些问题。
当然最好的办法还是在学习语言的过程中更留意语法细节。:)
-
注意你所使用的数据类型:对不同数据类型执行同一个操作可能会得到不同结果:
Python共有两种数据类型:
- 可变数据类型(列表,字典,集合)
- 不可变数据类型(数值,字符串,元组)
在Python世界中,一切变量皆为对象的引用。
变量引用一块内存地址中存储的值,它本身并不保存值。
对于不可变数据类型来说,不能通过变量来修改这个值。
举个例子:>>> a = "hello" >>> id(a) # 返回地址 1582645074160 >>> b = "hello" >>> id(b) 1582645074160 >>> b = "what" >>> id(b) 1582645072760 >>> id(a) 1582645074160
a和b都同样引用一个字符串对象"hello",该值保存在内存地址4160中。但当给b赋新值"what"时,由于不能改变当前值"hello",系统会分配给它另外一个地址2760,该地址保存"what"。于是b和a不再指向同一个地址的值。
还需要注意的一点是,多个变量引用同一个对象时,若要给其中一个变量赋另外一个值,实际上并不是改变原来地址里保存的值,而是重新开辟一块内存空间给它,于是修改其中一个变量不会影响其它变量的取值。>>> a = 10 >>> b = a >>> b += 2 >>> print(a,b) 10 12
(对于元组来说,相同的元组可能地址不同,但它是只读的,所以依然是不可变类型)
对于可变数据类型来说,可以通过变量修改值>>> a = [1,2,3] >>> id(a) 2763060425160 >>> b = [1,2,3] >>> id(b) 2763062998728 >>> b.append(4) >>> id(b) 2763062998728
a和b同样引用列表[1,2,3],但可以看出这两个相同的列表地址却是不同的。因此对于可变数据类型来说,具有相同值的对象不是同一个对象。并且对b做出修改以后,b的地址并没有变。也就是说,可变数据类型允许修改当前地址下保存的值。
于是乎,可变数据类型会带来下面一个坑:>>> a = [1,2,3] >>> b = a >>> b.append(4) >>> print(a,b) [1, 2, 3, 4] [1, 2, 3, 4]
当a和b引用同一个可变类型对象时,改变其中一个,另一个也会随之发生改变,因为它们引用同一个地址的值。
总结:
不可变类型相同值保存地址相同(元组除外),修改变量时并不改变当前地址保存的值,而是给它一个新的地址。当多个变量引用同一个值时,改变其中一个变量的取值不影响其他变量。
可变类型相同值保存地址可能不同,可以通过变量修改当前地址保存的值。当多个变量指向同一个值时,改变其中一个变量的取值,其他变量会跟着一起改变。 -
函数默认参数值不要使用可变数据类型
先来看一段代码:
class Test(object): def __init__(self,lst=[]): self.lst = lst if __name__ == "__main__": a = Test() a.lst.append(1) a.lst.append(2) b = Test() print(a.lst) print(b.lst)
定义一个Test类,并定义其构造函数的默认参数lst为一个列表。看着挺好的,然后我们创建两个对象a和b。修改a中的字段lst,向里面添加两个元素,b保持默认状态就好。
现在执行这段代码,输出a和b中lst的值。[1, 2] [1, 2]
哈?为什么b也跟着变了?
参数的默认值只在定义函数时定义一次。正如在第一个问题中提到过的,对可变类型可以直接进行修改,并不会影响对它的引用。也就是说,如果调用函数不传参数而是使用默认值,那么每次调用这个函数时都只会使用这一个可变类型对象。当对a.lst进行append的时候,这个列表被修改了,于是创建b时默认使用的还是这个列表,导致b.lst的值与a相同。
解决方案有两个:- 不使用默认参数(不推荐)
在创建b时不使用默认参数而是明确指定一个列表[4,5,6],这样可以规避上述问题,但是......如果不使用,这个默认参数的意义何在呢?class Test(object): def __init__(self,lst=[]): # 我要这默认值有何用 self.lst = lst if __name__ == "__main__": a = Test() a.lst.append(1) a.lst.append(2) b = Test([4,5,6]) print(a.lst) print(b.lst)
- 在函数内定义可变数据类型(推荐)
这样即可以两全其美解决问题:如果使用默认参数,则None会被传入构造函数中。这样一来每次执行都会创建一个新的列表。我们已经知道创建可变类型对象时即便值和之前的对象相等也不是同一个对象,于是互相之间不会影响彼此了。class Test(object): def __init__(self,lst=None): if lst is None: self.lst = [] else: self.lst = lst if __name__ == "__main__": a = Test() a.lst.append(1) a.lst.append(2) b = Test() print(a.lst) print(b.lst) [1, 2] []
- 不使用默认参数(不推荐)
-
浅拷贝与深拷贝
Python中的拷贝共分为三种:赋值拷贝,浅拷贝,深拷贝。
赋值拷贝就是“=”做的事情:>>> a = [1,2,3] >>> b = a >>> b[0] = 4 # 对b中元素进行修改 >>> print(a) [4, 2, 3]
将a赋值给b的意思是“b现在引用的对象和a相同了”,也就是说,并没有任何新对象产生,两个变量引用同一个地址里的值罢了。
如果希望让b和a互相不干扰怎么办呢?可以使用python中的切片操作:>>> a = [1,2,3] >>> b = a[:] >>> b[0] = 4 # 对b中元素进行修改 >>> print(a) [1, 2, 3]
此时再改变b就和a没有关系了。切片操作即是对a做了浅拷贝,此时a与b的地址也不一样了:
>>> id(a) # a与b的地址不同 2870775936712 >>> id(b) 2870775936648
python中的浅拷贝包括:
- 切片操作
- copy模块的copy函数
- 对象的copy函数
- ...
好了,巨坑要来了。当你认为这就是真正的复制时:>>> a = [1,2,3,[4,5]] >>> b = a[:] >>> id(a) # a和b地址不同 2870773363144 >>> id(b) 2870775950280 >>> b[3].append(6) # 对b中元素进行修改 >>> print(a) [1, 2, 3, [4, 5, 6]] # a怎么又变了?? >>> id(a[3]) # a和b中的元素子列表地址相同 2870775936712 >>> id(b[3]) 2870775936712
当改变列表中的子列表[4,5]时,a的值又随着b改变了。这是为什么呢?
所谓浅拷贝,实际上是“创建一个新的对象,这个对象内部元素与之前一样”,也就是说,a和b指向两个不同对象,但这两个对象的内部元素都相同!所以对于a和b来说,尽管他俩本身的地址不同,但是他们内部的元素地址是一样的。
此时,列表里面有两种类型,一个是整数类型的1,2和3,还有一个列表[4,5],前面已经说过,整数类型是不可变类型,所以修改b中的整数类型对a没有影响,但是如果修改b中的子列表[4,5],由于两个列表元素的地址都一样,b的改动势必会影响a。如何彻彻底底地复制拷贝一个对象呢,答案是深拷贝:
>>> from copy import deepcopy >>> a = [1,2,3,[4,5,6]] >>> b =deepcopy(a) >>> id(a) # a和b地址不同 2870775950280 >>> id(b) 2870775936584 >>> b[3].append(7) >>> print(a) [1, 2, 3, [4, 5, 6]] # b:这回拜拜了您
这回a和b是妥妥的互不干扰了,因为这次不仅列表对象本身是新的,对象里面的元素也都是新的。
>>> id(a[0]) # a和b中的首元素1地址相同 1943694400 >>> id(b[0]) 1943694400 >>> id(a[3]) # a和b中的尾元素子列表[4,5,6]地址不同 2870775950344 >>> id(b[3]) 2870775961864
有趣的是,尽管子列表地址不再相同,对于第一个元素1来说地址依旧相同。但是没关系啊,因为1是不可变类型。深拷贝只拷贝可变类型元素,而不可变类型元素本身就互不干扰,也就不需要拷贝。
以上是我在使用Python中遇到过的坑,之后会持续更新,希望能帮助到大家。