用可变对象作为参数默认值的问题
使用可变类型对象(列表、字典、集合),作为函数参数的默认值:
def add_END(t = []):
t.append('END')
return t
if __name__ == '__main__':
x = add_END()
print('x:', x)
y = add_END()
print('y:', y)
z = add_END()
print('z:', z)
x: ['END']
y: ['END', 'END']
z: ['END', 'END', 'END']
如果函数的参数默认值是可变对象(列表、集合、字典等),该默认值在程序运行之初的“编译阶段”就已经被创建,并一直保留在内存中。
所以 每次调用函数,修改的都是同一默认值对象。
最好不要使用列表、集合等可变容器作为参数的默认值。
容器复制与浅拷贝和深拷贝的问题
a = ['A', 'B', 'C']
b = a
b[0] = '甲'
a
['甲', 'B', 'C']
浅拷贝
a = ['A', 'B', 'C']
b = a.copy()
b[0] = '甲'
a
['A', 'B', 'C']
a = ['A', ['B', 'C'], 'D']
b = a.copy()
b[1][0] = '乙'
a
['A', ['乙', 'C'], 'D']
如果变量a包含多层列表(或字典等其他容器),a.copy()方法只会复制 第一层 的内容!
a = {'A':['一', '甲'], 'B':['二', '乙']}
b = a.copy()
b['B'][0] = 'Two'
a
{'A': ['一', '甲'], 'B': ['Two', '乙']}
深拷贝
from copy import deepcopy
a = ['A', ['B', 'C'], 'D']
b = deepcopy(a)
b[1][0] = '乙'
a
['A', ['B', 'C'], 'D']
a = {'A':['一', '甲'], 'B':['二', '乙']}
b = deepcopy(a)
b['B'][0] = 'Two'
a
{'A': ['一', '甲'], 'B': ['二', '乙']}
使用列表乘法创建二维列表的问题
a = [0] * 3
a
[0, 0, 0]
b = [a] * 3
b
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]
b[0][1] = 999
b
[[0, 999, 0], [0, 999, 0], [0, 999, 0]]
内置函数id(x)返回变量ⅹ的对象标识,一般为内存地址。
id(b[0]) # b中第1个元素[0, 9990]的内存地址
1612616627336
id(b[1]) # b中第2个元素[0, 9990]的内存地址
1612616627336
id(b[2]) # b中3个元素[0, 9990]的内存地址
1612616627336
id(a) # 列表a的内存地址
1612616627336
列表乘法执行的是浅拷贝,子列表都指向同一内存
可以使用列表生成式创建二维列表
b = [[0] * 3 for i in range(3)]
b
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]
b[0][1] = 999
b
[[0, 999, 0], [0, 0, 0], [0, 0, 0]]
因为[0]中没有子列表,所以[0]*3并不涉及列表乘法的浅拷贝问题。
只有存在子列表(或字典等其他可变容器)时,才需要考虑浅拷贝问题。
“_”只为控制循环次数
b = [[0] * 3 for _ in range(3)]
b
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]
练习
练习1:深拷贝与浅拷贝
请阅读下面的操作,推测它们的运行结果,然后在 IDLE 中实际运行并思考原因:
1.
a = [ 1, 'abc', [1,2,3] ]
b = a.copy()
b[1] = '甲乙丙'
print(b)
print(a)
2.
a = [ 1, 'abc', [1,2,3] ]
b = a.copy()
b[2] = ['一','二','三']
print(b)
print(a)
3.
a = [ 1, 'abc', [1,2,3] ]
b = a.copy()
b[2][1] = 500
print(b)
print(a)
4.
a = [ 1, 'abc', [1,2,3] ]
import copy
b = copy.deepcopy(a)
b[2][1] = 500
print(b)
print(a)
a = [ 1, 'abc', [1,2,3] ]
b = a.copy()
b[1] = '甲乙丙'
print(b)
print(a)
[1, '甲乙丙', [1, 2, 3]]
[1, 'abc', [1, 2, 3]]
a = [ 1, 'abc', [1,2,3] ]
b = a.copy()
b[2] = ['一','二','三']
print(b)
print(a)
[1, 'abc', ['一', '二', '三']]
[1, 'abc', [1, 2, 3]]
a = [ 1, 'abc', [1,2,3] ]
b = a.copy()
b[2][1] = 500
print(b)
print(a)
[1, 'abc', [1, 500, 3]]
[1, 'abc', [1, 500, 3]]
a = [ 1, 'abc', [1,2,3] ]
import copy
b = copy.deepcopy(a)
b[2][1] = 500
print(b)
print(a)
[1, 'abc', [1, 500, 3]]
[1, 'abc', [1, 2, 3]]
本例考察大家对深拷贝和浅拷贝的理解,实际结果和原因如下:
b 的第二个元素会被改为 ‘甲乙丙’,而 a 不变。因为 b 是 a 的拷贝,所以拷贝结束后, a 和 b 的第二个元素都是指向字符串 ‘abc’ 的地址数字;而在执行 b[1]=’甲乙丙’ 后,b 中第二个元素变成了指向字符串 ‘甲乙丙’ 的地址数字,而 a 没有受到影响。
b 的第三个元素(列表[1,2,3])都会被改为 [‘一’,’二’,’三’] ,而 a 没有任何变化。因为 b 是 a 的拷贝,所以拷贝结束后, a 和 b 的第三个元素(即a[2] 和 b[2])是两个相等的数字,都是列表 [1,2,3] 的地址数字;而 b[2]=[‘一’,’二’,’三’] 这个命令,让 b[2] 中的地址数字改为了列表 [‘一’,’二’,’三’] 的地址,但是并没有修改 a[2] 的数字。所以修改之后,b[2]指向了[‘一’,’二’,’三’],而 a[2] 仍然保持不变、指向 [1,2,3]。
a 和 b 的第三个元素(列表[1,2,3])都会被改为 [1,500,3] 。因为这一次修改的不是 b[2] ,而是 b[2][1] 。因此这个命令并没有修改 b[2] 的内容,而是修改了 b[2] 所指向的列表中的第二个元素(即b[2][1])。所以修改之后,a[2]和b[2]仍然都是指向同一个列表,但该列表里面的元素被改为500。
b 的第三个元素(列表[1,2,3])都会被改为 [1,500,3] ,而 a 不会有任何变化 。因为这一次 b 是 a 的深拷贝,所以a中的子列表 [1,2,3] 也被复制出一份,由 b[2] 指向。因此修改 b[2][1] 时只会影响到 b 中新拷贝出来的 [1,2,3] ,而与 a 中原版的子列表 [1,2,3] 没有任何关系。
练习2:创建二维列表
请判断下面两段操作的结果,然后在 idle 中实际运行并思考原因:
1.
a = [ 1, 2, 3 ]
b = a * 3
print(b)
b = [ a ] * 3
print(b)
print(b[1])
2.
a = [ 1, 2, 3 ]
b = [ a ] * 3
b[0][1] = 500
print(b)
print(a)
3.
b = [ [1,2,3] for _ in range(3) ]
print(b)
b[0][1] = 500
print(b)
4.
a = [1,2,3]
b = [ a for _ in range(3) ]
print(b)
b[0][1] = 500
print(b)
a = [ 1, 2, 3 ]
b = a * 3
print(b)
b = [ a ] * 3
print(b)
print(b[1])
[1, 2, 3, 1, 2, 3, 1, 2, 3]
[[1, 2, 3], [1, 2, 3], [1, 2, 3]]
[1, 2, 3]
a = [ 1, 2, 3 ]
b = [ a ] * 3
b[0][1] = 500
print(b)
print(a)
[[1, 500, 3], [1, 500, 3], [1, 500, 3]]
[1, 500, 3]
b = [ [1,2,3] for _ in range(3) ]
print(b)
b[0][1] = 500
print(b)
[[1, 2, 3], [1, 2, 3], [1, 2, 3]]
[[1, 500, 3], [1, 2, 3], [1, 2, 3]]
a = [1,2,3]
b = [ a for _ in range(3) ]
print(b)
b[0][1] = 500
print(b)
print(a)
[[1, 2, 3], [1, 2, 3], [1, 2, 3]]
[[1, 500, 3], [1, 500, 3], [1, 500, 3]]
[1, 500, 3]
本例考察大家对二维列表生成式的理解,实际结果和原因如下:
b=a3 会创建一个新的列表赋值给 b ,而这个列表包含 9 个元素,也就是 a 中元素复制三次的结果。b=[a]3 也会创建一个新的列表赋值给 b ,但这次被复制三次的是 [a] 中的元素,也就是 a 。所以 b 的实际构成是 [ a, a, a ] ,也就是 [ [1,2,3],[1,2,3],[1,2,3] ] 。因此 b[1] 就是一个 [1,2,3] 。
b=[a]*3 的含义见上题,需要注意的是,由于 b 实质上是 [a,a,a] ,而三个 a 都指向同一个列表 [1,2,3] ,所以即使只修改了 b[0] (即 b 中第一个 a)的[1]元素 ,也相当于同时修改了 b[1] 和 b[2] 以及 a 。
在这个列表生成式中,每次循环都向 b 中添加了一个新建的 [1,2,3] ,也就是每次添加的 [1,2,3] 都是不同的内存对象,所以最终 b[0] b[1] 和 b[2] 所指向的是三个不同的列表,只不过它们的内容暂时相同,都是 1,2,3 三个元素构成。而当修改了 b[0][1] 后,也只有 b[0] 所指向的 [1,2,3] 被修改,b[1]和b[2]两个列表不变。
与上一题不同,这个列表生成式中每次循环向 b 中添加的不是新建的 [1,2,3] ,而是变量 a 所保存的列表地址。因此向 b 中添加的三个列表地址都相同,即 a 所指向的那一个 [1,2,3] 。因而修改了 b[0][1] 后,相当于同时修改了 b[1] 和 b[2] 指向的列表。