8.23 循環(huán)引用數(shù)據(jù)結(jié)構(gòu)的內(nèi)存管理

2018-02-24 15:26 更新

問(wèn)題

你的程序創(chuàng)建了很多循環(huán)引用數(shù)據(jù)結(jié)構(gòu)(比如樹(shù)、圖、觀察者模式等),你碰到了內(nèi)存管理難題。

解決方案

一個(gè)簡(jiǎn)單的循環(huán)引用數(shù)據(jù)結(jié)構(gòu)例子就是一個(gè)樹(shù)形結(jié)構(gòu),雙親節(jié)點(diǎn)有指針指向孩子節(jié)點(diǎn),孩子節(jié)點(diǎn)又返回來(lái)指向雙親節(jié)點(diǎn)。這種情況下,可以考慮使用 weakref 庫(kù)中的弱引用。例如:

import weakref

class Node:
    def __init__(self, value):
        self.value = value
        self._parent = None
        self.children = []

    def __repr__(self):
        return 'Node({!r:})'.format(self.value)

    # property that manages the parent as a weak-reference
    @property
    def parent(self):
        return None if self._parent is None else self._parent()

    @parent.setter
    def parent(self, node):
        self._parent = weakref.ref(node)

    def add_child(self, child):
        self.children.append(child)
        child.parent = self

這種是想方式允許parent靜默終止。例如:

>>> root = Node('parent')
>>> c1 = Node('child')
>>> root.add_child(c1)
>>> print(c1.parent)
Node('parent')
>>> del root
>>> print(c1.parent)
None
>>>

討論

循環(huán)引用的數(shù)據(jù)結(jié)構(gòu)在Python中是一個(gè)很棘手的問(wèn)題,因?yàn)檎5睦厥諜C(jī)制不能適用于這種情形。例如考慮如下代碼:

# Class just to illustrate when deletion occurs
class Data:
    def __del__(self):
        print('Data.__del__')

# Node class involving a cycle
class Node:
    def __init__(self):
        self.data = Data()
        self.parent = None
        self.children = []

    def add_child(self, child):
        self.children.append(child)
        child.parent = self

下面我們使用這個(gè)代碼來(lái)做一些垃圾回收試驗(yàn):

>>> a = Data()
>>> del a # Immediately deleted
Data.__del__
>>> a = Node()
>>> del a # Immediately deleted
Data.__del__
>>> a = Node()
>>> a.add_child(Node())
>>> del a # Not deleted (no message)
>>>

可以看到,最后一個(gè)的刪除時(shí)打印語(yǔ)句沒(méi)有出現(xiàn)。原因是Python的垃圾回收機(jī)制是基于簡(jiǎn)單的引用計(jì)數(shù)。當(dāng)一個(gè)對(duì)象的引用數(shù)變成0的時(shí)候才會(huì)立即刪除掉。而對(duì)于循環(huán)引用這個(gè)條件永遠(yuǎn)不會(huì)成立。因此,在上面例子中最后部分,父節(jié)點(diǎn)和孩子節(jié)點(diǎn)互相擁有對(duì)方的引用,導(dǎo)致每個(gè)對(duì)象的引用計(jì)數(shù)都不可能變成0。

Python有另外的垃圾回收器來(lái)專門針對(duì)循環(huán)引用的,但是你永遠(yuǎn)不知道它什么時(shí)候會(huì)觸發(fā)。另外你還可以手動(dòng)的觸發(fā)它,但是代碼看上去很挫:

>>> import gc
>>> gc.collect() # Force collection
Data.__del__
Data.__del__
>>>

如果循環(huán)引用的對(duì)象自己還定義了自己的 __del__() 方法,那么會(huì)讓情況變得更糟糕。假設(shè)你像下面這樣給Node定義自己的 __del__() 方法:

# Node class involving a cycle
class Node:
    def __init__(self):
        self.data = Data()
        self.parent = None
        self.children = []

    def add_child(self, child):
        self.children.append(child)
        child.parent = self

    # NEVER DEFINE LIKE THIS.
    # Only here to illustrate pathological behavior
    def __del__(self):
        del self.data
        del.parent
        del.children

這種情況下,垃圾回收永遠(yuǎn)都不會(huì)去回收這個(gè)對(duì)象的,還會(huì)導(dǎo)致內(nèi)存泄露。如果你試著去運(yùn)行它會(huì)發(fā)現(xiàn),Data.__del__ 消息永遠(yuǎn)不會(huì)出現(xiàn)了,甚至在你強(qiáng)制內(nèi)存回收時(shí):

>>> a = Node()
>>> a.add_child(Node()
>>> del a # No message (not collected)
>>> import gc
>>> gc.collect() # No message (not collected)
>>>

弱引用消除了引用循環(huán)的這個(gè)問(wèn)題,本質(zhì)來(lái)講,弱引用就是一個(gè)對(duì)象指針,它不會(huì)增加它的引用計(jì)數(shù)。你可以通過(guò) weakref 來(lái)創(chuàng)建弱引用。例如:

>>> import weakref
>>> a = Node()
>>> a_ref = weakref.ref(a)
>>> a_ref
<weakref at 0x100581f70; to 'Node' at 0x1005c5410>
>>>

為了訪問(wèn)弱引用所引用的對(duì)象,你可以像函數(shù)一樣去調(diào)用它即可。如果那個(gè)對(duì)象還存在就會(huì)返回它,否則就返回一個(gè)None。由于原始對(duì)象的引用計(jì)數(shù)沒(méi)有增加,那么就可以去刪除它了。例如;

>>> print(a_ref())
<__main__.Node object at 0x1005c5410>
>>> del a
Data.__del__
>>> print(a_ref())
None
>>>

通過(guò)這里演示的弱引用技術(shù),你會(huì)發(fā)現(xiàn)不再有循環(huán)引用問(wèn)題了,一旦某個(gè)節(jié)點(diǎn)不被使用了,垃圾回收器立即回收它。你還能參考8.25小節(jié)關(guān)于弱引用的另外一個(gè)例子。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)