对象引用、可变性、垃圾回收之2
默认做浅复制
复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法。
l1 = [1, [2, 3], (4, 5, 6)]
l2 = list(l1) # list(l1)创建l1的副本
print(l2)
print(l2 == l1) # 副本与源列表相等
True
print(l2 is l1) # 但是二者只带不同的对象
构造方法和[:]做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)
如果所有的元素都是不可变的,那么没毛病,还能节省内存,但如果有可变元素,可能会导致意想不到的问题。
Why?
l1 = [2, [3,4,5], (6,7,8)]
l2 = list(l1)
l1.append(100)
l1[1].remove(3)
print('l1->',l1)
print('l2->',l2)
l2[1] += [66, 88]
l2[2] += (22, 33)
print('l1->', l1)
print('l2->', l2)
分析:l2 = list(l1) 赋值后的程序状态,l1和l2指代不同的列表,l1 和 l2中的列表最终值还是一致的,但是元组的内容不一致了,l2重新创建了一个元组。
为任意对象做深复制和浅复制
浅复制没什么问题,有时需做深复制(即副本不共享内部对象的引用),copy模块提供了deepcopy和copy来为任意对象做深复制和浅复制。
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(['x', 'q', 'l'])
bus2 = copy.copy(bus1)
bus3 = copy.deepcopy(bus1)
print(id(bus1), id(bus2), id(bus3))
140343326769896 140343326772080 140343326771296
bus1.drop('x')
print(bus2.passengers)
['q', 'l']
print(id(bus1), id(bus2), id(bus3))
140343326769896 140343326772080 140343326771296
print(bus3.passengers)
['x', 'q', 'l']
小结:一般来说,深复制不是一件简单的事,如果对象有循环引用,那么会进入无限循环。deepcopy会记住已经复制的对象,因此能优雅的处理循环引用。
eg:
a = [1, 2]
b = [a,4]
a.append(b)
print(a)
[1, 22, [[...], 4]]
from copy import deepcopy
c=deepcopy(a)
print(c)
思考:为什么通过别名共享对象能解释python中传递参数的方式,及使用可变类型作为参数默认值会产生什么问题?
函数的参数作为引用时
python唯一支持的参数传递模式是共享传参(call by sharing)
什么是共享传参?
共享传参指函数的各个形式参数获得各个引用的副本,即:函数内部的形参是实参的别名。
造成的后果?
这种方案的结果是,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象的标识(即不能把一个对象替换成另一个对象)
不要使用可变类型作为参数的默认值
class Bus:
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'])
print(bus1.passengers)
bus1.pick('Charlie')
bus1.drop('Alice')
print(bus1.passengers)
bus2 = HauntedBus()
bus2.pick('Carrie')
bus2.passengers
bus3 = HauntedBus()
bus3.passengers
bus3.pick('Dave')
print(bus2.passengers)
bus2.passengers is bus3.passengers
print(bus1.passengers)
防御可变参数
如果定义的函数接收可变参数,应该谨慎考虑调用方是否期望修改传入的参数。
TwilightBus 实例与客户共享乘客列表,产生意料之外的结果。
从 TwilightBus 下车后,乘客消失了
>>> basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat']
>>> bus = TwilightBus(basketball_team)
>>> bus.drop('Tina')
>>> bus.drop('Pat')
>>> basketball_team
['Sue', 'Maya', 'Diana']
TwilightBus 违反了设计接口的最佳实践,即“最少惊讶原则”。学生从校车中下车后,她的名字就从篮球队的名单中消失了,这确实不合理。
class TwilightBus:
"""惊魂校车"""
def __init__(self, passengers=None):
if passengers is None:
passengers = [] # 谨慎处理,当passengers为None时,创建一个新的空list
else:
self.passengers = passengers # self.passengers 变成 passengers 的别名,而passengers是传给 __
init__ 方法的实参
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
小结:
问题所在:校车为传给构造方法的列表创建了别名
正确的做法:校车自己维护乘客列表
修正的方法:在__init__中,传入passengers参数时,应该把参数值的副本赋值给self.passengers
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers) # 创建passengers列表的副本;如果不是列表,就将其转为list
这样,在内部像这样处理乘客列表,就不会影响初始化校车时传入的参数了。
注意事项:
除非这个方法确实想通过参数传入的对象,否则在类中直接把参数赋值给实例变量之前一定要三思,因为这样会为参数对象创建别名。如果不确定,那就创建副本。
del和垃圾回收
对象绝对不会自行销毁,然而,无法得到对象时,可能会被当作垃圾回收。
del语句删除名称,而不是对象,del命令可能会导致对象被当作垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时。重新绑定也可能会导致对象的引用计数量归0,导致对象被销毁。
注意事项:
"""有个__del__ 特殊方法,但是它不会销毁实例,不应该在代码中调用。即将销毁实例时,Python 解释器会调用__del__ 方法,给实例最后的机会,释放外部资源。自己编写的代码很少需要实现 __del__ 代码,有些 Python 新手会花时间实现,但却吃力不讨好,因为 __del__ 很难用对。"""
在Cpython中,垃圾回收使用的主要算法是引用计数。
实际上,每个对象都会统计有多少引用指向自己。当引用计数归零时,对象立即就被销毁:Cpython会在对象上调用__del__方法(如果定义了),然后释放分配给对象的内存。
CPython 2.0 增加了分代垃圾回收算法,用于检测引用循环中涉及的对象组——如果一组对象之间全是相互引用,即使再出色的引用方式也会导致组中的对象不可获取。Python 的其他实现有更复杂的垃圾回收程序,而且不依赖引用计数,这意味着,对象的引用数量为零时可能不会立即调用__del__方法.
没有指向对象的引用时,监视对象生命结束时的情形
import weakref
s1 = {1,2,3}
s2 = s1 # s1、s2 是别名,指向同一个集合
def bye():
# 此函数一定不能是要销毁的对象的绑定方法,否则会有一个指向对象的引用
print('Lin with the wind')
ender = weakref.finalize(s1, bye) # 在s1引用的对象上注册bye回调
ender.alive # 调用 finalize 对象之前,.alive 属性的值为 True
True
del s1 # del不删除对象,而是删除对象的引用
ender.alive
True
s2 = 'spam' # 重新绑定最后一个引用s2,让{1,2,3}无法获取,对象被销毁了,调用了bye回调
ender.alive
False
小结:
del不会删除对象,但是执行del操作后可能会导致对象不可获取,从而被删除
疑惑?
为什么{1,2,3}对象被销毁了?毕竟,把s1引用传给finalize函数了,而为了监控对象和调用回调,必要有引用,因为finalize持有{1,2,3}的弱引用。
弱引用
正是因为有引用,对象才会在内存中存在。
当对象的引用数量归零后,垃圾回收程序会把对象销毁。但是,有时需要引用对象,而不让对象存在的时间超过所需时间。这经常用在缓存中。
弱引用三剑客弱引用不会增加对象的引用数量。
引用的目标对象称为所指对象(referent)。
弱引用不会妨碍所指对象被当作垃圾回收。
弱引用在缓存应用中很有用,因为我们不想仅因为被缓存引用着而始终保存缓存对象。
弱引用是可调用的对象,返回的是被引用的对象;如果所指对象不存在了,返回 None
注意事项:
weakref 模块的文档(http://docs.python.org/3/library/weakref.html)指出,weakref.ref 类其 实是低层接口,供高级用途使用,多数程序最好使用 weakref 集合和 finalize。也就是说, 应该使用 WeakKeyDictionary、WeakValueDictionary、WeakSet 和 finalize(在内部使用弱 引用),不要自己动手创建并处理 weakref.ref 实例。
本章总结:
变量保存的是引用,这一点对python变成有很多实际的影响简单的复制不创建副本
对+=或*=所做的增量赋值来说,如果左边的变量绑定的是不可变对象,会创建新对象;如果是可变对象,会就地修改。
为现有的变量赋予新值,不会修改之前绑定的变量。这叫重新绑定:现在变量绑定了其他对象。如果变量是之前那个对象的最后一个引用,对象会被当作垃圾回收。
函数的参数以别名的形式传递,这意味着,函数可能会修改通过参数传入的可变对象。这一行为无法避免,除非在本地创建副本,或者使用不可变对象(即传入元组,而不传入列表)
使用可变类型作为函数参数的默认值有危险,因为如果就地修改了参数,默认值也就变 了,这会影响以后使用默认值的调用。
可变对象还是导致多线程编程难以处理的主要原因,因为某个线程改动对象后,如果不正确地同步,那就会损坏数据。但是过度同步又会导致死锁。