17.3_循环引用
17.3 循环引用
在了解循环引用之前,我们先来看一段示例代码。
a = obj()
b = obj()
c = obj()
a.b = b
b.c = c
c.a = a
a = b = c = None上面的代码与之前的代码几乎相同,唯一的区别是这次增加了一个从c到a的引用。这时,3个对象呈环状相互引用。这种状态就是循环引用。a、b、c之间的关系如图17-2所示。

图17-2 循环引用情况下的对象关系图(虚线表示引用)
图17-2右图中的a、b、c的引用计数均为1。这时用户已无法访问这3个对象(也就是说,它们是没有用的对象)。但是,如果只设置了 ,那么此时因为循环引用,引用计数不会为0,对象也不会从内存中释放出来。这时就需要使用第2种方法了。这种方法就是GC(准确来说是分代垃圾回收)。
GC比引用计数更智能,它可以判断对象是否有用(GC的原理很复杂,本书不对其进行介绍)。与引用计数不同,GC会在内存不足等情况下自动被Python解释器调用。GC也支持显式调用,具体做法是导入gc模块,然后调用gccollect()。
GC能够正确处理循环引用。因此在使用Python编程时,我们通常不需要关心循环引用。不过,(与没有循环引用时的情况相比)使用GC推迟内存释放会导致程序整体的内存使用量增加(详见参考文献[10])。内存是机器学习,尤其是神经网络运算时的重要资源。因此,在DeZero的开发过程中,建议避免循环引用。
以上就是Python的内存管理的基础知识。现在我们把目光转回DeZero。其实当前的DeZero中存在循环引用,就在图17-3所示的变量和函数部分。

图17-3 Variable和Function的循环引用
如图17-3所示,Function实例引用了输入和输出的Variable实例。同时,Variable实例也引用了作为创建者的Function实例。这时,Function实例和Variable实例之间就存在循环引用关系。我们可以使用作为Python标准模块的weakref来避免循环引用。
17.4 weakest模块
在Python中,我们可以使用weakref.ref函数来创建弱引用。弱引用是在不增加引用计数的情况下引用另一个对象的功能。下面是使用weakref.ref函数的例子。
>>> import weakref
>>> import numpy as np
>>> a = np.array([1, 2, 3])
>>> b = weakref.ref(a)
>>> b
<weakref at 0x103b7f048; to 'numpy.ndarray' at 0x103b67e90>
>>> b()
[1 2 3]上面的代码选用了ndarray实例作为对象。a是它的引用,b是它的弱引用。b的输出表明它是ndarray的弱引用(weakref)。我们可以编写b()来实际访问该引用中的数据。
接着在上面的代码之后运行 None。结果如下所示。
>>>a $\equiv$ None
>>>b
<weakref at 0x103b7f048;dead>如代码所示,ndarray实例通过引用计数这一内存管理方式被删除,b虽然引用了这个对象,但由于是弱引用,所以对引用计数没有影响。这时我们来看b的输出,会发现有dead出现,这表明ndarray实例已经被删除。

在Python解释器上运行是这里展示的弱引用的示例代码正常工作的前提。如果是在IPython或Jupyter Notebook等解释器上运行,b的输出中不会出现dead,因为这些解释器会在幕后持有额外的引用。
下面将weakref机制引入DeZero中。阴影部分是要向Function类添加的代码。
steps/step17.py
importweakref
class Function: def__call__(self,\*inputs): xs $=$ [x.data for x in inputs] ys $=$ self.forward(\*xs) if not isinstance(ys,tuple): ys $=$ (ys,) outputs $=$ [Variable(as_array(y))for y in ys] self_generation $=$ max([x_generation for $\mathbf{x}$ in inputs]) for output in outputs: output.set creator(self) selfInputs $=$ inputs self.output $=$ [weakref.ref(output) for output in outputs] return outputs if len(outpu $s$ ) $>1$ else outputs[0]设置实例变量self.outputs的代码被改为拥有对象的弱引用的代码。这样,函数的输出变量会变成弱引用。这处修改完成后,我们还需要修改其他类引用Function类的outputs的代码。目前,我们需要按如下方式修改Variable类的backward方法。
steps/step17.py
class Variable:
def backward(self):
while funcs: f $=$ funcs.pop() # gys $=$ [output_grad for output in f.outputs] gys $=$ [output().grad for output in f.outputs]上面的代码将 [output.grad for ...] 改为 [output().grad for ...]。这就解决了 DeZero 中循环引用的问题。