《流畅的Python》卢西亚诺·拉马略 第8章 读书笔记
8.1 变量不是盒子
为了理解Python中的赋值语句,应该始终先读右边。对象在右边创建或获取,在此之后左边的变量才会绑定到对象上,这就像为对象贴上标注。
因为变量只不过是标注,所以无法阻止为对象贴上多个标注。
8.2 标识、相等性和别名
每个变量都有标识、类型和值。对象一旦创建,它的标识绝不会变;你可以把标识理解为对象在内存中的地址。
==运算符比较两个对象的值(对象中保存的数据)
is比较对象的标识,id()函数返回对象标识的整数表示
最常使用is检查变量绑定的值是否为None
x is None
x is not None
is运算符比==速度快,因为它不能重载,所以Python不用寻找并调用特殊方法,而是直接比较两个整数ID。而a==b是语法糖,等同于a.__eq__(b)。继承自object的__eq__方法比较两个对象的ID,结果与is一样。但是多数内置类型使用更有意义的方式覆盖了__eq__方法,会考虑对象属性的值。
8.3 默认做浅复制
构造方法或[:]做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)。如果所有元素都是不可变的,那么这样没有问题,还能节省内存。但是,如果有可变的元素,可能就会导致意想不到的问题。
为任意对象做深复制和浅复制
有时我们需要的是深复制(即副本不共享内部对象的引用)。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)
交互式控制台中 创建一个Bus实例和两个副本
>>> import copy
>>> from Bus import Bus
>>> bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
>>> bus2 = copy.copy(bus1)
>>> bus3 = copy.deepcopy(bus1)
>>> id(bus1), id(bus2), id(bus3)
(140137100109976, 140137100110144, 140137100110312)
>>> bus1.drop('Bill')
>>> bus1.passengers
['Alice', 'Claire', 'David']
>>> bus2.passengers
['Alice', 'Claire', 'David']
>>> bus3.passengers
['Alice', 'Bill', 'Claire', 'David']
>>> id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)
(140137100086024, 140137100086024, 140137136242952)
>>>
说明:
line4, bus2是bus1的浅复制
line5, bus3是bus1的深复制
line8, bus1中的'Bill'下车
line12, bus2中也没有'Bill'
line15, 审查passengers属性后发现,bus1和bus2共享同一个列表对象
8.4 函数的参数作为引用时
8.4.1 不要使用可变类型作为参数
以例1为基础定义一个新类HauntedBus,然后修改__init__方法。passengers的默认值不是None,而是[],这样就不用像之前那样使用if判断了。这个“聪明的举动”会让我们陷入麻烦。
【例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)
line3 如果没传入passengers参数,使用默认绑定的列表对象,一开始是空列表。
line4 这个赋值语句把self.passengers变成passengers的别名,而没有传入passengers参数时,self.passengers是默认列表的别名。
line6、8 在self.passengers上调用.append()和.remove()方法时,修改的其实是默认列表,它是函数对象的一个属性。
HauntedBus的诡异行为 -->
>>> from Bus import HauntedBus
>>> bus1 = HauntedBus(['Alice', 'Bill'])
>>> bus1.passengers
['Alice', 'Bill']
>>> bus1.pick('Charlie')
>>> bus1.drop('Alice')
>>> bus1.passengers
['Bill', 'Charlie']
>>>
>>> bus2 = HauntedBus()
>>> bus2.pick('Carrie')
>>> bus2.passengers
['Carrie']
>>>
>>> bus3 = HauntedBus()
>>> bus3.passengers
['Carrie']
>>> bus3.pick('Dave')
>>> bus3.passengers
['Carrie', 'Dave']
>>>
>>> bus2.passengers
['Carrie', 'Dave']
>>> bus2.passengers is bus3.passengers
True
>>> bus1.passengers
['Bill', 'Charlie']
>>>
说明:
line7 目前没什么问题,bus1没有出现异常。
line10 一开始,bus2是空的,因此把默认的空列表赋值给self.passengers。
line15 bus3一开始也是空的,因此还是赋值默认的列表。
line16 但是默认列表不为空!
line22 登上bus3的Dave出现在bus2中。
line24 问题是,bus2.passengers和bus3.passengers指代同一个列表。
line26 但bus1.passengers是不同的列表。
问题在于,没有指定初始乘客的HauntedBus实例会共享同一个乘客列表。
实例化HauntedBus时,如果传入乘客,会按预期运作。
但是不为HauntedBus指定初始乘客的话,self.passengers变成了passengers参数默认值的别名。
审查HauntedBus.__init__对象,看看它的__defaults__属性中的那些幽灵学生:
>>> dir(HauntedBus.__init__)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>>
>>> HauntedBus.__init__.__defaults__
(['Carrie', 'Dave'],)
>>>
我们可以验证bus2.passengers是一个别名,它绑定到HauntedBus.__init__.__defaults__属性的第一个元素上:
>>> HauntedBus.__init__.__defaults__[0] is bus2.passengers
True
>>>
可变默认值导致的这个问题说明了为什么通常使用None作为接收可变值的参数的默认值。
在例1中,__init__方法检查passengers参数的值是否为None,如果是就把一个新的空列表赋值给self.passengers。如果passengers不是None,正确的实现会把passengers的副本赋值给self.passengers。
8.4.2 防御可变参数
在__init__中,传入passengers参数时,应该把参数值的副本赋值给self.passengers
除非这个方法确实想修改通过参数传入的对象,否则在类中直接把参数赋值给实例变量之前一定要三思,因为这样会为参数对象创建别名。
8.5 del和垃圾回收
del语句删除名称(对象的引用),而不是对象。
对象才会在内存中存在是因为有引用。当对象的引用数量归零后,垃圾回收程序会把对象销毁。
仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时,del命令会导致对象被当作垃圾回收。
重新绑定也可能会导致对象的引用数量归零,导致对象被销毁。
8.6 弱引用
有时需要引用对象,而不让对象存在的时间超过所需时间,这经常用在缓存中。
弱引用不会增加对象的引用数量 ==> 弱引用不会妨碍所指对象被当作垃圾回收
引用的目标对象称为所指对象(referent),如果所指对象不存在了,返回None
>>> import weakref
>>> a_set = {0, 1}
>>> wref = weakref.ref(a_set) #创建弱引用对象wref
>>> wref
<weakref at 0x7f86f289af98; to 'set' at 0x7f86f0b799e8>
>>> wref() #调用wref()返回的是被引用的对象,{0, 1}。因为这是控制台会话,所以{0, 1}会绑定给_变量
{0, 1}
>>> _
{0, 1}
>>>
>>> a_set = {2, 3, 4} #a_set不再指代{0, 1}集合,因此集合的引用数量减少了。但是_变量仍然指代它。
>>>
>>> _
{0, 1}
>>> wref() #调用wref()依旧返回{0, 1}。
{0, 1}
>>>
>>> wref() is None #计算这个表达式时,{0, 1}存在,因此wref()不是None。
False
>>> _ #随后_绑定到结果值False。现在{0, 1}没有强引用了。
False
>>>
>>> wref() is None #因为{0, 1}对象不存在了,所以wref()返回None。
True
>>>
说明:多数程序最好使用WeakKeyDictionary、WeakValueDictionary、WeakSet和finalize(在内部使用弱引用),不要自己动手创建并处理weakref.ref实例。
8.6.1 WeakValueDictionary简介
WeakValueDictionary类实现的是一种可变映射,里面的值是对象的弱引用。被引用的对象在程序中的其它地方被当作垃圾回收后,对应的键会自动从WeakValueDictionary中删除。因此,WeakValueDictionary经常用于缓存。
class Cheese:
def __init__(self, kind):
self.kind = kind
def __repr__(self):
return 'Cheese(%r)'%self.kind
把catalog中的各种奶酪载入WeakValueDictionary实现的stock中。删除catalog后,stock中只剩下一种奶酪了。你知道为什么帕尔马干酪(Parmesan)比其他奶酪保存的时间长吗?代码后面的提示中有答案。
交互式环境测试:
>>> from Cheese import Cheese
>>> import weakref
>>> stock = weakref.WeakValueDictionary() #stock是WeakValueDictionary实例
>>> catalog = [Cheese('Red Leicester'), Cheese('Tilsit'),Cheese('Brie'), Cheese('Parmesan')]
>>> for cheese in catalog:
... stock[cheese.kind] = cheese #stock把奶酪的名称映射到catalog中Cheese实例的弱引用上
>>> sorted(stock.keys())
['Brie', 'Parmesan', 'Red Leicester', 'Tilsit'] #stock是完整的。
>>> del catalog
>>> sorted(stock.keys())
['Parmesan'] #删除catalog之后,stock中的大多数奶酪都不见了,这是WeakValueDictionary的预期行为。为什么不是全部呢?
>>>
>>> del cheese
>>> sorted(stock.keys())
[]
>>>
临时变量引用了对象,这可能会导致该变量的存在时间比预期长。通常,这对局部变量来说不是问题,因为它们在函数返回时会被销毁。但上述举例中,for循环中的变量cheese是全局变量,除非显式删除,否则不会消失。
与WeakValueDictionary对应的是WeakKeyDictionary,后者的键是弱引用。
8.6.2 弱引用的局限
不是每个Python对象都可以作为弱引用的目标(或称所指对象)。
基本的list和dict实例不能作为所指对象,但是它们的子类可以轻松地解决这个问题;
set实例可以作为所指对象,用户定义的类型也没问题;
int和tuple实例不能作为弱引用的目标,甚至它们的子类也不行。