对象引用、可变性、垃圾回收

变量可变性与不可变性

引用式变量:变量是标注,而不是盒子

1
2
3
4
5
>>> a = [1, 3, 5]
>>> b = a
>>> a.append(7)
>>> b
[1, 3, 5, 7]

变量只是对象的一个tag标签,一个对象可以有多个tag。

  • 标识、相等性和别名

类似于一个人可以有真名,别名,笔名,绰号

== 运算符比较两个对象的值,而 is 比较对象的标识(根据此判断两个变量绑定的是不是同一个对象)(如果是常用对象如整型-5–255之间,则都是同一个对象)

1
2
3
4
5
6
>>> a = 800
>>> b = 800
>>> a == b
True
>>> a is b
False
  • 元组的相对不变性

元组的不变性是指tuple数据结构的物理内容(保存的引用)不可变,当引用的对象是可变对象如list,此时该引用(list)可变,换句话说,元组的不可变性与引用的对象无关。

1
2
3
4
5
6
7
8
9
10
11
12
>>> t1 = (1, 2, [3, 4])
>>> t2 = (1, 2, [3, 4])
>>> t1 == t2
True
>>> t3 = t1
>>> t1 is t3
True
>>> t1[-1].append(5)
>>> t1
(1, 2, [3, 4, 5])
>>> t1 == t2
False
  • 浅复制与深复制

浅复制:副本共享内部对象的引用

1
2
3
4
5
6
7
8
9
10
l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1) # 1
l1.append(100) # 2
l1[1].remove(55) # 3
print(l1)
print(l2)
l2[1] += [33, 22] # 4
l2[2] += [10, 11] # 5
print(l1)
print(l2)

1: l2 是 l1 的浅复制副本,共享内部对象的引用,也就是说,l1 和 l2中[66, 55, 44], (7,8,9)其实指的是同一对象

2: 浅复制只是共享内部对象的引用,当然本身的修改则不会对副本产生影响

3: 内部列表l1[1]中的55删除,则对l2产生影响,因为两者绑定的同一对象

4: 对可变对象来说,+= 运算符就地修改列表,故两者都会改变

5: 对元组不可变对象来说 += 运算符创建一个新元组,然后重新绑定给变量l2[2],于是 l1,l2中的元组不是同一对象,则l1 不会改变

1
2
3
4
[3, [66, 44], (7, 8, 9), 100]
[3, [66, 44], (7, 8, 9)]
[3, [66, 44, 33, 22], (7, 8, 9), 100]
[3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]

深复制:副本不共享内部对象的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import copy

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)

bus1 = Bus(['alice', 'bill', 'claire', 'david'])
bus2 = copy.copy(bus1)
bus3 = copy.deepcopy(bus1)
print(id(bus1), id(bus2), id(bus3))

bus1.drop('bill')
print(bus2.passengers)
print(id(bus1.passengers), id(bus2.passengers), id(bus3.passengers))
print(bus3.passengers)

结果:

1
2
3
4
4446707784 4446708008 4446708680
['alice', 'claire', 'david']
4446689736 4446689736 4446688136
['alice', 'bill', 'claire', 'david']

因为copy.copy是浅复制,bus1让 bill 下车,则使 bus2的乘客少了 bill,而 bus3 是深复制,bus3.passengers 和bus1.passengers 不是同一对象,则对bus3的乘客不产生影响。

  • 函数不要使用可变对象作为参数

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
30
31
class HaunteBus:
def __init__(self, passengers=[]):
self.passengers = passengers

def pick(self, name):
self.passengers.append(name)

def drop(self, name):
self.passengers.remove(name)


bus1 = HaunteBus(['alice', 'bill'])
print(bus1.passengers)

bus1.pick('charlie')
bus1.drop('alice')
print(bus1.passengers)

bus2 = HaunteBus()
bus2.pick('carrie')
print(bus2.passengers)

bus3 = HaunteBus()
print(bus3.passengers)

bus3.pick('dave')
print(bus2.passengers)

print(bus2.passengers is bus3.passengers)

print(bus1.passengers)

结果:

1
2
3
4
5
6
7
['alice', 'bill']
['bill', 'charlie']
['carrie']
['carrie']
['carrie', 'dave']
True
['bill', 'charlie']

为什么会出现这种情况呢?bus3 自带一位”幽灵”乘客。这是因为,在实例化对象HaunteBus时,不指定乘客的话,self.passengers 变成 passengers参数默认值的别名,即 bus2.passengers 和 bus3.passengers指的同一对象。根源是,默认值在定义函数计算(通常是在加载模块时),默认值变成了函数对象的属性。

因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。

  • 防御可变参数

如同上述例子,为了防止出现self.passengers变成passengers的别名,校车应该自己维护乘客列表。

1
self.passengers = list(passengers)

这种方法使得传给passengers参数的值可以是元组或任何其他可迭代对象,因为list构造方法接受任何可迭代对象。

del 和 垃圾回收

对象绝不会自行销毁;然而,无法得到对象时,可能会被当作垃圾回收。

del 语句删除名称,而不是对象。del 命令可能会导致对象被当作垃圾回收,仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时。

在CPython中,垃圾回收的主要算法是引用计数。当引用计数归零时,对象立即被销毁。

弱引用

弱引用不会增加对象的引用数量。引用的目标对象为所指对象。所以,弱引用不会妨碍所指对象被当作垃圾回收。


总结

  • 简单的赋值不创建副本
  • 对 += 或 *= 所做的增量赋值来说,如果左边的变量绑定的是不可变对象,会创建新对象;如果是可变对象,会就地修改。
  • 为现有的变量赋于新值,不会修改之前绑定的变量,这叫重新绑定:现在变量绑定了其他对象。如果之前是最后一个引用,对象会被当作垃圾回收
  • 函数的参数以别名的形式传递,这意味着,函数可能会修改通过参数传入的可变对象,这一行为无法避免,除非在本地创建副本,或者使用不可变对象
  • 使用可变类型作为函数参数的默认值有危险,因为如果就地修改了参数,默认值也就变了,这会影响以后使用默认值的调用
  • 在CPython 中,两对象循环引用时,两个对象都会被销毁