容器类型与整数相比,内存结构要复杂得多。
image.png 拷贝容器细分深与浅,可变对象难做默认值 - 图2

用可变对象作为参数默认值的问题

使用可变类型对象(列表、字典、集合),作为函数参数的默认值:

  1. def add_END(t = []):
  2. t.append('END')
  3. return t
  4. if __name__ == '__main__':
  5. x = add_END()
  6. print('x:', x)
  7. y = add_END()
  8. print('y:', y)
  9. z = add_END()
  10. print('z:', z)
  1. x: ['END']
  2. y: ['END', 'END']
  3. z: ['END', 'END', 'END']

如果函数的参数默认值是可变对象(列表、集合、字典等),该默认值在程序运行之初的“编译阶段”就已经被创建,并一直保留在内存中。

所以 每次调用函数,修改的都是同一默认值对象。
最好不要使用列表、集合等可变容器作为参数的默认值。

容器复制与浅拷贝和深拷贝的问题

  1. a = ['A', 'B', 'C']
  2. b = a
  3. b[0] = '甲'
  4. a
  1. ['甲', 'B', 'C']

image.png

image.png

浅拷贝

  1. a = ['A', 'B', 'C']
  2. b = a.copy()
  3. b[0] = '甲'
  4. a
  1. ['A', 'B', 'C']

image.png

  1. a = ['A', ['B', 'C'], 'D']
  2. b = a.copy()
  3. b[1][0] = '乙'
  4. a
  1. ['A', ['乙', 'C'], 'D']

如果变量a包含多层列表(或字典等其他容器),a.copy()方法只会复制 第一层 的内容!

  1. a = {'A':['一', '甲'], 'B':['二', '乙']}
  2. b = a.copy()
  3. b['B'][0] = 'Two'
  4. a
  1. {'A': ['一', '甲'], 'B': ['Two', '乙']}

image.png

深拷贝

  1. from copy import deepcopy
  2. a = ['A', ['B', 'C'], 'D']
  3. b = deepcopy(a)
  4. b[1][0] = '乙'
  5. a
  1. ['A', ['B', 'C'], 'D']

image.png

  1. a = {'A':['一', '甲'], 'B':['二', '乙']}
  2. b = deepcopy(a)
  3. b['B'][0] = 'Two'
  4. a
  1. {'A': ['一', '甲'], 'B': ['二', '乙']}

