本章先用一个比喻说变量不是盒子,而是标注,然后讨论对象标示,值和别名的概念。随后会揭露元组的一个神奇的特性,元组是不可变的,其中的值可以改变。之后引申到浅复制和深复制。接下来是引用和函数参数。然后最后一节讨论垃圾回收,del 命令以及如何使弱引用 “记住” 对象,而无需对象本身存在
本章的内容是许多 Python 程序中不易察觉的 bug 关键
变量不是盒子¶
人们经常使用变量是盒子这样的比喻,但是这不易理解面向对象语言中的引用式变量。最好把它们理解为附加在对象上的标注:
a = [1, 2, 3]
b = a
a.append(4)
b
这里是 a 和 b 都指向同一块地址,而不是把 a 的内容复制一份给 b
关于赋值方式,是从右面先执行的,下面例子说明了这一点:
class Gizmo:
def __init__(self):
print('Gizmo id: %d' % id(self))
x = Gizmo()
y = Gizmo() * 10
这里表明,在尝试求积之前,会创建一个 Gizmo 对象,但是肯定不会创建 y,因为在求乘积时候抛出了异常
标示、相等性和别名¶
charles = {'name': 'Charles L. Dodgson', 'born': 1832}
lewis= charles
lewis is charles
id(charles), id(lewis)
lewis['balance'] = 950
charles
这里 lewis 是 charles 的别名
alex = {'balance': 950, 'born': 1832, 'name': 'Charles L. Dodgson'}
alex == charles #比较内容
alex is not charles # 比较对象标识
对象 ID 的真正意义在不同的实现中有所不同。在 CPython 中, id() 返回对象的内存地址,但是在其它 Python 解释器中可能是别的值。关键是,ID 是唯一的数值标注,而且在生命周期中不会改变。
其实编程很少使用 id() 函数,标识最常使用 is 检查,而不是直接比较 ID
在 == 和 is 之间选择¶
== 运算符比较两个对象的值(对象中保存的数据),而 is 比较对象的标识
通常我们关注的是值,而不是标识,所以 Python 代码中一般 == 比 is 出现的频率要高
然而,在变量和单例值之间比较时,应该使用 is。目前,最常用 is 检查绑定变量的值是不是 None。下面是推荐的写法
x is None
否定的正确写法
x is not None
is 运算符比 == 速度快,因为它不能重载,所以 Python 不用寻找不调用特殊方法,而是直接比较两个整数 ID。而 a == b 是语法糖,等同于 a.__eq__(b)。继承自 object 的 __eq__ 方法比较两个对象的 ID,结果与 is 一样。但是多数内置类型使用更有意义的方式覆盖了 eq 方法,会考虑对象属性的值。相等性测试可能设计大量处理工作,例如,比较大型集合或嵌套层级深的结构时。
元组的相对不变性¶
元组与多数 Python 集合(列表,字典,集等)一样,保存的也是对象的引用。如果引用的元素是一成不变的,即元组本身不可变,元素依然可变。也就是说,元祖的不可变性其实是指 tuple 数据结构的物理内容(即保存的引用)不变,与引用的对象无关
t1 = (1, 2, [30, 40])
t2 = (1, 2, [30, 40])
t1 == t2
id(t1[-1])
t1[-1].append(99)
t1
id(t1[-1])
t1 == t2
默认做浅复制¶
复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法。例如:
l1 = [3, [55, 44], (7, 8, 9)]
l2 = list(l1) #创建 l1 的副本
l2
l2 == l1
l2 is l1
对于列表其它可变序列来说,还能使用简洁的 l2 = l1[:] 语句创建副本
然而,构造方法或 [:] 做的是浅复制(即复制了最外层容器,副本中的元素是原容器中元素的引用)。如果所有元素是不可变的,这样做没有没有任何问题,还能节省内存,但是如果有可变元素,可能会导致意想不到的后果
l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1)
l1.append(100) #对 l2 没有影响
l1[1].remove(55) # l1 和 l2 指向同一个引用,l2 也会有影响
print(l1)
print(l2)
l2[1] += [33, 22] #因为指向同一个引用,所以有影响
l2[2] += (10, 11) # 因为元组不可变,实际上是新建了个元组,l1 和 l2 的元组不是同一个对象了
print(l1)
print(l2)
为任意对象做深复制和浅复制¶
copy 模块提供的 deepcopy 和 copy 能为任意对象做深复制和浅复制。
为了演示 copy 和 deepcopy 的用法,下面定义了一个简单的类, Bus,这个类表示公交车,在途中乘客会上下车
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)
import copy
bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
bus2 = copy.copy(bus1)
bus3 = copy.deepcopy(bus1)
id(bus1), id(bus2), id(bus3)
bus1.drop('Bill')
bus2.passengers
id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)
bus3.passengers
注意,一般来说,深复制不是简单的事,如果对象有循环引用,那么这个朴素的算法会进入无限循环,deepcopy 函数会记住已经复制的对象,因此能优雅地处理循环引用,如下所示
a = [10, 20]
b = [a, 30]
a.append(b)
a
from copy import deepcopy
c = deepcopy(a)
c
此外,深赋值有时可能太深了,例如,对象可能会引用不该复制的外部资源或单例值。我们可以实现特殊方法的 __copy__() 和 __deepcopy__(),控制 copy 和 deepcopy 的行为
函数的参数作为引用时¶
Python 唯一支持的的参数传递方式是共享传参,也就是函数的各个形式的参数获得实参中各个引用的副本,也就是说,函数内部的形参是实参的别名。
这种方案的结果是,如果传入可变对象,可能会受到一些影响
def f(a, b):
a += b
return a
x = 1
y = 2
f(x, y)
a = [1, 2]
b = [3, 4]
f(a, b)
a, b
t = (10, 20)
u = (30, 40)
f(t, u)
t, u
不要使用可变类型作为参数的默认值¶
可选参数可以有默认值,这是一个很棒的特性,然而,我们应该避免使用可变的对象作为参数的默认值。
我们拿上面的公交车例子来说,如果我们的 __init__ 中 passengers 的默认参数不是 None,而是 [ ],会发生什么问题
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)
bus1 = HauntedBus(['Alice', 'Bill'])
bus1.passengers
bus1.pick('Charlie')
bus1.drop('Alice')
bus1.passengers
bus2 = HauntedBus()
bus2.pick('Carrie') # 此时默认列表不为空
bus2.passengers
bus3 = HauntedBus()
bus3.passengers # 看到默认列表不为空
bus3.pick('Dave')
bus2.passengers
bus2.passengers is bus3.passengers
bus1.passengers
问题在于,没有指定出使乘客的 HauntedBus 实例会共享同一个乘客列表,这种问题很难发现。如果传入乘客,会按照预期运作,如果不传,奇怪的事就发生了,这是因为 self.passengers 变成了 passengers 参数默认值的别名。出现这个问题的根源是,默认值在定义函数时计算(通常是在加载模块时),因此默认值变成了函数对象的属性。因此,如果默认值是可变对象,而且修改了它的值,那么后续函数调用都会收到影响
我们可以审查 HauntedBus.__init__ 对象,看它的 __defaults__ 属性中的那些幽灵乘客
dir(HauntedBus.__init__)
HauntedBus.__init__.__defaults__
最后,我们可以验证 bus2.passengers 是一个别名,它绑定到 HauntedBus.__init__.__defaults__ 属性的第一个元素上
HauntedBus.__init__.__defaults__[0] is bus2.passengers
可变默认值导致的这个问题说明了为什么通常使用 None 作为接收可变参数的默认值。在一开始公交车例子中,检查参数值是不是 None,如果是就把新的空列表赋值给 self.passengers
自己发现,下面的这种方法也可以简洁的完成此功能:
def __init__(self, passengers = []):
self.passengers = list(passengers) #这样列表即使是空的,也会把它的一个副本给 self.passengers
防御可变参数¶
如果定义的函数接收可变参数,应该谨慎考虑调用方是否期望修改传入的参数,我们来看一个公交车的行为:
basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat']
bus = TwilightBus(basketball_team)
bus.drop('Tina')
bus.drop('Pat')
basketball_team
看到从车上下去人之后,它的名字就从篮球队中消失了,这违反了设计接口的最佳实践,即 ”最少惊讶原则“,下面是 Twilight 的实现:
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)
这里的问题是直接为传进来的列表创建了别名。正常应该像第一个小车例子那么做
def __init__(self, passengers = None):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers)
除非真的想修改传入的对象,否则在勒种直接把参数赋值给实例变量一定要三思,因为这样会创建别名。如果不确定,就创建副本,会少些麻烦
del 和垃圾回收¶
对象绝不会自行销毁,然而,无法得到对象时,可能会被当做垃圾回收 -- Python 语言参考手册
del 删除名称,而不是对象,del 命令可能会导致对象被当做垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时。重新绑定也可能会导致对象的引用数量归零,从而导致对象销毁
有个
__del__特殊方法,但是它不会销毁实例,不应该在代码中调用。即销毁实例时,Python 解释器会调用此方法,给实例最后的机会释放外部资源,但自己很少需要这个方法
在 CPython 中,垃圾回收使用的主要算法是引用计数,实际上,每个对象都会统计有多少引用指向自己。当引用计数归零时,对象立即销毁:CPython 会在对象上调用 __del__ 方法(如果定义了),然后释放分配给对象的内存。CPython 2.0 增加了 分代垃圾回收算法,用于检测引用循环中涉及的对象组 -- 如果一组对象之间全是相互引用,即使再出色的引用方式也会导致数组中的对象不可获取,Python 的其它实现有更复杂的垃圾回收程序,而且不依赖引用计数,这意味着,对象引用计数为 0 时可能不会立即调用 __del__ 方法。
为了演示对象生命结束时的情形,下面使用了 weakre.finalize 注册一个回调函数,在对象销毁时候调用。下面的例子演示了没有指向对象的引用时,监视对象生命结束时的情形:
import weakref
s1 = {1, 2, 3}
s2 = s1
def bye():
print('Gone with the wind...')
ender = weakref.finalize(s1, bye)
ender.alive
del s1
ender.alive
s2 = 'spam'
ender.alive
弱引用¶
正是因为有引用,对象才会在内存中存在。当对象引用数量归零后,垃圾回收程序会把对象销毁。但是,有时需要引用对象,而不让对象存在的时间超过所需的时间。这经常用在缓存中
弱引用不会增加对象的引用数量。引用的目标对象称为所指对象(referent)。因此我们说,弱引用不会妨碍所指对象被当做垃圾回收。弱引用在缓存应用中很有用,因为我们不想仅因为被缓存引用者而始终保存缓存对象
下面例子展示了如何使用 weakref.ref 实例获取所指对象。如果对象存在,调用弱引用可以获取对象,否则返回 None
下面的这段控制台程序,Python 会自动把 _ 变量绑定到结果不为 None 的表达式结果上。这对演示的行为有影响,不过却凸显了一个实际的问题:微观管理内存时,往往会得到意外的结果,因为不明显的隐式赋值会为对象创建新的引用。下面的 _ 变量是一例。调用跟踪对象也常导致意料之外的引用
弱引用是可调用对象,返回的是被引用的对象;如果所指对象不存在了,返回 None
'''
>>> import weakref
>>> a_set = {0, 1}
>>> wref = weakref.ref(a_set) # 创建弱引用对象
>>> wref
<weakref at 0x7fa060e36050; to 'set' at 0x7fa060e40d00>
>>> wref() # 调用 wref() 返回的是被引用的对象{0, 1},因为这是控制台会话,索引{0, 1} 会被绑定给 _ 变量
set([0, 1])
>>> a_set = {2, 3, 4} # a_set 不再指向 {0, 1} 集合,因此集合引用数量减少了。但是 _ 变量仍然指代它
>>> wref() # 调用 wref() 仍然返回 {0, 1}
set([0, 1])
>>> wref() is None # 计算这个表达式时, {0, 1} 存在,因此 wref() 不是 None。但是随后 _ 绑定到结果值为 False,现在 {0, 1} 没有强引用了
False
>>> wref() is None # 因为 {0, 1} 对象不存在了,所以 wref() 返回 None
True
'''
print("上面程序在控制台才会有这个效果")
weakref.ref 类其实是低层接口,供高级用途使用,多数程序最好使用weakref 集合和 finalize。也就是说,应该使用 WeakKeyDictionary, WeakValueDictionary, WeakSet 和 finalize。不要自己创建并处理 weakref.ref 实例。我们上面例子是希望通过使用 weakref.ref 来褪去它的神秘色彩。但实际上,多数 Python 程序使用 weakref 集合
WeakValueDictionary 简介¶
WeakValueDictionary 类实现的是一种可变映射,里面的值是对象的弱引用。被引用的对象在程序中的其它地方被当做垃圾回收,对应的键会自动从 WeakValueDictionary 中删除。因此,WeakValueDictionary 经常用于缓存。
下面实现了一个简单的类,表示各种奶酪。
class Cheese:
def __init__(self, kind):
self.kind = kind
def __repr__(self):
return 'Cheese(%r)' % self.kind
然后敲以下代码,思考为什么 Parmesan 奶酪比其它的保存的时间长
import weakref
stock = weakref.WeakValueDictionary()
catalog = [Cheese('Red Leicester'), Cheese('Tilsit'),
Cheese('Brie'), Cheese('Parmesan')]
for cheese in catalog:
stock[cheese.kind] = cheese
sorted(stock.keys())
del catalog
sorted(stock.keys())
del cheese
sorted(stock.keys())
这是因为临时变量 cheese 引用了对象,这可能会导致该变量的存在时间比预期的长。通常,这对局部变量来说不是问题,因为它们在函数返回时会被销毁。但是在上面例子中,for 循环中的 cheese 是全局变量,除非显示删除,否则不会消失
与 WeakValueDictionary 对应的是 WeakKeyDictionary,后者的键是弱引用。后者的一般的应用如下:可以为应用中其他部分拥有的对象附加数据,这样就无需为对象添加属性。这对覆盖属性访问权限很有用
weakref 模块中还提供了 WeakSet 类,按照文档说明,这个类的作用很简单:保存元素弱引用的集合类。元素没有强引用时,集合会把它删除。如果一个类需要知道所有实例,一种好的方案是创建一个 WeakSet 类型的类属性,保存实例的引用。如果使用常规的 set,实例永远不会被垃圾回收,因为类中有实例的强引用,而类存在的时间与 Python 进程一样长,除非显式删除类
弱引用局限¶
不是每个类都能做弱引用目标(或称所指对象)。基本的 list 和 dict 实例不能作为所指对象,但是它们的子类可以轻松解决这个问题
class MyList(list):
'''list 的子类,实例可以作为弱引用的目标'''
a_list = MyList(range(10)
#a_list 可以作为弱引用目标
wref_to_a_list = weakref.ref(a_list)
set 实例可以作为所指对象,因此前面这段采用 set 实例:
>>> import weakref
>>> a_set = {0, 1}
>>> wref = weakref.ref(a_set) # 创建弱引用对象
>>> wref
...
其次用户定义的类型也可以,这也就解释了为什么上面使用 Cheese 类。但是,int 和 tuple 实例不能作为弱引用的目标,它们的子类也不行。
这些局限基本是 CPython 的实现细节,在其它 Python 解释器可能不一样。这些局限内部优化导致的结果
Python 对不可变类型施加的把戏¶
对于元组 t 来说, t[:] 不创建副本,而是返回同一个对象的引用。此外,tuple(t) 获得的也是同一个元组的引用。
t1 = (1, 2, 3)
t2 = tuple(t1)
t2 is t1
t3 = t1[:]
t3 is t1
str, bytes 和 frozenset 实例也有这些行为,注意,frozenset 实例不是序列,因此不能使用 fs[:](fs 是一个 frozenset 实例)。但是 fs.copy() 具有相同的效果:它会欺骗你,返回同一个对象的引用,而不是创建一个副本
t1 = (1, 2, 3)
t3 = (1, 2, 3)
t3 is t1
s1 = 'ABC'
s2 = 'ABC'
s2 is s1 # 共享字面量
共享字符串字面量是一种优化措施,称为 驻留(interning)。CPython 还会在小的整数上使用这个优化措施,防止重复创建“热门”数字,如 0,-1 和 42.注意,CPython 不会驻留所有字符串和整数,驻留的条件是实现细节,而且没有文档说明,所以千万不要依赖字符串或整数驻留,当你比较字符串或整数是否相等的时候,一定要使用 == 而不是 is。
这些讨论的把戏,包括 frozenset.copy 行为,是 “善意的谎言” 能节省内存,提供解释器的速度,不会为你带来麻烦,它对你唯一的用处应该是用来和其它程序员打赌= =