第8章 对象引用、可变性和垃圾回收
1 #《流畅的Python》读书笔记 2 # 第四部分 面向对象惯用法 3 # 第8章 对象引用、可变性和垃圾回收 4 5 # 8.1 变量不是盒子 6 # Python 变量类似于 Java 中的引用式变量,因此最好把它们理解为附加在对象上的标注。 7 8 # 示例 8-1 变量 a 和 b 引用同一个列表,而不是那个列表的副本 9 # 对引用式变量来说,说把变量分配给对象更合理,反过来说就有问题。 10 # >>> a=[1,2,3] 11 # >>> b=a 12 # >>> b.append(4) 13 # >>> a 14 # [1, 2, 3, 4] 15 16 # 示例 8-2 创建对象之后才会把变量分配给对象 17 # 为了理解 Python 中的赋值语句,应该始终先读右边。 18 # 对象在右边创建或获取,在此之后左边的变量才会绑定到对象上,这就像为对象贴上标注。 19 # 因为变量只不过是标注,所以无法阻止为对象贴上多个标注。贴的多个标注,就是别名。 20 # >>> x=Gizmo() 21 # ❶ 输出的 Gizmo id: ... 是创建 Gizmo 实例的副作用。 22 # Gizmo id: 34079216 23 # ❷ 在乘法运算中使用 Gizmo 实例会抛出异常。 24 # >>> y=Gizmo() * 10 25 # ❸ 这里表明,在尝试求积之前其实会创建一个新的 Gizmo 实例。 26 # Gizmo id: 35171312 27 # ❹ 但是,肯定不会创建变量 y,因为在对赋值语句的右边进行求值时抛出了异常。 28 # Traceback (most recent call last): 29 # File "<pyshell#9>", line 1, in <module> 30 # y=Gizmo() * 10 31 # TypeError: unsupported operand type(s) for *: 'Gizmo' and 'int' 32 # >>> dir() 33 # ['Gizmo', '__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b', 'x'] 34 35 # 8.2 标识、相等性和别名 36 # Lewis Carroll 是 Charles Lutwidge Dodgson 教授的笔名。Carroll 先生指的就是 Dodgson 教授,二者是同一个人。 37 38 # 示例 8-3 charles 和 lewis 指代同一个对象 39 # >>> charles={'name':'Charles L. Dodgson','born':1832} 40 # >>> lewis=charles 41 # >>> lewis is charles 42 # True 43 # >>> id(charles),id(lewis) 44 # (34927120, 34927120) 45 # >>> lewis['balance']=950 46 # >>> charles 47 # {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950} 48 49 # 示例 8-4 alex 与 charles 比较的结果是相等,但 alex 不是charles 50 # 在那段代码中,lewis 和 charles 是别名,即两个变量绑定同一个对象。 51 # 而 alex 不是 charles 的别名,因为二者绑定的是不同的对象。 52 # alex 和 charles 绑定的对象具有相同的值(== 比较的就是值),但是它们的标识不同。 53 # 每个变量都有标识、类型和值。 54 # 对象一旦创建,它的标识绝不会变;你可以把标识理解为对象在内存中的地址。 55 # is 运算符比较两个对象的标识;id() 函数返回对象标识的整数表示。 56 # >>> alex={'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950} 57 # ❷ 比较两个对象,结果相等,这是因为 dict 类的 __eq__ 方法就是这样实现的。 58 # >>> alex == charles 59 # True 60 # ❸ 但它们是不同的对象。这是 Python 说明标识不同的方式:a is notb。 61 # >>> alex is not charles 62 # True 63 64 # 8.2.1 在==和is之间选择 65 # == 运算符比较两个对象的值(对象中保存的数据),而 is 比较对象的标识。 66 # 通常,我们关注的是值,而不是标识,因此 Python 代码中 == 出现的频率比 is 高。 67 68 # 8.2.2 元组的相对不可变性 69 # 元组与多数 Python 集合(列表、字典、集,等等)一样,保存的是对象的引用。 70 # 如果引用的元素是可变的,即便元组本身不可变,元素依然、可变。 71 72 # 示例 8-5 一开始,t1 和 t2 相等,但是修改 t1 中的一个可变元素后,二者不相等了 73 # ❶ t1 不可变,但是 t1[-1] 可变。 74 # >>> t1=(1,2,[30,40]) 75 # ❷ 构建元组 t2,它的元素与 t1 一样。 76 # >>> t2=(1,2,[30,40]) 77 # ❸ 虽然 t1 和 t2 是不同的对象,但是二者相等——与预期相符。 78 # >>> t1 == t2 79 # True 80 # ❹ 查看 t1[-1] 列表的标识。 81 # >>> id(t1[-1]) 82 # 33289600 83 # ❺ 就地修改 t1[-1] 列表。 84 # >>> t1[-1].append(99) 85 # >>> t1 86 # (1, 2, [30, 40, 99]) 87 # ❻ t1[-1] 的标识没变,只是值变了。 88 # >>> id(t1[-1]) 89 # 33289600 90 # ❼ 现在,t1 和 t2 不相等。 91 # >>> t1 == t2 92 # False 93 94 # 8.3 默认做浅复制 95 # 复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法。 96 # >>> l1=[3,[55,44],(7,8,9)] 97 # ❶ list(l1) 创建 l1 的副本。 98 # >>> l2=list(l1) 99 # >>> l2 100 # [3, [55, 44], (7, 8, 9)] 101 # ❷ 副本与源列表相等。 102 # >>> l2 == l1 103 # True 104 # ❸ 但是二者指代不同的对象。对列表和其他可变序列来说,还能使用简洁的 l2 = l1[:] 语句创建副本。 105 # >>> l1 is l2 106 # False 107 108 # 示例 8-6 为一个包含另一个列表的列表做浅复制;把这段代码复制粘贴到 Python Tutor 网站中,看看动画效果 109 # >>> l1=[3,[66,55,44],(7,8,9)] 110 # ❶ l2 是 l1 的浅复制副本。 111 # >>> l2=list(l1) 112 # ❷ 把 100 追加到 l1 中,对 l2 没有影响。 113 # >>> l1.append(100) 114 # ❸ 把内部列表 l1[1] 中的 55 删除。这对 l2 有影响,因为 l2[1] 绑定的列表与 l1[1] 是同一个。 115 # >>> l1[1].remove(55) 116 # >>> print('li',l1) 117 # li [3, [66, 44], (7, 8, 9), 100] 118 # >>> print('l2',l2) 119 # l2 [3, [66, 44], (7, 8, 9)] 120 # >>> l2[1] += [33,22] 121 # >>> l2[2] += (10,11) 122 # >>> print('l1',l1) 123 # l1 [3, [66, 44, 33, 22], (7, 8, 9), 100] 124 # >>> print('l2',l2) 125 # l2 [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)] 126 127 # 示例 8-8 校车乘客在途中上车和下车 128 class Bus: 129 def __init__(self,passengers): 130 if passengers is None: 131 self.passengers=[] 132 else: 133 self.passengers=list(passengers) 134 def pick(self,name): 135 self.passengers.append(name) 136 def drop(self,name): 137 self.passengers.remove(name) 138 139 # 示例 8-9 使用 copy 和 deepcopy 产生的影响 140 # >>> import copy 141 # >>> bus1=Bus(['Alice','Bill','Claire','David']) 142 # >>> bus2=copy.copy(bus1) 143 # >>> bus3=copy.deepcopy(bus1) 144 # >>> id(bus1),id(bsu2),id(bus3) 145 # Traceback (most recent call last): 146 # File "<pyshell#44>", line 1, in <module> 147 # id(bus1),id(bsu2),id(bus3) 148 # NameError: name 'bsu2' is not defined 149 # >>> id(bus1),id(bus2),id(bus3) 150 # (35175888, 35175856, 37550768) 151 # ❷ bus1中的'Bill'下车后,bus2中也没有他了。 152 # >>> bus2.passengers 153 # ['Alice', 'Claire', 'David'] 154 # >>> id(bus1.passengers),id(bus2.passengers),id(bus3.passengers) 155 # ❸ 审查passengers属性后发现,bus1和bus2共享同一个列表对象,因为bus2是bus1的浅复制副本。 156 # (37562848, 37562848, 37609760) 157 # ❹ bus3是bus1的深复制副本,因此它的passengers属性指代另一个列表。 158 # >>> bus3.passengers 159 # ['Alice', 'Bill', 'Claire', 'David'] 160 161 # 示例 8-10 循环引用:b 引用 a,然后追加到 a 中;deepcopy 会想办法复制 a 162 # >>> a=[10,20] 163 # >>> b=[a,30] 164 # >>> a.append(b) 165 # >>> a 166 # [10, 20, [[...], 30]] 167 # >>> from copy import deepcopy 168 # >>> c=deepcopy(a) 169 # >>> c 170 # [10, 20, [[...], 30]] 171 172 # 8.4 函数的参数作为引用时 173 # Python 唯一支持的参数传递模式是共享传参(call by sharing)。 174 # 共享传参指函数的各个形式参数获得实参中各个引用的副本。 175 # 也就是说,函数内部的形参是实参的别名。 176 # >>> def f(a,b): 177 # a += b 178 # return a 179 # >>> x=1 180 # >>> y=2 181 # >>> f(x,y) 182 # 3 183 # ❶ 数字x没变。 184 # >>> x,y 185 # (1, 2) 186 # >>> a=[1,2] 187 # >>> b=[3,4] 188 # >>> f(a,b) 189 # [1, 2, 3, 4] 190 # ❷ 列表a变了。 191 # >>> a,b 192 # ([1, 2, 3, 4], [3, 4]) 193 # >>> t=(10,20) 194 # >>> u=(30,40) 195 # >>> f(t,u) 196 # (10, 20, 30, 40) 197 # >>> t,u 198 # ❸ 元组t没变。 199 # ((10, 20), (30, 40)) 200 201 # 8.4.1 不要使用可变类型作为参数的默认值 202 # 可选参数可以有默认值,这是 Python 函数定义的一个很棒的特性,这样我们的 API 在进化的同时能保证向后兼容。 203 # 然而,我们应该避免使用可变的对象作为参数的默认值。 204 205 # 示例 8-12 一个简单的类,说明可变默认值的危险 206 class HauntedBus: 207 """备受幽灵乘客折磨的校车""" 208 # ❶ 如果没传入passengers参数,使用默认绑定的列表对象,一开始是空列表。 209 def __init__(self,passengers=[]): 210 self.passengers = passengers 211 def pick(self,name): 212 self.passengers.append(name) 213 def drop(self,name): 214 self.passengers.remove(name) 215 216 # 示例 8-13 备受幽灵乘客折磨的校车 217 # >>> bus1=HauntedBus(['Alice','Bill']) 218 # >>> bus1.passengers 219 # ['Alice', 'Bill'] 220 # >>> bus1.pick('Charles') 221 # >>> bus1.drop('Alice') 222 # ❶ 目前没什么问题,bus1 没有出现异常。 223 # >>> bus1.passengers 224 # ['Bill', 'Charles'] 225 # ❷ 一开始,bus2 是空的,因此把默认的空列表赋值给self.passengers。 226 # >>> bus2=HauntedBus() 227 # >>> bus2.pick('Carrie') 228 # >>> bus2.passengers 229 # ['Carrie'] 230 # ❸ bus3 一开始也是空的,因此还是赋值默认的列表。 231 # >>> bus3=HauntedBus() 232 # ❹ 但是默认列表不为空! 233 # >>> bus3.passengers 234 # ['Carrie'] 235 # ❺ 登上 bus3 的 Dave 出现在 bus2 中。 236 # >>> bus3.pick('Dave') 237 # >>> bus2.passengers 238 # ['Carrie', 'Dave'] 239 # ❻ 问题是,bus2.passengers 和 bus3.passengers 指代同一个列表。 240 # >>> bus2.passengers is bus3.passengers 241 # True 242 # ❼ 但 bus1.passengers 是不同的列表。 243 # >>> bus1.passengers 244 # ['Bill', 'Charles'] 245 246 # 可以审查 HauntedBus.__init__ 对象,看看它的 __defaults__ 属性中的那些幽灵学生: 247 # >>> dir(HauntedBus.__init__) 248 # ['__annotations__', '__call__', ...,'__defaults__',...] 249 # >>> HauntedBus.__init__.__defaults__ 250 # (['Carrie', 'Dave'],) 251 # 最后,我们可以验证 bus2.passengers 是一个别名,它绑定到HauntedBus.__init__.__defaults__ 属性的第一个元素上: 252 # >>> HauntedBus.__init__.__defaults__[0] is bus2.passengers 253 # True 254 255 # 8.4.2 防御可变参数 256 # 如果定义的函数接收可变参数,应该谨慎考虑调用方是否期望修改传入的参数。 257 258 # 示例 8-15 一个简单的类,说明接受可变参数的风险 259 class TwilightBus: 260 """让乘客销声匿迹的校车""" 261 def __init__(self,passengers=None): 262 if passengers is None: 263 # ❶ 这里谨慎处理,当passengers为None时,创建一个新的空列表。 264 self.passengers = [] 265 else: 266 self.passengers = passengers 267 def pick(self,name): 268 self.passengers.append(name) 269 def drop(self,name): 270 self.passengers.remove(name) 271 272 # 示例8 - 14 从TwilightBus下车后,乘客消失了 273 # ❶ basketball_team 中有 5 个学生的名字。 274 # >>> basketball_team=['Sue','Tina','Maya','Diana','pat'] 275 # ❷ 使用这队学生实例化 TwilightBus。 276 # >>> bus=TwilightBus(basketball_team) 277 # ❸ 一个学生从 bus 下车了,接着又有一个学生下车了。 278 # >>> bus.drop('Tina') 279 # >>> bus.drop('pat') 280 # ❹ 下车的学生从篮球队中消失了! 281 # >>> basketball_team 282 # ['Sue', 'Maya', 'Diana'] 283 284 # 8.5 del和垃圾回收 285 # 对象绝不会自行销毁;然而,无法得到对象时,可能会被当作垃圾回收。 286 # del 语句删除名称,而不是对象。 287 # del 命令可能会导致对象被当作垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时。 288 # 重新绑定也可能会导致对象的引用数量归零,导致对象被销毁。 289 290 # 示例 8-16 没有指向对象的引用时,监视对象生命结束时的情形 291 # >>> import weakref 292 # >>> s1={1,2,3} 293 # ❶ s1 和 s2 是别名,指向同一个集合,{1, 2, 3}。 294 # >>> s2=s1 295 # ❷ 这个函数一定不能是要销毁的对象的绑定方法,否则会有一个指向对象的引用。 296 # >>> def bye(): 297 # print('Gone with the wind...') 298 # ❸ 在 s1 引用的对象上注册 bye 回调。 299 # >>> ender=weakref.finalize(s1,bye) 300 # ❹ 调用 finalize 对象之前,.alive 属性的值为 True。 301 # >>> ender.alive 302 # True 303 # >>> del s1 304 # ❺ 如前所述,del 不删除对象,而是删除对象的引用。 305 # >>> ender.alive 306 # True 307 # ❻ 重新绑定最后一个引用 s2,让 {1, 2, 3} 无法获取。对象被销毁了,调用了 bye 回调,ender.alive 的值变成了 False。 308 # >>> s2='spam' 309 # Gone with the wind... 310 # >>> ender.alive 311 # False 312 313 # 8.6 弱引用 314 # 正是因为有引用,对象才会在内存中存在。当对象的引用数量归零后,垃圾回收程序会把对象销毁。 315 # 但是,有时需要引用对象,而不让对象存在的时间超过所需时间。这经常用在缓存中。 316 317 # 示例 8-17 弱引用是可调用的对象,返回的是被引用的对象;如果所指对象不存在了,返回 None 318 # >>> import weakref 319 # >>> a_set={0,1} 320 # ❶ 创建弱引用对象 wref,下一行审查它。 321 # >>> wref=weakref.ref(a_set) 322 # >>> wref 323 # <weakref at 0x023D19C0; to 'set' at 0x02185558> 324 # ❷ 调用wref()返回的是被引用的对象,{0, 1}。因为这是控制台会话,所以{0, 1}会绑定给_变量。 325 # >>> wref() 326 # {0, 1} 327 # ❸ a_set 不再指代 {0, 1} 集合,因此集合的引用数量减少了。但是 _变量仍然指代它。 328 # >>> a_set={2,3,4} 329 # ❹ 调用 wref() 依旧返回 {0, 1}。 330 # >>> wref() 331 # {0, 1} 332 # ❺ 计算这个表达式时,{0, 1} 存在,因此 wref() 不是 None。 333 # 但是,随后 _ 绑定到结果值 False。现在 {0, 1} 没有强引用了。 334 # >>> wref() is None 335 # ❻ 因为 {0, 1} 对象不存在了,所以 wref() 返回 None。 336 # False 337 # >>> wref() is None 338 # True 339 340 # 8.6.1 WeakValueDictionary简介 341 # WeakValueDictionary 类实现的是一种可变映射,里面的值是对象的弱引用。 342 # 被引用的对象在程序中的其他地方被当作垃圾回收后,对应的键会自动从 WeakValueDictionary 中删除。 343 # 因此,WeakValueDictionary 经常用于缓存。 344 class Cheese: 345 def __init__(self,kind): 346 self.kind=kind 347 def __repr__(self): 348 return 'Cheese(%r)'%self.kind 349 350 # 示例 8-19 顾客:“你们店里到底有没有奶酪?” 351 >>> import weakref 352 >>> stock = weakref.WeakValueDictionary() 353 >>> catalog=[Cheese('red Leicester'),Cheese('Tilsit'),Cheese('Brie'),Cheese('Parmesan')] 354 >>> for cheese in catalog: 355 stock[cheese.kind]=cheese 356 >>> sorted(stock.keys()) 357 ['Brie', 'Parmesan', 'Tilsit', 'red Leicester'] 358 >>> del catalog 359 >>> sorted(stock.keys()) 360 ['Parmesan'] 361 >>> del cheese 362 >>> sorted(stock.keys()) 363 [] 364 365 # 8.6.2 弱引用的局限 366 # 不是每个 Python 对象都可以作为弱引用的目标(或称所指对象)。基本的 list 和 dict 实例不能作为所指对象,但是它们的子类可以轻松地解决这个问题: 367 368 # 8.7 Python对不可变类型施加的把戏 369 # 你可以放心跳过本节。这里讨论的是 Python 的实现细节,对Python 用户来说没那么重要。 370 371 # 8.8 本章小结 372 # 每个 Python 对象都有标识、类型和值。只有对象的值会不时变化。 373 374 # 8.9 延伸阅读