使用列表乘法创建二维列表的问题

  1. a = [0] * 3
  2. a
  1. [0, 0, 0]
  1. b = [a] * 3
  2. b
  1. [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
  1. b[0][1] = 999
  2. b
  1. [[0, 999, 0], [0, 999, 0], [0, 999, 0]]

内置函数id(x)返回变量ⅹ的对象标识,一般为内存地址。

  1. id(b[0]) # b中第1个元素[0, 9990]的内存地址
  1. 1612616627336
  1. id(b[1]) # b中第2个元素[0, 9990]的内存地址
  1. 1612616627336
  1. id(b[2]) # b中3个元素[0, 9990]的内存地址
  1. 1612616627336
  1. id(a) # 列表a的内存地址
  1. 1612616627336

列表乘法执行的是浅拷贝,子列表都指向同一内存

可以使用列表生成式创建二维列表

  1. b = [[0] * 3 for i in range(3)]
  2. b
  1. [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
  1. b[0][1] = 999
  2. b
  1. [[0, 999, 0], [0, 0, 0], [0, 0, 0]]

因为[0]中没有子列表,所以[0]*3并不涉及列表乘法的浅拷贝问题。
只有存在子列表(或字典等其他可变容器)时,才需要考虑浅拷贝问题。

“_”只为控制循环次数

  1. b = [[0] * 3 for _ in range(3)]
  2. b
  1. [[0, 0, 0], [0, 0, 0], [0, 0, 0]]

练习

练习1:深拷贝与浅拷贝

请阅读下面的操作,推测它们的运行结果,然后在 IDLE 中实际运行并思考原因:

  1. 1.
  2. a = [ 1, 'abc', [1,2,3] ]
  3. b = a.copy()
  4. b[1] = '甲乙丙'
  5. print(b)
  6. print(a)
  7. 2.
  8. a = [ 1, 'abc', [1,2,3] ]
  9. b = a.copy()
  10. b[2] = ['一','二','三']
  11. print(b)
  12. print(a)
  13. 3.
  14. a = [ 1, 'abc', [1,2,3] ]
  15. b = a.copy()
  16. b[2][1] = 500
  17. print(b)
  18. print(a)
  19. 4.
  20. a = [ 1, 'abc', [1,2,3] ]
  21. import copy
  22. b = copy.deepcopy(a)
  23. b[2][1] = 500
  24. print(b)
  25. print(a)
  1. a = [ 1, 'abc', [1,2,3] ]
  2. b = a.copy()
  3. b[1] = '甲乙丙'
  4. print(b)
  5. print(a)
  1. [1, '甲乙丙', [1, 2, 3]]
  2. [1, 'abc', [1, 2, 3]]
  1. a = [ 1, 'abc', [1,2,3] ]
  2. b = a.copy()
  3. b[2] = ['一','二','三']
  4. print(b)
  5. print(a)
  1. [1, 'abc', ['一', '二', '三']]
  2. [1, 'abc', [1, 2, 3]]
  1. a = [ 1, 'abc', [1,2,3] ]
  2. b = a.copy()
  3. b[2][1] = 500
  4. print(b)
  5. print(a)
  1. [1, 'abc', [1, 500, 3]]
  2. [1, 'abc', [1, 500, 3]]
  1. a = [ 1, 'abc', [1,2,3] ]
  2. import copy
  3. b = copy.deepcopy(a)
  4. b[2][1] = 500
  5. print(b)
  6. print(a)
  1. [1, 'abc', [1, 500, 3]]
  2. [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. 1.
  2. a = [ 1, 2, 3 ]
  3. b = a * 3
  4. print(b)
  5. b = [ a ] * 3
  6. print(b)
  7. print(b[1])
  8. 2.
  9. a = [ 1, 2, 3 ]
  10. b = [ a ] * 3
  11. b[0][1] = 500
  12. print(b)
  13. print(a)
  14. 3.
  15. b = [ [1,2,3] for _ in range(3) ]
  16. print(b)
  17. b[0][1] = 500
  18. print(b)
  19. 4.
  20. a = [1,2,3]
  21. b = [ a for _ in range(3) ]
  22. print(b)
  23. b[0][1] = 500
  24. print(b)
  1. a = [ 1, 2, 3 ]
  2. b = a * 3
  3. print(b)
  4. b = [ a ] * 3
  5. print(b)
  6. print(b[1])
  1. [1, 2, 3, 1, 2, 3, 1, 2, 3]
  2. [[1, 2, 3], [1, 2, 3], [1, 2, 3]]
  3. [1, 2, 3]
  1. a = [ 1, 2, 3 ]
  2. b = [ a ] * 3
  3. b[0][1] = 500
  4. print(b)
  5. print(a)
  1. [[1, 500, 3], [1, 500, 3], [1, 500, 3]]
  2. [1, 500, 3]
  1. b = [ [1,2,3] for _ in range(3) ]
  2. print(b)
  3. b[0][1] = 500
  4. print(b)
  1. [[1, 2, 3], [1, 2, 3], [1, 2, 3]]
  2. [[1, 500, 3], [1, 2, 3], [1, 2, 3]]
  1. a = [1,2,3]
  2. b = [ a for _ in range(3) ]
  3. print(b)
  4. b[0][1] = 500
  5. print(b)
  6. print(a)
  1. [[1, 2, 3], [1, 2, 3], [1, 2, 3]]
  2. [[1, 500, 3], [1, 500, 3], [1, 500, 3]]
  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] 指向的列表。