可变对象与不可变对象
可变对象与不可变对象的区别在于对象本身是否可变.
在 Python 常用的数据类型中,
- 可变对象: list, dict, set
 
- 不可变对象: int, float, str, bool, tuple
 
如下示例直观的展示了 list 可变对象 与 tuple 不可变对象的区别
1 2 3 4 5 6 7 8 9 10 11
   |  >>> a = [1, 2, 3] >>> a[1] = 4 >>> a [1, 4, 3]
  >>> b = (1, 2, 3) >>> b[1] = 4 Traceback (most recent call last):   File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment
 
  | 
 
元组的相对不可变性
元组与多数 Python 集合(list,dict,set 等等)一样,保存的是对象的引用.如果引用的元素是可变的,即便元组本身不可变,元素依然可变.也就是说,元组的不可变性其实是指 tuple 数据结构的物理内容(即保存的引用)不可变,与引用的对象无关
1 2 3 4 5 6 7 8 9 10 11 12 13
   | >>> t1 = (1, 2, [30, 40]) >>> t2 = (1, 2, [30, 40]) >>> t1 == t2 True >>> id(t1[-1]) 4302515784 >>> t1[-1].append(99) >>> t1 (1, 2, [30, 40, 99]) >>> id(t1[-1]) 4302515784 >>> t1 == t2 False
   | 
 
深浅拷贝
对于不可变对象来说,不管是深拷贝还是浅拷贝,都是对该对象添加一次引用,其内存地址及内部元素的内存地址均不会改变.如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
   | import copy
 
 
  imutable_object = ('a', ('b', ('c', 'd'), 'e'), 'f')  imutable_copy_object = copy.copy(imutable_object) imutable_deepcopy_object = copy.deepcopy(imutable_object)
  print('%s\t%s\t%s' % (id(imutable_object), id(imutable_copy_object), id(imutable_deepcopy_object))) print('%s\t%s\t%s' % (id(imutable_object[1]), id(imutable_copy_object[1]), id(imutable_deepcopy_object[1]))) print('%s\t%s\t%s' % (id(imutable_object[1][1]), id(imutable_copy_object[1][1]), id(imutable_deepcopy_object[1][1])))
  2364244967072   2364244967072   2364244967072 2364244737480   2364244737480   2364244737480 2364244918728   2364244918728   2364244918728
 
 
   | 
 
而对于可变对象来说,深拷贝与浅拷贝的拷贝粒度是不相同的,如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
   | import copy
  mutable_object = ['a', ['b', ['c', 'd'], 'e'], 'f']  mutable_copy_object = copy.copy(mutable_object) mutable_deepcopy_object = copy.deepcopy(mutable_object)
  print('%s\t%s\t%s' % (id(mutable_object), id(mutable_copy_object), id(mutable_deepcopy_object))) print('%s\t%s\t%s' % (id(mutable_object[0]), id(mutable_copy_object[0]), id(mutable_deepcopy_object[0]))) print('%s\t%s\t%s' % (id(mutable_object[1]), id(mutable_copy_object[1]), id(mutable_deepcopy_object[1]))) print('%s\t%s\t%s' % (id(mutable_object[1][1]), id(mutable_copy_object[1][1]), id(mutable_deepcopy_object[1][1])))
 
  1625855263240   1625853920840   1625855164296   1625816559208   1625816559208   1625816559208   1625855263048   1625855263048   1625855164232   1625846665032   1625846665032   1625855156360  
   | 
 
因此总结如下
- 浅拷贝可以理解为对象中不可变对象元素进行一次完整拷贝,并直接引用原来的可变对象元素.原始对象与浅拷贝对象不是同一个对象,只是值相同而已.修改浅深贝对象中的可变对象会对原始对象造成影响.
 
- 深拷贝可以理解为对象中数据进行一次完整的拷贝,会重新分配内存地址.原始对象与深拷贝对象不是同一个对象,只是值相同而已.修改浅深贝对象不会对原始对象造成影响.
 

1 2 3 4 5 6
   | import copy a = [1,2,[3,4]] b = copy.copy(a) c = copy.deepcopy(a) b.insert(0,0) b[-1].append(5)
   | 
 

