第四部分第8章,对象引用、可变性和垃圾回收
1. 创建对象之后才会把变量分配给对象
变量是对象的标注,是对象的别名,是对象的引用,并不是对象存储的地方。
例子1. 证明赋值语句的右边先执行
class Gizmo():
def __init__(self):
print('Gizmo id: %d' % id(self))
x = Gizmo()
#这里表明,在尝试求积之前会创建一个新的Gizmo实例。
y = Gizmo() * 10
print(dir())
2. 标识(id())、相等性(==)和别名
例子1. 两个变量指向同一个对象
charles = {'name': 'Charles L. Dodgson', 'born': 1832}
lewis = charles
print(id(charles), id(lewis))
print(charles is lewis)
lewis['balance'] = 950
print(charles)
例子2. chrles和lewis绑定同一个对象,alex绑定另外一个具有相同内容的对象
charles = {'name': 'Charles L. Dodgson', 'born': 1832}
alex = {'name': 'Charles L. Dodgson', 'born': 1832}
print(id(charles), id(alex))
#dict类的__eq__方法实现了==运算符
print(charles == alex)
print(charles is alex)
例子1和例子2中,charles和lewis是别名,即两个变量绑定同一个对象。而alex不是charles的别名,因为二者绑定的是不同的对象。alex和charles绑定的对象具有相同的值(==比较的是值),但它们的标识(id)不同。
每个对象都有标识(id)、类型和值。对象一旦创建,在它的生命周期中它的id不会变;可以将标识理解为对象在内存中的地址。is运算符比较两个对象的id;id()函数返回对象标识的整数表示。
3. ==和is
==运算符比较两个对象的(对象中保存的数据),is运算符比较对象的标识。
通常我们关注的是值,不是id,所以Python中出现的频率比is高。
is运算符比速度快。 a == b是语法糖,等同于a.eq(b)。继承自object的__eq__方法比较两个对象的id,结果与is一样。多数内置类型覆盖了__eq__方法,所以相等性测试可能涉及大量处理工作,例如比较大型集合或嵌套层级深的结构时。
4. 元组tuple的相对不可变性
- 元组与多数Python的collections(list、dict、set等等)一样,保存的是对象的引用。
- 元组的不可变性是值tuple本身不可变,元素依然可变。
- 而str、bytes和array.array等单一类型序列是扁平的,它们保存的不是引用,而是在连续的内存中保存数据本身(字符、字节和数字)。
例子1. 元组中不变的是元素的标识(id),元组的值会随着引用的可变对象的变化而变
t1 = (1, 2, [30, 40])
t2 = (1, 2, [30, 40])
#t1和t2是两个不同的对象,标识不同,但是值相同
print(id(t1), id(t2))
print(t1 == t2)
#print('id(t1)', id(t1))
print(id(t1[-1]))
#t1[-1]为列表的引用,可以修改其中的值
t1[-1].append(50)
print(id(t1[-1]))
print(t1)
#print('id(t1)', id(t1))
5. 浅复制(shallow copy)
例子1. 构造方法list和[:]做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)
l1 = [3, [55,44], (7,8,9)]
l2 = list(l1)
print(l1 == l2, id(l1), id(l2))
l3 = l2[:]
print(l3 == l2, id(l3), id(l2))
浅复制,如果所有元素都是不可变的,那么这样没有问题,且节省内存。但是,如果有可变的元素,可能会导致意想不到的问题。
例子2. 一个包含另外一个列表和一个元组的列表做浅复制,再做些修改,看看影响。
#可以在Python Tutor网站查看过程
l1 = [3, [66, 55, 44], (7, 8, 9)]
# 1.list()构造方法是浅复制
l2 = list(l1)
# 2.浅复制复制的是最外层容器,保留源容器中元素的引用,所以把100追加到l1中,对l2没有影响。
l1.append(100)
# 3.l1[1]与l2[1]绑定的列表是同一个
l1[1].remove(55)
print('l1:', l1)
print('l2:', l2)
# 4.对列表来说,+=运算符就地修改列表。
print('id(l2[1],列表): ', id(l2[1]))
l2[1] += [33, 22]
print('id(l2[1],列表): ', id(l2[1]))
print('id(l2[2],元组): ', id(l2[2]))
# 5.对元组来说,+=运算符创建一个新的元组,然后重新绑定给变量。
l2[2] += (10, 11)
print('id(l2[2],元组): ', id(l2[2]))
print('l1:', l1)
print('l2:', l2)
对列表来说,+=运算符就地修改列表。对元组来说,+=运算符创建一个新的元组,然后重新绑定给变量。
6.深复制(Deep copy)
- 有时候我们需要的是深复制(即副本不共享内部对象的引用)
- copy模块提供的deepcopy和copy函数能为任意对象做深复制和浅复制。
例子1. 演示copy()和deepcopy()的用法,定义了Bus类。
class Bus:
"""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)
创建一个Bus类的实例bus1,一个浅复制副本bus_shallow, 一个深复制副本bus_deep
class Bus:
"""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'])
#copy()浅复制
bus_shallow = copy.copy(bus1)
#deepcopy()深复制
bus_deep = copy.deepcopy(bus1)
#bus1和bus_shallow元素中passenger是同一个列表的引用,而bus_deep中passengers是指向另一个列表
bus1.drop('Claire')
print(bus_shallow.passengers)
print(bus_deep.passengers)
print(id(bus1.passengers), id(bus_shallow.passengers), id(bus_deep.passengers))
例子2.deepcopy解决循环引用问题
如果对象有循环引用,那么这个算法会进入无限循环。deepcopy函数会想办法复制对象,解决循环引用。
a = [10 ,20]
b = [a , 30]
a.append(b)
print(a)
from copy import deepcopy
c = deepcopy(a)
print(c)
通过别名共享对象还能解释Python中传递参数的方式,以及使用可变类型作为参数默认值引起的问题。接下来讨论这些问题。
7. 函数的参数作为引用的时候
- 基本类型按值传参,引用类型是共享传参(call by sharing)。
- 共享传参指函数的形式参数获得实参中引用的副本。也就是说,函数内部的形参是实参的别名。这种方案的结果是,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象的id(不能把一个对象替换成另外一个对象)。
例子1. 函数可能会修改接收到的任何可变对象
本例子用+=运算符,实际传入的实参可能会受到影响
def f(a, b):
a += b
return a
#数字没变
x = 1
y = 2
print(f(x,y), x, y)
#列表变了
a = [1, 2]
b = [3, 4]
print(f(a, b), a, b)
#元组没变
t = (10, 20)
u = (30, 40)
print(f(t, u), t, u)
例子2. 说明可变默认值的危险,类HauntedBus(幽灵巴士),从Bus类而来。
不要使用可变类型作为参数的默认值
题外话:可选参数可以有默认值,这是Python函数定义的一个很棒的特性,这样API在进化的同时能保证向后兼容。
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和bus2相互影响
bus1 = HauntedBus()
bus1.pick('Bill')
bus2 = HauntedBus()
print(bus2.passengers)
bus2.pick('Allen')
print(bus1.passengers)
使用默认值的HauntedBus实例会共享同一个乘客列表。
这个问题的根源是,默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。
因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。
例子3. 查看例子2中的函数对象的属性,证明默认值变成了函数对象的属性
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()
bus1.pick('Bill')
bus2 = HauntedBus()
print(bus2.passengers)
bus2.pick('Allen')
print(bus1.passengers)
print(dir(HauntedBus.__init__))
#输出(['Bill', 'Allen'])
print(HauntedBus.__init__.__defaults__)
#bus2.passengers是一个别名,绑定到HauntedBus.__init__.__defaults__属性的第一个元素上
print(HauntedBus.__init__.__defaults__[0] is bus2.passengers)
可变默认值导致的问题说明了为什么通常使用None作为接受可变值的参数的默认值。
例子4. TwilightBus,下车的学生从篮球队中消失了。
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)
这里的问题是,校车为传给构造方法的列表创建了别名,即self.passengers = passengers。正确的做法是,校车自己维护乘客列表。 方法是在__init__中,传入passengers参数时,应该把参数值的副本赋值给self.passengers,即self.passengers = list(passengers)代替self.passengers = passengers。
def __init__(self, passengers):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers)
- list()构造函数创建了另外一个对象,维护相同值,这个就是副本。注意,tuple(t)获得的是同一个对象。这些是CPython实现的细节。
- 更加灵活。传给passengers参数的值可以是元组或者其他可迭代对象。
8. del和垃圾回收
- del语句删除名称,而不是对象。
- del命令可能会导致对象被当作垃圾回收,仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时。
- 重新绑定也可能导致对象的引用计数归零,导致对象被回收。
- 有个__del__特殊方法。即将销毁实例时,Python解释器会调用__del__方法,给实例最后的机会,释放资源(如回收内存)。自己很少需要实现__del__方法,因为很难用对。
- 在CPython中,垃圾回收使用的主要算法是引用计数。但Python的其他实现有更复杂的垃圾回收程序,而且不依赖引用计数。这意味着,对象的引用数为0时可能不会立即调用__del__方法。
例子1. 演示对象生命结束时的情形。并说明del不会删除对象。对象不可获取,从而被删除。
import weakref
s1 = {1, 2, 3}
s2 = s1
def bye():
print('Gone with the wind')
wek_ref = weakref.finalize(s1, bye)
print(wek_ref.alive)
del s1
print(wek_ref.alive)
s2 = 'spam'
print(wek_ref.alive)
我们把s1引用传给finalize函数了,虽然s1,s2都不指向{1, 2, 3}了,为什么{1, 2, 3}对象被销毁了?
这是因为finalize持有{1, 2, 3}的弱引用,为了监控对象和调用回调。
9. 弱引用
弱引用不会增加对象的引用数量。
弱引用不会妨碍所指对象被当做垃圾回收。
如果弱引用指向的对象死亡,弱引用就返回None。
弱引用在缓存应用中很有用,因为我们不想仅因为被缓存引用着而始终保存缓存对象。
例子1. 弱引用是可调用对象,返回的是被引用的对象;如果所指对象不存在了,返回None
Note: 这里隐式赋值给_, _是强引用。
例子1的wearef.ref是底层接口,这里为了做演示。多数程序最好使用WeakKeyDictionary、WeakValueDictionary、WeakSet和finalize(在内部使用弱引用),不要自己手动创建并处理weakref.ref实例。
WeakValueDictionary简介:中文电子书P370
弱引用的局限:中文电子书P372
10. Python对不可变类型施加的把戏。
这是CPython的实现细节。中文电子书P374。
- list(l1),l1[:]返回的都是一个新的对象,tuple(t1)得到同一个元组。
- str、bytes、fronzenset实例也有这种行为。
- 注意,frozenset实例不是序列,因此不能使用fs[:]。但是,fs.copy()是善意的谎言,返回同一个对象的引用而不是创建一个副本。为的是接口的兼容性,节省内存,提高解释器的速度,使得fronzenset的兼容性比set强。
- 两个不可变对象是同一个对象还是副本,对用户来说没什么区别。
另外,
总结:
变量保存的是引用,这一点对Python编程有很多实际的影响。
- 简单的赋值不创建副本。
- 对+=或*=所做的增量赋值来说,如果左边的变量绑定的是不可变对象,会创建新对象;如果是可变对象,就原地修改。
- 为现有的变量赋予新值,不会修改之前绑定的变量。这就重新绑定:变量绑定了其他的对象。如果变量是之前那个对象的最后一个引用,对象会被当作垃圾回收。
- 函数的参数以引用的形式传递。这意味着,函数可能会修改通过参数传入的可变对象。解决与否看需求。如果不想被修改,在函数本地创建副本,或者传入的参数使用不可变对象。(例如传入元组而不是列表)。
- 使用可变类型作为函数参数的默认值有危险,因为如果使用默认值初始化实例,在实例中修改了参数,会影响到以后再使用默认值初始化的实例的调用。
- 在CPython中,对象的引用数量归零后和除了循环引用之外没有其他引用,两个对象都会被销毁。
- 某些情况下,可能需要保存对象的引用,但不保存对象本身。例如,有一个类想要记录所有实例。这个需求可以使用弱引用实现,这是一种底层机制,是weakref模块中WeakValueDictionary、WeakKeyDictionary和WeakSet等有用的集合类以及weakref.finalize函数的底层支持。
- 在Python中比较的是对象的值, is才是比较对象的引用或者标识(id)。而在Java中,比较的是对象(不是基本类型)的引用。
- 当然,自己可以在类中定义__eq__方法,决定==如何比较对象。如果不覆盖__eq__方法,从object几成的方法就是比较对象的ID。
- Python支持重载运算符,不支持函数重载。
- 处理不可变的对象时,变量保存的是真正的底下那个还是共享对象的引用无关紧要。如果a == b成立,而且两个对象都不会变,那么它们就可能是相同的对象。这就是为什么字符串可以安全使用驻留(intering)。仅当对象可变时,对象标识(id)才重要。
- 可变对象是导致多线程编程难以处理的主要原因,因为某个线程改动对象后,如果不正确地同步,那就会损坏数据。但是过度同步又会导致死锁。
- Python没有直接销毁对象的机制,这其实是一个好的特性:如果随时可以销毁对象,那么指向对象的强引用怎么办?
- 在CPython中, 垃圾回收依靠引用计数。
open('test.txt', 'wt', encoding='utf-8').write('1,2,3
')
这段代码在CPython是安全的,一旦引用数量归零,就立即销毁对象,因为文件对象的引用数量会在write方法返回后归零,Python在销毁内存中表示文件的对象之前,会立即关闭文件。然而在JPython和IronPython中却不安全,因为它们使用的是宿主运行时(Java VM和.NET CLR)中的垃圾回收程序,不依靠引用计数,更加复杂。所以,在任何情况下,应该改为以下的代码:
with open('test.txt', 'wt', encoding='utf-8') as fp:
fp.write('1,2,3')