《流畅的Python》卢西亚诺·拉马略 第8章 读书笔记
为了理解Python中的赋值语句,应该始终先读右边。对象在右边创建或获取,在此之后左边的变量才会绑定到对象上,这就像为对象贴上标注。
因为变量只不过是标注,所以无法阻止为对象贴上多个标注。
每个变量都有标识、类型和值。对象一旦创建,它的标识绝不会变;你可以把标识理解为对象在内存中的地址。
==运算符比较两个对象的值(对象中保存的数据)
is比较对象的标识,id()函数返回对象标识的整数表示
最常使用is检查变量绑定的值是否为None
x is None
x is not None
is运算符比==速度快,因为它不能重载,所以Python不用寻找并调用特殊方法,而是直接比较两个整数ID。而a==b是语法糖,等同于a.__eq__(b)。继承自object的__eq__方法比较两个对象的ID,结果与is一样。但是多数内置类型使用更有意义的方式覆盖了__eq__方法,会考虑对象属性的值。
构造方法或[:]做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)。如果所有元素都是不可变的,那么这样没有问题,还能节省内存。但是,如果有可变的元素,可能就会导致意想不到的问题。
为任意对象做深复制和浅复制
有时我们需要的是深复制(即副本不共享内部对象的引用)。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共享同一个列表对象
以例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。
在__init__中,传入passengers参数时,应该把参数值的副本赋值给self.passengers
除非这个方法确实想修改通过参数传入的对象,否则在类中直接把参数赋值给实例变量之前一定要三思,因为这样会为参数对象创建别名。
del语句删除名称(对象的引用),而不是对象。
对象才会在内存中存在是因为有引用。当对象的引用数量归零后,垃圾回收程序会把对象销毁。
仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时,del命令会导致对象被当作垃圾回收。
重新绑定也可能会导致对象的引用数量归零,导致对象被销毁。
有时需要引用对象,而不让对象存在的时间超过所需时间,这经常用在缓存中。
弱引用不会增加对象的引用数量 ==> 弱引用不会妨碍所指对象被当作垃圾回收
引用的目标对象称为所指对象(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实例。
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,后者的键是弱引用。
不是每个Python对象都可以作为弱引用的目标(或称所指对象)。
基本的list和dict实例不能作为所指对象,但是它们的子类可以轻松地解决这个问题;
set实例可以作为所指对象,用户定义的类型也没问题;
int和tuple实例不能作为弱引用的目标,甚至它们的子类也不行。