流畅的python:对象引用、可变性、垃圾回收-Part2

第八章 对象引用、可变性、垃圾回收-Part2

1、回顾

我们首先对上一部分的内容进行简单的回顾:

  • 变量的产生:这个地方是我觉得上一部分最重要的地方,一定要注意,python首先创建一个对象,然后变量名对其进行标注。对应于赋值语句中,先执行=号右侧的对象创建语句,然后将该对象绑定到-号左侧的变量名上。
  • 序列保存方式:python中大多数的序列保存的是子序列的引用,所以尽管对于tuple这类不可变序列来说,只要地址不变,可变子序列的值可以修改。
  • ==与is的区别:判断的内容是否相同,is判断的是内存地址是否一样。事实上,我们有时判断内容更多一点,所以==出现频率更大。然而,在变量和单例值之间比较时,应该使用is。目前,最常使用is检查变量绑定的值是不是None。
  • 浅复制与深复制:构造函数,切片等属于浅复制

2、函数参数

Python中的函数参数到底是传值还是传引用呢?在我学习的时候我也一直很迷糊,其中最经典的是说可变对象传引用,不可变对象传值,我当时觉得很有道理,直到我看了这本书才知道,这样的说法并不对。Python唯一支持的参数传递模式是共享传参,换句话说函数内部的形参是实参的别名,他们指向的是同一个对象。这样做的后果是函数可能会修改作为参数传入的可变对象,但是无法修改那些对象的标识(即不能把一个对象替换成另一个对象)。比如下面的这个例子:

def f(a, b):
    print(id(a))
    a += b
    print(id(a))
    return a


x1 = 'hello, '
y1 = 'world!'
x2 = [1, 2]
y2 = [3, 4]
print(id(x1))
print(f(x1, y1)) # 1551664293104 1551664293104 1551684081840 hello, world!
print(id(x2))
print(f(x2, y2)) # 1551663242184 1551663242184 1551663242184 [1, 2, 3, 4]

我们首先创建了一个字符串对象,并绑定变量名x1,其地址是1551664293104,再传入函数以后,形参a的地址还是1551664293104,这就说明他们是指向同一个对象的。而在进行增量赋值的同时,对于不可变元素a,其基本过程是先计算a+b,然后又新创建一个地址,把值再绑定给变量a,所以地址改变为1551684081840。而对于可变序列x2来说,实参和形参都指向同一个对象,在增量计算时,相当于运行了a.extend(b),所以地址依然不变。

根据上面的例子可以看出,形参和实参事实上都是指向同一个对象,而不可变元素地址的改变,纯粹是函数定义中出现了新的赋值运算导致的。

所以对于函数的传参,一定要注意传参的可变性以及默认值的可变性,因为可变对象的形参和实参指向同一个对象,函数内部的修改同样会带来外部的改变,例如下面的简单例子:

def add_list(alist, added=[]):
    '''将两个列表拼接起来'''
    added += alist
    return added

print(add_list([5]))
print(add_list([2]))

# 输出
[5]
[5, 2]

当第一次拼接的时候,由于没有added,所以使用默认的空列表,结果为[5]没有问题,但是当第二次调用时,应该返回[2],但实际返回[5, 2]。根本原因是默认参数在函数被调用的时候仅仅被评估一次,以后都会使用第一次评估的结果。因此实际上对象空间里面缺省值如果是常数,则每次调用该常数;如果缺省值是变量(比如列表),每次调用都会将缺省值初始化为该列表。如果第一次调用的时候缺省列表发生变化,在第二次调用时,初始化会读取已变化的列表地址。对于这种情况,建议采用不可变序列或者直接赋值为None。

3、del和垃圾回收

del语句删除引用名称,而不是对象。在没有指向对象的引用时,python会自动对该对象进行销毁。为了便于说明,使用weakref.finalize注册一个回调函数监控对象生命结束时的情形。

import weakref
s1 = {1, 2, 3}
s2 = s1       # ➊ 


def bye():    # ➋
    print('这个对象已经随风而去了')


ender = weakref.finalize(s1, bye)  # ➌
del s1
ender.alive # True

del s2 # 这个对象已经随风而去了
ender.alive # False

在➊ 创建了一个集合对象,并绑定了两个别名s1和s2;然后我们 ➌中注册了一个监控ender,当集合对象消失时,会调用bye()。当删除s1时,有s2绑定着集合对象,但是一旦删除s2,没有指向集合{1,2,3}的变量,此时该对象就被销毁了。

你可能觉得奇怪,我们把s1引用也传给finalize函数了,而为了监控对象和调用回调,必须要有引用。为什么示例中的{1, 2, 3}对象最终还是被销毁了?这是因为,finalize持有{1, 2, 3}的弱引用。

4、弱引用

  1. 弱引用的定义

正是因为有引用,对象才会在内存中存在。当对象的引用数量归零后,垃圾回收程序会把对象销毁。引用的目标对象称为所指对象(referent),但是弱引用不会增加对象的引用数量,也不会妨碍所指对象被当作垃圾回收。弱引用在缓存应用中很有用,因为我们不想仅因为被缓存引用着而始终保存缓存对象。

