Skip to main content

弱引用

我们来看这么一个程序。我们定义了一个非常简单的 class Player,在这个 Player 的 __init__ 函数里,我们给它赋值了一个唯一的 id。同时这里注意一下,我们把建立的 Player 给放到了一个全局的字典 players 里。那这个字典可能有很多用处,比如说记录哪些 id 已经被使用了,或者说它可能是某种形式的 cache,用来找到这个 Player。那我相信大家都有过这种用全局字典或者缓存字典的经历。

缓存中的对象​

接下来我们定义一个函数 game,也就是一场游戏。当然这个函数我们写得非常非常简单,我们只是拿来举例子。在每一场游戏里面,我们建立两个玩家,然后后面可能这两个玩家需要干点什么事,这些我们就没写。那在这个 game 里面,我们可能会用到这个全局字典 players,对吧?这些都不重要,跟我们今天的内容无关。

这里我们需要在意的是,在这个 game 函数运行结束之后,这个 p1 和 p2 就没有用了。在下一场游戏开始的时候,我们建立的是两个全新的 p1 和 p2。

players = {}

class Player:
def __init__(self):
for i in range(1000):
if i not in players:
self.id = i
break
players[self.id] = self

def game():
p1 = Player()
p2 = Player()

for _ in range(10):
game()

print(players)

那可能已经有小伙伴发现这么写的一个问题了。我们假设我们运行了十场游戏,我们会发现,players 这个字典越来越大了。我们一直在不断地往这个字典里面添加内容,从来没有把这些用完的 Player 给拿出来。尽管我们知道在每一次 game 这个函数调用结束之后,这个 p1、p2 都没有用了,但是它们依然存活在字典里。

我们知道,Python 的垃圾回收机制是一个引用计数的机制。尽管在 game 函数结束之后,p1 跟 p2 这两个变量都没了,但是由于我们把它放到了字典里,所以字典对它还有一个引用。那如果我们程序写成这样的话,随着时间的推移、游戏的进行,这个字典就越来越大,大到爆炸,对吧?

弱引用的作用​

那这里我们有什么解决办法呢?当然有。比如说你可以在这个函数的最后,从 players 这个字典里面把这两个东西给移出来。但是无论怎么看,这个解决方案都过于不优雅,对吧?稍微有点蠢。我们也可以在每一次 game 结束之后把这个字典给清空。

但是大家发现没有,我们上面的方案都不太能 scale。也就是说,当我们的程序变得更复杂的时候,这些方案很有可能都会遇到问题。

我们期待的功能是什么呢?是当这个 Player object 没有其他的引用的时候,就从这个字典里把它扔掉。就是当全世界只有你这个字典里还保存着这个 Player 的时候,你就不需要保存这个 Player 了。

这个实际上是一个非常常用的功能,所以在 Python 中,也有专门针对它的解决方案。这个解决方案呢,叫做 weak reference,弱引用。

弱引用,顾名思义,就是我这儿对你的引用不是一个强引用。也就是说,如果你这个 object 身上其他的引用都没有了,你就可以被 release 了。我对你的引用,并不构成你存活的条件。

那这个弱引用放到我们当前的场景下就非常合适,对吧?如果这个 Player object 已经没有任何游戏的部分在使用它了,我们就把它 release 掉。

对值使用弱引用​

那有关弱引用的各种内容呢,是在 weakref 这个 module 里。我们今天只讲两个最基础也最常用的内容。第一个就是我们这里的 WeakValueDictionary。

WeakValueDictionary,首先它是一个 dictionary,也就是它是一个字典。你平常字典怎么用的,你现在还怎么用就行。所以我们完全不需要更改其他地方的代码。但是这个字典有个特殊之处,就是它对每一个值都是弱引用,而当这个值上面的其他引用归零的时候,它就会把这个值跟它的 key 一起扔掉。

我们看,和刚才的代码相比,我们仅仅是把这个 players 变成了一个 WeakValueDictionary。当然在打印的时候呢,我们还要把这个 WeakValueDictionary 给变成 dict,打印出来比较清晰。我们可以看到这段代码在运行完 game 之后,players 就被清空了。

我们可以尝试在 game 这个函数的最后打印一下,然后在 game 结束之后再打印一下。

import weakref

# players = {}
players = weakref.WeakValueDictionary()

class Player:
def __init__(self):
for i in range(1000):
if i not in players:
self.id = i
break
players[self.id] = self