可在此网址查看如上代码完整的过程.
什么时候发生深/浅拷贝,各自的应用场景有哪些
以列表为例,如下情况发生浅拷贝:
- 使用 copy 方法,如 
copy_list = copy.copy(list_1) 
- 构造方法,如 
copy_list = list(list_1) 
- 切片,如 
copy_list = list_1[:] 
应用如下:
1 2 3 4 5 6 7 8 9
   |  person = ['name', ['saving', 0]] person_1 = person[:] person_2 = person[:] person_1[0] = 'name_1' person_2[0] = 'name_2' person_2[1][1] += 100  >>> person  ['name', ['saving', 100]]
 
  | 
 
其它情况发生的都是深拷贝
参数传递
Python 中函数的传参与 Go 中函数参数传递类似.均可以理解为对变量值进行拷贝后进行传入函数中.只不过,对于不可变参数来说,传入的是对象的内存地址.而对于可变对象来说,传入的是指向对象内存地址的内存地址.
Python 唯一支持的参数传递模式是共享传参.共享传参指函数的各个形式参数获得实参中各个引用的副本.也就是说,函数内部的形参是实参的别名.
这种情况下,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象的标识.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
   | >>> def f(a, b): ...     a += b ...     return a ... >>> x = 1 >>> y = 2 >>> f(x, y) 3 >>> x, y (1, 2) >>> a = [1, 2] >>> b = [3, 4] >>> f(a, b) [1, 2, 3, 4] >>> a, b ([1, 2, 3, 4], [3, 4]) >>> t = (10, 20) >>> u = (30, 40) >>> f(t, u) (10, 20, 30, 40) >>> t, u ((10, 20), (30, 40))
   | 
 
对于上述示例,可以总结如下:
对于函数传参调用 f(obj_1, obj_2) 来说,函数内部其实是将 obj_1 添加了引用(或设置了别名/标签) a,obj_2 添加了引用(或设置了别名/标签)b,即执行了如下代码 a=obj_1,b=obj_2.因此,在函数中的 += 操作会影响到原始对象.只不过对于不可变对象来说,+= 操作会重新分配内存空间,然后赋值给同名变量.
不要使用可变对象作为参数的默认值
如果使用可变对象作为默认值,则在新创建对象时,所有的赋值操作都是对该可变对象添加新的引用(或贴新的标签),而其实底层使用的是同一个对象.
如下是一个将空列表作为默认参数传入示例,简单说明可变默认值可能引发的问题.
1 2 3 4 5 6 7 8
   | class HauntedBus:     """备受幽灵乘客折磨的校车"""     def __init__(self, passengers=[]):         self.passengers = passengers     def pick(self, name):         self.passengers.append(name)     def drop(self, name):         self.passengers.remove(name)
   | 
 
下面是对该类进行的引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
   | >>> bus1 = HauntedBus(['Alice', 'Bill'])  >>> bus1.passengers ['Alice', 'Bill'] >>> bus1.pick('Charlie') >>> bus1.drop('Alice') >>> bus1.passengers ['Bill', 'Charlie']
  >>> bus2 = HauntedBus()  >>> bus2.pick('Carrie') >>> bus2.passengers ['Carrie'] >>> bus3 = HauntedBus()  >>> bus3.passengers  ['Carrie'] >>> bus3.pick('Dave')  >>> bus2.passengers ['Carrie', 'Dave'] >>> bus2.passengers is bus3.passengers  True
   | 
 
为解决上述问题,我们做如下修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
   | class TwilightBus:     """让乘客销声匿迹的校车"""     def __init__(self, passengers=None):         if passengers is None:             self.passengers = []          else:             self.passengers = passengers      def pick(self, name):         self.passengers.append(name)     def drop(self, name):         self.passengers.remove(name)
  >>> bus2 = TwilightBus()  >>> bus2.pick('Carrie') >>> bus2.passengers ['Carrie'] >>> bus3 = TwilightBus()  >>> bus3.passengers  [] >>> bus3.pick('Alice') >>> bus2.passengers  ['Carrie'] >>> bus2.passengers is bus3.passengers False
 
 
 
 
 
   | 
 
以上示例虽然解决了前一个示例中两个对象引用一个列表的问题,但是当传入参数是可变对象(如 list) 时,所有对该 list 的操作会影响到外部 list.如
1 2 3 4 5 6
   | >>> basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat']  >>> bus = TwilightBus(basketball_team)  >>> bus.drop('Tina')  >>> bus.drop('Pat') >>> basketball_team   ['Sue', 'Maya', 'Diana']
   | 
 
产生上述问题的原因是由于对函数传入可变对象后,在函数中的所有操作都会影响到原始对象.因此做如下改进
1 2 3 4 5 6 7 8 9 10 11 12
   | class Bus:     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)
   |