《流畅的Python》读书笔记10(第八章:对象引用、可变性和垃圾回收)

130 阅读6分钟

本章先以一个比喻说明Python的变量:变量是标注,而不是盒子(variables are labels,not boxes)。

8.1 变量不是盒子

>>> a = [1, 2, 3]
>>> b = a
>>> a.append(4)
>>> b
[1, 2, 3, 4]

image.png 变量只不过是标注,所有无法阻止为对象贴上多个标注。贴的多个标注,就是别名。

8.2 标识、相等性和别名

示例8-3 charles和lewis指代同一个对象

>>> charles = {'name':'Charles L. Dodgson', 'born':1832}
>>> lewis = charles
>>> lewis is charles
True
>>> id(charles), id(lewis)
(1367254942848, 1367254942848)
>>> lewis['balance'] = 950
>>> charles
{'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}

image.png 示例8-4 测试另一对象alex

{'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
>>> alex = {'name':'Charles L. Dodgson', 'born':1832, 'balance':950}
>>> alex == charles
True
>>> alex is charles
False

每个变量都有标识类型。对象一旦创建,它的标识绝不会;可以把标识理解为对象在内存中的地址。is运算符比较两个对象的标识;id()函数返回对象标识的整数表示。

编程中很少使用id()函数。标识最常使用is运算符检查,而不是直接比较ID。

8.2.1 在==和is之间选择

==运算符比较两个对象的值,而is比较两个对象的标识。

目前,最常使用is检查变量绑定的值是不是None。下面是推荐的写法:

x is None

否定的正确写法是:

x is not None

is运算符比==速度快,因为它不能重载,所以Python不用寻找并调用特殊方法,而是直接比较两个整数的ID。而a==b是语法糖,等同于a.__eq__(b)。

8.2.2 元组的相对不可变性

元组与多数Python集合一样,保存的是对象的引用。如果引用的元素是可变的,即便元组本身不可变,元素依然可变。

>>> t1 = (1, 2, [30, 40])
>>> t2 = (1, 2, [30, 40])
>>> t1 == t2
True
>>> id(t1[-1])
1367255527624
>>> t1[-1].append(99)
>>> t1
(1, 2, [30, 40, 99])
>>> id(t1[-1])
1367255527624
>>> t1 == t2
False

8.3 默认做浅拷贝

复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法。

>>> L1 = [3, [55, 44], (7, 8, 9)]
>>> L2 = list(L1) #创建L1的副本
>>> L2
[3, [55, 44], (7, 8, 9)]
>>> L1 == L2
True
>>> L2 is L1
False

image.png 为任意对象做深拷贝和浅拷贝 有时我们需要的是深拷贝(即副本不共享内部对象的引用)。copy模块提供的deepcopy和copy函数能为任意对象做深拷贝和浅拷贝。

示例8-8 校车乘客在途中上车和下车

>>> 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)

示例8-9 使用copy和deepcopy

>>> import copy
>>> bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
>>> bus2 = copy.copy(bus1)
>>> bus3 = copy.deepcopy(bus1)
>>> id(bus1), id(bus2), id(bus3)
(1367262177264, 1367262177152, 1367262177488)
>>> bus1.drop('Bill')
>>> bus2.passengers
['Alice', 'Claire', 'David']
>>> id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)
(1367262164936, 1367262164936, 1367262069704)
>>> bus3.passengers
['Alice', 'Bill', 'Claire', 'David']

示例8-10 循环引用:b引用a,然后追加到a中;deepcopy会想办法复制啊

>>> a = [10, 20]
>>> b = [a, 30]
>>> a.append(b)
>>> a
[10, 20, [[...], 30]]
>>> from copy import deepcopy
>>> c = deepcopy(a)
>>> c
[10, 20, [[...], 30]]

8.4 函数的参数作为引用时

Python唯一支持的参数传递模式是共享传参(call by sharing)。

共享传参指函数的各个形式参数获得实参中各个引用的副本。也就是说,函数内部的形参是实参的别名。

示例8-11 函数可能会修改接收到的任意可变对象