def game():
p1 = Player()
p2 = Player()

print(dict(players))

for _ in range(2):
game()

print(dict(players))

可以看到在 game 函数里面,players 这个 dictionary 里面还是有东西的,但是出了 game 函数之后它就没有了。因为在 game 函数结束之后,p1 和 p2 这两个引用都没有了,所以在 players 这个字典里面,这两个值和它们对应的 key 就都被 release 了。

对键使用弱引用​

那有 WeakValueDictionary 就有 WeakKeyDictionary,对吧?那大家很容易想见,所谓的 WeakKeyDictionary,就是对字典的 key 去弱引用。当你这个 key 没有其他引用的时候,它就被 release 了。比如说我们把登记玩家的这条代码变成 players[self] = self.id,也就是我们把这个 key 跟 value 调换一下,让这个 object 本身变成 key,然后它的 id 是它的 value。

import weakref

players = weakref.WeakKeyDictionary()

class Player:
def __init__(self):
for i in range(1000):
if i not in players:
self.id = i
break
players[self] = self.id

def game():
p1 = Player()
p2 = Player()

print(dict(players))

for _ in range(2):
game()

print(dict(players))

可以看到在程序运行结束的时候,players 这个字典依然被清空了。

支持弱引用的对象​

整数与字符串​

那如果我们没有修改登记玩家的这条代码,假设它还是原来的样子,我们运行这段程序会发生什么结果呢?

import weakref

players = weakref.WeakKeyDictionary()

class Player:
def __init__(self):
for i in range(1000):
if i not in players:
self.id = i
break
players[self.id] = self

def game():
p1 = Player()
p2 = Player()

print(dict(players))

for _ in range(2):
game()

print(dict(players))

可以看到它这里报错了,错误是我们不能对一个 int object 做弱引用。同样的,如果我们把这个 int 变成 string,我们依然没有办法对它做弱引用。

import weakref

players = weakref.WeakKeyDictionary()

class Player:
def __init__(self):
for i in range(1000):
i = str(i)
if i not in players:
self.id = i
break
players[self.id] = self

def game():
p1 = Player()
p2 = Player()

print(dict(players))

for _ in range(2):
game()

print(dict(players))

在 Python 中,并不是每一个 object 都支持弱引用的。有不少的 C-level 的 built-in object 都是不支持弱引用的。当然,在绝大多数情况下,对这些 object 做弱引用也是没有意义的。比如说你对一个整数做弱引用,那你是要干嘛呢?且不说小整数现在是永久存活的,就算是大整数,它有可能会被很多其他的无关代码所引用,所以对这些东西取弱引用是没有任何实际意义的。

自定义实例​

那在实际应用中,具体有哪些东西可以被弱引用,哪些东西不能,大家可以去慢慢探索。最常见的,你在 Python level 定义的普通类的实例通常都是可以被弱引用的。那每一个 instance 的方法、集合,包括还有些什么生成器什么的,这些东西都是可以被弱引用的。

__slots__ 与弱引用​

那都聊到这儿了呢,这里还有一个小坑让大家注意一下。如果你的 class 定义了 __slots__声明实例可使用的属性槽位,例如 ['id'];通常能减少内存占用,并限制随意添加新属性。 这个 member,却没有保留弱引用支持,这个类的实例就不支持弱引用了。不知道 __slots__ 是什么的,翻我之前的文章。

限定实例属性​

import weakref

players = weakref.WeakValueDictionary()

class Player:
__slots__ = ['id']

def __init__(self):
for i in range(1000):
if i not in players:
self.id = i
break
players[self.id] = self

def game():
p1 = Player()
p2 = Player()

print(dict(players))

for _ in range(2):
game()

print(dict(players))

启用弱引用支持​

在这种情况下,如果还想让它支持弱引用,需要在 __slots__ 里面增加 __weakref__ 这一项。

import weakref

players = weakref.WeakValueDictionary()

class Player:
__slots__ = ['id', '__weakref__']

def __init__(self):
for i in range(1000):
if i not in players:
self.id = i
break
players[self.id] = self

def game():
p1 = Player()
p2 = Player()

print(dict(players))

for _ in range(2):
game()

print(dict(players))

这个 __weakref__ 就是 Python 自定义的 class 里面,去保存跟弱引用相关的一些信息的这么一个东西。不定义 __slots__ 没事,如果定义了 __slots__,记得在 __slots__ 里面加上它。