Wlgls 冲鸭!

第八章 对象引用,可变性和垃圾回收


变量不是盒子

在Python中,我们不应当把变量当作盒子,更加形象的比喻是变量是便利贴。

在Python中,一个简单的赋值语句,如list=[1, 2, 3], 具体的操作过程应当是首先在内存中建立一个[1, 2, 3]的对象,之后变量list指向了这个对象[1, 2, 3],也就是把变量list分配给了[1, 2, 3]

2019-10-16-23-28-19.png

我们不妨举个例子

>>> list = [1, 2]
>>> list1 = list
>>> list1
[1, 2]
>>> id(list1)
139783188350280
>>> id(list)
139783188350280
>>> list1.append(3)
>>> list1
[1, 2, 3]
>>> list
[1, 2, 3]

我们使用id内置函数,可以看出,listlist1的内存地址是一致的,并且如果我们对list1进行操作,是可以反映在list`中的。

==和is之间的选择

  • ==运算符比较的是两个对象的值
  • is比较的是对象的标识

通常情况下,我们更关注的是值,所以==要比is出现的频率多高。

最常见的is是检查变量捆绑的是否是None。如x is None, x is not None

is运算符比==速度快,因为is运算符不能重载,所以Python不会寻找并调用特殊方法,而是直接比较两个整数ID。

a == b其实是语法糖(Syntactic sugar)。等同于a.__eq__(b)。继承自Object的__eq__方法比较两个对象的ID,与is一致。但多数内置对象使用了更有意义的方式覆盖了__eq__方法

元组的相对不变性

实际上,元组列表字典等保存的是对象的引用,而不是对象本身。所以元组的相对不变性指的是其保存的引用不可变,而与引用的对象无关。如果元组内部引用的元素是可变的,则即便元组本身不可变,但是元素依然可变。以便利贴为例,相当于便利贴是无法被破坏的,但是我们可以对便利贴上的内容进行任意的修改。

>>> t1 = (1, [2, 3], 4)     # 1 
>>> t1[1].append(5)         # 2
>>> t1
(1, [2, 3, 5], 4)

2019-10-16-23-28-41.png 2019-10-16-23-28-46.png

在上述的例子中,尽管t1元组不可变,但是t1[1]是可变的,所以当我们修改t1[1]时,t1中的元素也就发生了变化。

元组的相对不变性,也解释了为什么有些元组不可散列。

tips:

可散列的数据类型
如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而且这个对象需要实现__hash__()方法。另外可散列对象还要有__eq__()方法,这样才能跟其他键做比较。如果两个可散列对象是相等,那么它们的散列值一定是一样的。

原子不可变数据类型(str, bytes和数值类型)都是可散列的,元组的话,只有当一个元组包含的全部元素都是可散列类型才是可散列的。

在前面我们有时也对于元组本身的修改,但事实上是新建了对象,然后将这个变量分配给了新的对象。 2019-10-16-23-28-59.png

深复制和钱复制

浅拷贝

使用构造方法或者[:]做的是浅复制(即复制了最外层的容器,副本中的元素是源容器中元素的引用),我们使用图片来形象的看一下

假如有代码:

>>> l1 = [1, [2, 3], (4, 5, 6)]
>>> l2 = list(l1)

我们将其放在pythontutor试一下

2019-10-16-23-29-08.png

对于其中的[2, 3]列表和(4, 5, 6)元组都是复制的是引用,而不是新建了一个副本,这也就意味着,如果我们通过l2修改,会影响到l1中的列表

>>> l2[1].append(7)
>>> l1
[1, [2, 3, 7], (4, 5, 6)]

2019-10-16-23-29-53.png

但是由于了l1和l2指向的内存空间不同,所以,我们对于l2本身的修改有不会影响到l1

>>> l2.append(10)
>>> l1
[1, [2, 3, 7], (4, 5, 6)]
>>> l2
[1, [2, 3, 7], (4, 5, 6), 10]

2019-10-16-23-30-00.png

深复制

尽管浅复制节省了内存空间,但是在某些情况下,我们需要使用深复制(即副本不共享内部空间的引用),但是深复制仍然有其节约内存的地方,相比于浅复制,深复制只是把元素中可变的部分进行了复制,而对于其中不可变的元素,仍然是复制的引用。

1	import copy
2	
3	l1 = [1, [2, 3], (4, 5, 6)]
4	l2 = copy.deepcopy(l1)

2019-10-16-23-30-08.png

函数的参数作为引用时

Python唯一支持的参数传递模式是共享传参(call by sharing)。共享传参指函数的各个形式参数获得实参中各个引用的副本。而不是对象的副本,也就是说形参是实参的别名

这种方案意味着函数会修改作为参数传入的可变参数,当然,是无法修改那些对象的标识的。

def foo(l1, l2):
    l1 += l2
    return l1

x = [1, 2]
y = [3, 4]

foo(x, y)

2019-10-16-23-30-20.png 2019-10-16-23-30-30.png

如果是元组的话

def foo(x, y):
    x += y
    return x

x = (1, 2)
y = (3, 4)

foo(x, y)

2019-10-16-23-30-44.png

不要使用可变类型作为参数的默认值

可选参数可以有默认值,这是Python函数定的一个很棒的特性。

但是如果我们将默认值设为[],则很可能不可预料的错误。

我们举一个例子:

>>> def foo(l = []):
...     l += [1, 2]
...     print(l)
... 
>>> foo([3, 4])     # 1
[3, 4, 1, 2]        
>>> foo()           # 2
[1, 2]
>>> foo()           # 3
[1, 2, 1, 2]    
>>> foo([5 ,6])     # 4
[5, 6, 1, 2]
>>> foo()           # 5
[1, 2, 1, 2, 1, 2]

我们将函数中的参数l设置为空列表,当我们第一次传入参数时,我们的代码是没有问题的。当我们第二次使用foo函数时,我们没有传入参数,所以l为默认值,但是第三次的时候,当我们不传入参数,发现竟然出现了[1, 2, 1, 2]这样奇怪的样子。出现这个情况的根源是,默认值在定义函数时计算(通常是加载模块时),因此默认值变成了函数对象的属性。

2019-10-16-23-30-52.png

我们可以看出,当第一次运行foo之后,l指向了[1, 2],也就是说,由于默认对象是可变对象,当我们没有传入参数的时候,我们对参数指向的内存空间进行了操作,导致可变对象发生了改变。

防御可变参数

由于当我们传入的是可变参数时,我们在函数内部进行的修改体现在函数外部,但是具体情况具体分析,假如我们不愿意将体现到函数外部呢。这个时候使用浅复制或深复制是一个很好的选择。

例如:

>>> def foo(l = None):
...     if l is None:
...             l = []
...     else:
...             l = list(l)
...     
...     l += [3, 4]
... 
>>> 
>>> l = [1]
>>> foo(l)
>>> l
[1]

在这里我们使用了浅复制,来保证函数内部不影响到函数外部。而且这种方式十分灵活:我们可以传入包括元组在内的可迭代对象。

tips:

在传入可变参数时,我们应当慎重选择,除非我们确实想通过参数修改传入的对象,否则,使用副本才是最好的选择。

del和垃圾回收

我们在使用del命令时,只是删除了名称,而不是变量。但是del命令可能导致对象被当作垃圾回收,当且仅当删除最后一个保存对象的变量,或者无法得到对象。

在Cython中,垃圾回收主要通过引用计数。每个对象都会记录有多少个引用指向自己,当引用为0时,对象立即被销毁。

弱引用

对于这一点,我实在有点不能理解,在之后,更多的使用python之后,再做了解。