
Python 对象引用、可变性和垃圾回收
一般人们解释变量时,都会把变量比作盒子。但对于像 Python 这样的引用式变量就不能这样理解了,更合理的解释应当是变量是便利贴。赋值语句就相当于贴便利贴。因此,b = a 应当理解为在标注为 a 的对象上再贴一个标志 b。也因此,对于引用式变量,赋值的准确描述应该是把变量分配给对象。同时按照 Python 是先执行表达式右边的内容也可以很好理解,对象是先创建再赋值的。
标识、相等性与别名
当两个不同的变量指向同一个对象时,这两个变量就互为对方的别名,同时你可以用 is 运算符和 id 函数来确定这一点。但如果两个不同的变量绑定的是具有相同内容的不同对象时,这两个变量就只具有相等性了,== 运算符会返回 True,is 则会返回 False。(在 CPython 中,id 函数返回的是对象的内存地址,其他类型的 Python 则可能不是)
而在面对 == 和 is 时,我们应该如何选择呢?一般来说,== 比较的是两个对象的值,is 则是比较两个对象的标识,所以一般我们都用 == 更多。但当比较一个变量和一个单例时,is 更合适,比如 x is None 这样的比较。毕竟 is 比 == 速度更快,因为它不能重载。(a == b 其实是语法糖,等同于 a.__eq__(b)。)
元组的相对不可变性
对于元组,大家的第一个印象应该就是不可变的。而更熟悉 Python 的程序员会知道元组并非完全不可变,元组与大多数 Python 容器一样,存储的都是对对象的引用。因此,当你在元组中存储像列表这样不可哈希的容器时,元组本身就会变为不可哈希的了,你也可以通过改变存储在元组的列表来改变元组的值。也就是说,元组的不可变性其实是指 tuple 数据结构的物理内容不可变,与引用的对象无关。
浅拷贝与深拷贝
要复制列表或多数内置的可变容器很简单,只需要 list(a) 或者 a[:] 即可。但是这种复制默认是浅拷贝,也就是说它只会复制最外层的容器,内部引用的项还是和原容器一样的。这样就可能会造成一些意想不到的问题。比如改变复制后的列表中内部的一个可变容器的内容时,原列表也会因此改变。而为了解决这个问题,我们就有了深拷贝。copy 模块提供的 copy 和 deepcopy 就分别是浅拷贝和深拷贝,深拷贝复制时就会连着容器的项一起复制了。
注意,深拷贝并不是一件简单的事,如果对象有引用循环,那么简单的算法会出现死循环。当然,deepcopy 能优雅地解决这个问题(引用循环指 a = [10, 20]; b = [a, 30]; a.append(b))。
函数的参数
Python 唯一支持的参数传递模式就是共享传参。也就是说函数的形参获得的是实参的引用副本,是实参的别名,也因此在函数内部对传入的列表就地修改时,列表本身就会发生变化。
而与之相关的一个问题就是默认值。可选参数具有默认值,但如果你给的默认值是一个可变的值,会发生什么情况呢?答案就是在不指定默认值的内容时,函数内部与默认值相关的变量都会是默认值的别名。举个例子,你给一个类的默认值设置为空列表,并在类中实现了 append 方法。如果你创建实例时使用了默认值,那你使用 append 方法时就会改变这个类本身默认值的内容,新创建的实例的默认值也会包含另一个实例添加的内容。这也是为什么通常用 None 作为接收可变值参数的默认值。
del 与垃圾回收
很多人对 del 有个误解,认为使用 del 就能删除对象。但实际上,del 语句删除的是引用。del 可能导致对象被当作垃圾回收,但是仅仅是当删除的变量是保存对象的最后一个引用时才会发生。重新绑定也能导致引用数量归零而对象被销毁。在 CPython 中,垃圾回收主要就是引用计数,引用计数归零时就会销毁对象。而有些 Python 的垃圾回收原理则更复杂,不依赖引用计数。
如果想要演示对象生命结束是怎么样的,可以使用弱引用库 weakref,用它对对象产生的引用不会被作为引用计数,也就是说使用它时,对象依然可能会因为引用计数归零而被销毁,这样你就可以监控对象生命了。
不可变容器的复制行为
值得一提的是,Python 对元组进行如 tuple(a) 或 a[:] 这样的操作时并不会复制,返回的依然是对象本身,因为元组毕竟不可变,复制了也没什么意义。如果你进行测试,会发现副本的 id 不会发生任何变化。当然,不止元组,其他不可变容器像 str,bytes 以及 frozenset 也一样(frozenset 就没有什么 [:] 的用法了,不过使用 a.copy() 也是一样的结果)。
除此之外,str 还有一种驻留机制,当你给不同变量赋值内容相同的 str 时,一般它们的 id 也会是相等的,在 CPython 中整型也有驻留机制,一些热门数字也会使用同一个对象而不是重新创建。但是注意,这些驻留机制并不对所有 str 或 int 都起作用,这种和它本身的技术实现有关了,所以不要认为 str 就可以使用 is 了。
Tips:在使用
+=或*=时,如果对象可变那会直接就地操作,如果不可变比如元组则会创建一个新元组。对于元组这种容器,因为其内部没有对对象是否可哈希进行判断,因此是相对不可变的。但是像frozenset这种内部对象都要求必须可哈希,就是完全不可变的了(set也是内部对象必须可哈希,因此你不能嵌套set,这也是frozenset出现的一个原因)。



