[Python] 对象引用、可变性和垃圾回收

《流畅的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实例不能作为弱引用的目标,甚至它们的子类也不行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值