>>> def f(a, b):
...     a += b
...     return a
...
>>> x = 1
>>> y = 2
>>> f(x, y)
3
>>> x, y
(1, 2)
>>> a = [1, 2]
>>> b = [3, 4]
>>> f(a, b)
[1, 2, 3, 4]
>>> a, b
([1, 2, 3, 4], [3, 4])
>>> t = (10, 20)
>>> u = (30, 40)
>>> f(t, u)
(10, 20, 30, 40)
>>> t, u
((10, 20), (30, 40))
>>>

8.4.1 不要使用可变类型作为参数的默认值

可选参数可以有默认值,这是Python函数定义的一个很棒的特性,这样我们的API在进化的同时能保证向后兼容。然而,我们应该避免使用可变对象作为参数的默认值。

示例8-12 一个简单的类,说明可变默认值的危险

>>> 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)

示例8-13 备受幽灵乘客折磨的校车

>>> bus1 = HaunterdBus(['Alice', 'Bill'])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'HaunterdBus' is not defined
>>> bus1 = HauntedBus(['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')
>>> bus2.passengers
['Carrie', 'Dave']
>>> bus2.passengers is bus3.passengers
True
>>> bus1.passengers
['Bill', 'Charlie']
>>>

问题在于,没有指定初始乘客的HauntedBus实例会共享同一个乘客列表。

可以审查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'],)

可变默认值导致的这个问题说明了为什么通常使用None作为接收可变值的参数的默认值。

8.4.2 防御可变参数

>>> class TwilightBus:
...     def __init__(self, passengers=None):
...         if passengers is None:
...             self.passengers = []
...         else:
...             self.passengers = list(passengers) # 创建passengers类别的副本

8.5 del和垃圾回收

del语句删除名称,而不是对象。

示例8-16: 没有指向对象的引用时,监视对象生命结束时的情形

>>> import weakref
>>> s1 = {1,2,3}
>>> s2 = s1
>>> def bye():
...     print('Gone with the wind...')
...
>>> ender.weakref.finalize(s1, bye)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'ender' is not defined
>>> ender = weakref.finalize(s1, bye)
>>> ender.alive
True
>>> del s1
>>> ender.alive
True
>>> s2 = 'spam'
Gone with the wind...
>>> ender.alive
False

上面的示例目的是明确指出del不会删除对象,但是执行del操作后可能会导致对象不可获取,从而被删除。

8.6 弱引用

正是因为有引用,对象才会在内存中存在。当对象的引用数量归零后,垃圾回收程序会把对象销毁。但是,有时需要引用对象,而不让对象的存在的时间超过所需的时间。这经常用在缓存。

弱引用不会增加对象的引用数量。引用的目标对象称为所指对象(referent)。弱引用不会妨碍所指对象被当做垃圾回收。

弱引用在缓存中很有用,因为我们不想紧因为被缓存引用着而始终保存缓存对象。

8.6.1 WeakValueDictionary简介

WeakValueDictionary类实现的是一种可变映射,里面的值是对象的弱引用。被引用的对象在程序中的其他地方被当做垃圾回收后,对应的键会自动从WeakValueDictionary中删除。因此,WeakValueDictionary经常用于缓存。

示例8-18 Cheese有个kind属性和标准的字符串表示形式

>>> class Cheese:
...     def __init__(self, kind):
...         self.kind = kind
...     def __repr__(self):
...         return 'Cheese(%r)'%self.kind
...

示例8-19 顾客:“你们店里到底有没有奶酪?”

>>> import weakref
>>> stock = weakref.WeakValueDictionary()
>>> catalog = [Cheese('Red Leicester'), Cheese('Tilsit'), Cheese('Brie'), Cheese('parmesan')]
>>> for cheese in catalog:
...     stock[cheese.kind] = cheese
...
>>> sorted(stock.keys())
['Brie', 'Red Leicester', 'Tilsit', 'parmesan']
>>> del catalog
>>> sorted(stock.keys())
['parmesan']
>>> del cheese
>>> sorted(stock.keys())
[]

示例8-19中,for循环中的变量cheese是全局变量,除非显式删除,否则不会消失。