目录
一、变量不是盒子
python中变量不是盒子,而是盒子上贴的标签。对象才是盒子。对象一般是指一块存储空间,变量是对象上贴的标签,一个对象可以贴好多个标签,即可以对应很多个变量名。如下例子,把变量a和b分配给了同一个列表对象,类似于C++中的引用概念,python中的变量的本质都是C++中的引用。
因为对象在右边,总是先于变量创建,因此正确说法是把某变量分配给某对象,而不是反过来。
二、标识、相等性和别名
对象的标识、类型和值
每个对象都有标识、类型和值。
- 对象标识,一经对象创建,在对象的生命周期中标识就不会再改变。id()函数返回对象标识的整数表示。
- 类型,对象对应的类型。
- 值,两个对象的值可能一样,但是这还是两个不同的对象,因为其标识不同。
== 和 is的不同
== 运算符比较两个对象的值,而 is 比较对象的标识。
一个特殊的例子,最常使用is检查变量绑定的值是不是None。
- is运算符比 == 速度快,因为is不能重载,因此python不用寻找并调用特殊方法。
- 而 a == b 是语法糖,等价于 a.__eq__(b),即调用魔术方法比较a与b。
- 继承自object的最原始的__eq__方法等价于is,是直接比较id的,但是很多类型都重写了__eq__方法,去考虑值的相等性。
元组的相对不可变性
元组里保存的是元素的引用,但如果引用的元素是可变的,即便元组本身不可变,其元素也依然可以变化。即元组中不可变的是元素的标识,而不是元素的值。
三、默认做浅拷贝
浅拷贝
我们知道python中的变量不是盒子,二是标签,那么当我们要复制一个列表的时候,如果直接用
l2 = l1 则相当于l2和l1是别名,两个对应同一个对象。要复制列表最简单的方式是使用其构造函数。如下面例子中,l1 和 l2的值相同,但是是两个不同的对象。
对于列表和其它可变序列来说,还可以使用 l2 = l1[ : ] 来创建副本。
l1 = [1, 2, 3]
l2 = l1[:] # l2 = list(l1)得到的结果相同
print(l1 == l2)
print(l1 is l2)
print(l1[0] is l2[0])
根据实验结果可看出,用 l2 = l1[:] 创建副本时 l2与l1确实是不同的对象,但是由于列表对象中的元素是引用,而l2直接复制了l1中的引用,因此l1和l2中元素对应的是相同的对象。若所有元素不可变,这样做没有问题且可以节省内存,但如果有可变的元素则这样不对。
综上,用构造方法或者[ : ]创建副本时做的都是浅拷贝。即只复制了外壳,副本中的元素都是源容器中元素的引用。
接下来一个例子,对一个包含一个列表和一个元组的列表做浅拷贝。
- l2是l1的浅拷贝副本、
- 给l1列表加一个元素,100,此时不会影响到l2
- 给l1列表的第1个元素(即列表[66, 55, 44])移除元素55,因为l2中的元素是l1中元素的引用,因此此操作会影响到l2
- 给l2的第1个元素拼接上[33, 22](可变对象+=是原地操作),同理,l2中与l1中元素互为别名,因此会影响到l1
- 给l2的第2个元素拼接上(10, 11)(不可变对象+=会创建一个新对象绑定给原变量),因为元组不可变,因此拼接之后l2中对应位置的元组变成了一个新的元组对象(7,8,9,10,11),但是l1中不会改变,此时l1与l2中对应位置已经是不同的元组对象了。
上述例子输出如下,符合预期。
深拷贝
深拷贝即副本不共享内部对象的引用。
不管浅拷贝还是深拷贝,拷贝之后的副本与原对象肯定是不同的对象,即标识不同,但是浅拷贝中副本与原对象共享内部元素的引用。copy模块提供的deepcopy和copy函数能为任意对象深拷贝和浅拷贝。下面例子展示deepcopy和copy的用法。
from copy import deepcopy, copy
class Bus:
def __init__(self, passengers=None): # 这里用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(bus1)
bus3 = deepcopy(bus1)
print("three buses' ids: ", id(bus1), id(bus2), id(bus3))
bus1.drop('Bill')
print('bus1: ', bus1.passengers)
print('bus2: ', bus2.passengers)
print('bus3: ', bus3.passengers)
print("three buses' passengers' ids: ", id(bus1.passengers),id(bus2.passengers),id(bus3.passengers))
通过特殊方法__copy__()和__deepcopy__()可以控制copy和deepcopy的行为。
含有循环引用的拷贝问题不做讨论。
四、函数的参数作为引用
python函数默认引用传参
C里边默认的传参方式是值传递,而python里边默认的传参方式是引用传递,即形参是实参的别名,因此如果传入的实参是可变对象,则函数体内可能会改变实参。如下例子所示,传入不可变对象时是改变不了实参的,因为 a += b,实际上a已经分配给一个新的对象了,因此不会改变实参。
不要使用可变类型作为参数默认值
函数的有些参数可以设置缺省/默认值,但是这个值不能用可变类型。
上边例子中把HauntedBus类的构造函数中passengers的默认值由None改成了空列表[ ]。带来的改变是某个HauntedBus类的对象的self.passengers属性是__init__函数的参数passengers的别名,而默认情况下,如果在构造的时候没有传参,即默认构造时,passengers时空列表[ ] 的别名。而空列表[ ] 这个时候已经变成了函数__init__的一个属性(python中函数是一等对象,因此空列表[ ]是函数对象__init__的属性这点不难理解。
因此,当我们构造bus1时,因为传参构造,所以passengers是传入参数的别名,而非函数对象__init__的属性空列表[ ] 的别名,因此正常运行,不会有错。
但是当我们构造bus2时,因为是默认构造,因此bus2的self.passengers实际上是函数对象__init__的属性空列表 [ ] 的别名,因此对于bus2的self.passengers属性的一切操作都是反应到函数对象__init__的属性上,因此当我们默认构造bus3的时候,实际上这个时候默认的passengers已经不是空列表了,而且bus2和bus3的self.passengers属性是同一个列表对象的别名(这个列表对象即为构造函数中的默认列表参数)。
防御可变参数
如果定义的函数接收可变参数,需要谨慎考虑是否修改传入的参数。
还是这个校车类,下面例子中我们传入实参basketball_team,然后经过校车类对象处理后影响到了实参,如果要避免影响到实参,在构造函数中应该给对象的self.passengers属性分配实参的一个副本,而不应该直接跟实参绑定。
改造方案如下:
五、del和垃圾回收
del只会删除对象的引用,而不会删除对象,但删除对象的引用可能会导致对象被删除。
python对象被删除有两种情况:
- 某个对象的引用计数为零
- 一组对象之间全是相互引用,导致组中对象不可获取
两种情况可以归结于同一种情况,即这个对象不可获取了,那么就会被当作垃圾回收。
weakref.finalize用函数bye来监视被s1绑定的对象,如果该对象被回收,则bye就会被调用,其返回值ender可以看s1绑定对象的状态,一开始的时候肯定是或者的,即 alive == True,当我们用del删除s1的时候,bye并没有被调用,因此对象没有被删除,这是因为对象还有一个引用s2,引用计数并不等于0。但是当我们将s2指向其他对象(即‘spam’)的时候,原先的对象就不可获取了,因此会被回收,也因此bye函数被调用的,而ender.alive也变成False。
有一点问题是,我们在ender = weakref.finalize中把s1引用传给了finalize函数了,为了监控对象{1, 2,3}和调用回调,理论上是必须要引用{1,2,3}的,那么为什么最终{1,2,3}被销毁了呢?这是因为finalize持有{1,2,3}的弱引用。
六、弱引用
弱引用可调用,返回所指对象
首先有一点注意,python控制台会自动把 _ 变量绑定到结果不为None的表达式结果上,因此可能会隐式地增加了新的引用。
- 创建一个弱引用
- 弱引用是可调用对象,返回值即被引用的对象,需要注意的是因为控制台输出了{0,1},因给其此增加了一个新的引用,即 _
- 将a_set不再引用{0,1}集合,引用数量减少了
- 调用wref()仍然能返回{0,1},这是因为调用的时候其还有一个引用 _,
- 但是当调用wref完成后,控制台输出False,_就绑定到了False上,因此{0,1}被回收。
- 上一步{0,1}已被回收,因此这里弱引用为None,弱引用不会妨碍所指对象被当作垃圾回收
weakref.ref类是低级接口,不要直接使用,应该使用weakref集合(WeakKeyDictionary、WeakValueDictionary、WeakSet)和finalize。
WeakValueDictionary
实现一种可变映射,键是对象的某种可散列的属性,值值对象的弱引用。如果对象被回收,则对应的键会从WeakValueDictionary中删除。
- stock是WeakValueDictionary实例
- stock把cheese对象中属性,即奶酪的名称映射到cheese对象的弱引用上
- stock中的所有键,即不同奶酪的名称,此时一共四个
- 删除catalog,即cheese对象的列表名称后,stock大多数奶酪都不见了,但是还剩下了一个Parmesan,为什么?原因是在for cheese in catalog这一语句中,cheese是一个全局变量,因此其最后一次循环引用了Parmesan,因此del catalog后,前三个奶酪引用计数都归零,只有Parmesan引用计数为1。
弱引用的局限
不是每个python对象都可以作为弱引用的所指对象。
- list和dict实例不能作为所指对象,但是他们的子类可以。
- set可以作为所指对象
- 自定义类型可以作为所指对象
- int和tuple不能作为所指对象,其子类也不行+
由于弱引用并不保证引用对象不会被回收,因此如果代码中需要用到循环引用,则可以用弱引用来实现,用弱引用可以解决循环引用不能正常垃圾回收的问题。