弱引用
我们来看这么一个程序。我们定义了一个非常简单的 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__ 里面加上它。