可变对象与不可变对象
可变对象与不可变对象的区别在于对象本身是否可变.
在 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)
|