使用weakref.ref实例获取所指对象。如果对象存在,调用弱引用可以获取对象;否则返回None。我们看下面的这个例子(在控制台运行,请留意注释):

>>> import weakref
>>> a_set = {0, 1} # 创建一个集合对象,然后绑定到a_set上
>>> wref = weakref.ref(a_set) # 弱引用:调用所指对象
>>> wref
<weakref at 0x100637598; to 'set' at 0x100636748>
>>> wref( )# 当对象存在时,弱引用返回所指对象
{0, 1}
>>> a_set = {2, 3, 4} # 新的对象绑定给a_set
>>> wref( )#  ➊ 对象没有被销毁?
{0, 1}
>>> wref( ) is None # ➋ 对象没有被销毁?
False
>>> wref( ) is None  # ➌ 咋的又没了?
True

以这个例子作为说明是为了说明微观内存管理的艰难,所以我们一定要充分明白里面的原理。在第8行中已经给a_set分配了新的对象,但是原始的{0,1}对象没有被销毁,{0,1}应该没有引用了才对呀!这是因为Python控制台会自动把_变量绑定到结果不为None的表达式结果上。我想大家应该知道,在python控制台中,_返回上一次运算的结果,这就是上述程序出现灵异时间的原因。而当运行➋第十一行后,_会绑定到十一行运行的结果False上,这时{0,1}才是真正的没有引用,此对象被销毁,所以#➌第十三行返回None。

  1. WeakValueDictionary

weakref.ref类其实是低层接口,供高级用途使用,所以我们尽量不要自己动手创建并处理weakref.ref实例。在多数程序最好使用weakref集合和finalize。也就是说,应该使用WeakKeyDictionary、WeakValueDictionary、WeakSet和finalize(在内部使用弱引用)。

WeakValueDictionary类实现的是一种可变映射,里面的值是对象的弱引用。被引用的对象在程序中的其他地方被当作垃圾回收后,对应的键会自动从WeakValueDictionary中删除。因此,WeakValueDictionary经常用于缓存。假设小王打牌时手里有一炸,我们现在用WeakValueDictionary来模拟以下这个例子:

import weakref
stock = weakref.WeakValueDictionary()
suits = 'spades diamonds clubs hearts'.split()


class Card:
    def __init__(self, rank, suit):
        self.suit = suit
        self.rank = rank


cards = [Card('A', eachsuit) for eachsuit in suits]  # 剩下四张A
for card in cards:
    stock[card.suit] = card
print(list(stock.keys())) # ['spades', 'diamonds', 'clubs', 'hearts']

del cards # 现在牌全打完了
print(list(stock.keys())) # ['hearts']
del card
print(list(stock.keys())) # []

在对四张牌创建弱引用以后,删除cards却仍保留了一个[‘hearts’],这是因为for循环中的变量card是全局变量,除非显式删除,否则不会消失。在删除以后,全部对象都被销毁,WeakValueDictionary为空。

  1. 弱引用的局限

不是每个Python对象都可以作为弱引用的目标(或称所指对象)。基本的list、dict、str等实例不能作为所指对象,但是它们的子类可以:

import weakref
stock = weakref.WeakValueDictionary()
a = [1, 2, 3]
stock['a'] = a # 报错 TypeError: cannot create weak reference to 'list' object


class Mylist(list):
    pass
alist = Mylist(a)
stock['b'] = alist # 成功运行

int,tuple等实例的子类也不可以作为所指对象:

import weakref
stock = weakref.WeakValueDictionary()
a = 2, 2, 3
stock['a'] = a
# TypeError: cannot create weak reference to 'tuple' object

class Mytuple(tuple):
    pass

atuple = Mytuple(a)
stock['b'] = atuple 
# TypeError: cannot create weak reference to 'Mytuple' object

5、不可变对象的陷阱(选读)

我们在上一章曾经说过,python中的通过构造函数,切片等创建变量属于浅复制,然而对于有一些不可变对象却发生了例外:

a = 2, 2, 3
b = tuple(a)
c = a[:]
id(a), id(b), id(c)
# (1551686953624, 1551686953624, 1551686953624)

不管是构造函数还是切片居然返回的是同一对象,str、bytes等实例也有这种行为,甚至会出现明明创建的不同的对象,但是依然地址相同,指向同一对象:

s1 = '123'
s2 = '123'
s1 is s2
# True

对于整数来说,甚至还有更怪异的:

# 小的数
n1 = 1
n2 = 1
n1 is n2 # True

# 大的数
n1 = 1000
n2 = 1000
n1 is n2 # False

事实上,这是一种优化措施,因为不可变对象来说最大的功能就是读取,通过共享字符串字面量可以减少内存,在小的整数上可以防止重复创建“热门”数字,如0、-1和42,这种优化措施称之为驻留。

驻留是Python解释器内部使用的一个特性。尽管存在这种驻留机制,但是千万不要依赖字符串或整数的驻留!CPython不会驻留所有字符串和整数,驻留的条件是实现细节,而且没有文档说明。对我们最大的作用恐怕只有两个:

1、警示我们两个比较字符串或整数是否相等时,应该使用==,而不是is。

2、用来。。

——本章完——
欢迎关注我的微信公众号
扫码关注公众号

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值