weakref – オブジェクトのガベージコレクション可能な参照

目的:“巨大な” オブジェクトを参照して、他に非弱参照のオブジェクトがない場合にガベージコレクションの対象にする
利用できるバージョン:2.1 以上

weakref モジュールは、オブジェクトに対する弱参照をサポートします。通常の参照は、オブジェクトの参照カウンタを増加させて、そのオブジェクトがガベージコレクタの対象にならないようにします。これは必ずしも望ましいことではありません。例えば、循環参照が存在するとき、またはメモリが必要なときに削除すべきオブジェクトのキャッシュを作るとき等です。

参照

オブジェクトに対する弱参照は ref クラスを通して管理します。元のオブジェクトを取得するには、参照オブジェクトを呼び出します。

import weakref

class ExpensiveObject(object):
    def __del__(self):
        print '(Deleting %s)' % self

obj = ExpensiveObject()
r = weakref.ref(obj)

print 'obj:', obj
print 'ref:', r
print 'r():', r()

print 'deleting obj'
del obj
print 'r():', r()

この場合、 obj は2回目の参照オブジェクトの呼び出し前に削除されるので refNone を返します。

$ python weakref_ref.py
obj: <__main__.ExpensiveObject object at 0x10046e410>
ref: <weakref at 0x1004696d8; to 'ExpensiveObject' at 0x10046e410>
r(): <__main__.ExpensiveObject object at 0x10046e410>
deleting obj
(Deleting <__main__.ExpensiveObject object at 0x10046e410>)
r(): None

参照コールバック

ref コンストラクタは、オプションで2番目の引数に参照されるオブジェクトが削除されたときに実行するコールバック関数を取ります。

import weakref

class ExpensiveObject(object):
    def __del__(self):
        print '(Deleting %s)' % self
        
def callback(reference):
    """Invoked when referenced object is deleted"""
    print 'callback(', reference, ')'

obj = ExpensiveObject()
r = weakref.ref(obj, callback)

print 'obj:', obj
print 'ref:', r
print 'r():', r()

print 'deleting obj'
del obj
print 'r():', r()

このコールバック関数は、元のオブジェクトに対する参照ではなくその参照が “dead” になった後で、引数として参照オブジェクトを受け取ります。例えば、これはキャッシュから弱参照オブジェクトを削除させます。

$ python weakref_ref_callback.py
obj: <__main__.ExpensiveObject object at 0x10046e610>
ref: <weakref at 0x100469730; to 'ExpensiveObject' at 0x10046e610>
r(): <__main__.ExpensiveObject object at 0x10046e610>
deleting obj
callback( <weakref at 0x100469730; dead> )
(Deleting <__main__.ExpensiveObject object at 0x10046e610>)
r(): None

プロキシ

直接 ref を使用する代わりに、プロキシを使用する方がもっと便利です。プロキシは元のオブジェクトを扱うように使用できるので、そのオブジェクトへアクセスする前に ref を呼び出す必要はありません。

import weakref

class ExpensiveObject(object):
    def __init__(self, name):
        self.name = name
    def __del__(self):
        print '(Deleting %s)' % self

obj = ExpensiveObject('My Object')
r = weakref.ref(obj)
p = weakref.proxy(obj)

print 'via obj:', obj.name
print 'via ref:', r().name
print 'via proxy:', p.name
del obj
print 'via proxy:', p.name

参照されたオブジェクトが削除された後でこのプロキシにアクセスすると、 ReferenceError 例外が発生します。

$ python weakref_proxy.py
via obj: My Object
via ref: My Object
via proxy: My Object
(Deleting <__main__.ExpensiveObject object at 0x10046d490>)
via proxy:
Traceback (most recent call last):
  File "weakref_proxy.py", line 26, in <module>
    print 'via proxy:', p.name
ReferenceError: weakly-referenced object no longer exists

循環参照

弱参照の使用法の1つは、ガベージコレクションを妨げずに循環参照できることです。このサンプルは、循環するグラフを含むときに普通のオブジェクトとプロキシを使用することの違いを説明します。

まずシーケンスの “次” ノードとして渡された任意のオブジェクトをもつ Graph クラスが必要です。簡単にするために、この Graph は、各ノードから1つの外向きの参照をサポートします。つまらないグラフですが、簡単に循環参照を作成します。関数 demo() は、循環参照を作成した後で様々な参照を削除することで、グラフのクラスにアクセスするユーテリティ関数です。

import gc
from pprint import pprint
import weakref

class Graph(object):
    def __init__(self, name):
        self.name = name
        self.other = None
    def set_next(self, other):
        print '%s.set_next(%s (%s))' % (self.name, other, type(other))
        self.other = other
    def all_nodes(self):
        "Generate the nodes in the graph sequence."
        yield self
        n = self.other
        while n and n.name != self.name:
            yield n
            n = n.other
        if n is self:
            yield n
        return
    def __str__(self):
        return '->'.join([n.name for n in self.all_nodes()])
    def __repr__(self):
        return '%s(%s)' % (self.__class__.__name__, self.name)
    def __del__(self):
        print '(Deleting %s)' % self.name
        self.set_next(None)

class WeakGraph(Graph):
    def set_next(self, other):
        if other is not None:
            # 弱参照をもつ他の参照に置き換えるべきかを調べる
            if self in other.all_nodes():
                other = weakref.proxy(other)
        super(WeakGraph, self).set_next(other)
        return

def collect_and_show_garbage():
    "Show what garbage is present."
    print 'Collecting...'
    n = gc.collect()
    print 'Unreachable objects:', n
    print 'Garbage:', 
    pprint(gc.garbage)

def demo(graph_factory):
    print 'Set up graph:'
    one = graph_factory('one')
    two = graph_factory('two')
    three = graph_factory('three')
    one.set_next(two)
    two.set_next(three)
    three.set_next(one)
    
    print
    print 'Graphs:'
    print str(one)
    print str(two)
    print str(three)
    collect_and_show_garbage()

    print
    three = None
    two = None
    print 'After 2 references removed:'
    print str(one)
    collect_and_show_garbage()

    print
    print 'Removing last reference:'
    one = None
    collect_and_show_garbage()

ここでメモリリークをデバッグするときに便利な gc モジュールを使用したテストプログラムがあります。 DEBUG_LEAK フラグは、 gc モジュールを経由しないと見れないガベージコレクタがもつ参照先であるオブジェクトに関する情報を表示します。

import gc
from pprint import pprint
import weakref

from weakref_graph import Graph, demo, collect_and_show_garbage

gc.set_debug(gc.DEBUG_LEAK)

print 'Setting up the cycle'
print
demo(Graph)

print
print 'Breaking the cycle and cleaning up garbage'
print
gc.garbage[0].set_next(None)
while gc.garbage:
    del gc.garbage[0]
print
collect_and_show_garbage()

demo()Graph インスタンスに対するローカル参照を削除した後でさえ、そのグラフはガベージリストに表示されて、ガベージコレクションされません。このガベージリストのディクショナリは、 Graph インスタンスの属性を保持します。このグラフはそれが何なのかを知っているので強制的に削除できます。

$ python -u weakref_cycle.py
Setting up the cycle

Set up graph:
one.set_next(two (<class 'weakref_graph.Graph'>))
two.set_next(three (<class 'weakref_graph.Graph'>))
three.set_next(one->two->three (<class 'weakref_graph.Graph'>))

Graphs:
one->two->three->one
two->three->one->two
three->one->two->three
Collecting...
Unreachable objects: 0
Garbage:[]

After 2 references removed:
one->two->three->one
Collecting...
Unreachable objects: 0
Garbage:[]

Removing last reference:
Collecting...
gc: uncollectable <Graph 0x100471f50>
gc: uncollectable <Graph 0x100471f90>
gc: uncollectable <Graph 0x100471fd0>
gc: uncollectable <dict 0x100363160>
gc: uncollectable <dict 0x100366560>
gc: uncollectable <dict 0x1003672f0>
Unreachable objects: 6
Garbage:[Graph(one),
 Graph(two),
 Graph(three),
 {'name': 'one', 'other': Graph(two)},
 {'name': 'two', 'other': Graph(three)},
 {'name': 'three', 'other': Graph(one)}]

Breaking the cycle and cleaning up garbage

one.set_next(None (<type 'NoneType'>))
(Deleting two)
two.set_next(None (<type 'NoneType'>))
(Deleting three)
three.set_next(None (<type 'NoneType'>))
(Deleting one)
one.set_next(None (<type 'NoneType'>))

Collecting...
Unreachable objects: 0
Garbage:[]

今度は普通の参照を使用して循環参照を作成しないもっと賢い WeakGraph クラスを定義しましょう。しかし、循環参照が削除されるときは ref を使用します。

import gc
from pprint import pprint
import weakref

from weakref_graph import Graph, demo

class WeakGraph(Graph):
    def set_next(self, other):
        if other is not None:
            # 弱参照をもつ他の参照に置き換えるべきかを調べる
            if self in other.all_nodes():
                other = weakref.proxy(other)
        super(WeakGraph, self).set_next(other)
        return
                
demo(WeakGraph)

WeakGraph インスタンスは、既に見たことがあるオブジェクトを参照するためにプロキシを使用するので、 demo() がこのオブジェクトの全てのローカル属性を削除して、循環参照が壊れて、ガベージコレクタはこのオブジェクトを削除します。

$ python weakref_weakgraph.py
Set up graph:
one.set_next(two (<class '__main__.WeakGraph'>))
two.set_next(three (<class '__main__.WeakGraph'>))
three.set_next(one->two->three (<type 'weakproxy'>))

Graphs:
one->two->three
two->three->one->two
three->one->two->three
Collecting...
Unreachable objects: 0
Garbage:[]

After 2 references removed:
one->two->three
Collecting...
Unreachable objects: 0
Garbage:[]

Removing last reference:
(Deleting one)
one.set_next(None (<type 'NoneType'>))
(Deleting two)
two.set_next(None (<type 'NoneType'>))
(Deleting three)
three.set_next(None (<type 'NoneType'>))
Collecting...
Unreachable objects: 0
Garbage:[]

キャッシュオブジェクト

refproxy クラスは “低レベル” なものと考えられます。そういったクラスが個々のオブジェクトに対する弱参照を保持して循環参照をガベージコレクションできるものの、オブジェクトのキャッシュを作成する必要があるなら、 WeakKeyDictionaryWeakValueDictionary がもっと適切な API を提供します。

期待通り、 WeakValueDictionary は保持するオブジェクトに対する弱参照を使用します。そして、別のコードが実際にそういったオブジェクトを使用しないときにガベージコレクションできるようにします。

普通のディクショナリと WeakValueDictionary を使用することでメモリ操作の違いを説明するために、再度ガベージコレクタを呼び出して実験してみましょう。

import gc
from pprint import pprint
import weakref

gc.set_debug(gc.DEBUG_LEAK)

class ExpensiveObject(object):
    def __init__(self, name):
        self.name = name
    def __repr__(self):
        return 'ExpensiveObject(%s)' % self.name
    def __del__(self):
        print '(Deleting %s)' % self
        
def demo(cache_factory):
    # オブジェクトを保持して
    # 全ての弱参照がすぐに削除されないようにする
    all_refs = {}
    # 渡されたファクトリを使用するキャッシュ
    print 'CACHE TYPE:', cache_factory
    cache = cache_factory()
    for name in [ 'one', 'two', 'three' ]:
        o = ExpensiveObject(name)
        cache[name] = o
        all_refs[name] = o
        del o # 参照カウンタを減らす

    print 'all_refs =',
    pprint(all_refs)
    print 'Before, cache contains:', cache.keys()
    for name, value in cache.items():
        print '  %s = %s' % (name, value)
        del value # decref
        
    # キャッシュを除く全てのオブジェクトに対する参照を削除
    print 'Cleanup:'
    del all_refs
    gc.collect()

    print 'After, cache contains:', cache.keys()
    for name, value in cache.items():
        print '  %s = %s' % (name, value)
    print 'demo returning'
    return

demo(dict)
print
demo(weakref.WeakValueDictionary)

キャッシュしている値を参照する任意のループ変数は、そのオブジェクトの参照カウンタを減らすために明示的にクリアしなければならないことに注意してください。それ以外は、ガベージコレクタはそのオブジェクトを削除せずにキャッシュに残ったままになります。同様に all_ref 変数は、途中でガベージコレクションされないようにそのオブジェクトに対する参照を保持するために使用されます。

$ python weakref_valuedict.py
CACHE TYPE: <type 'dict'>
all_refs ={'one': ExpensiveObject(one),
 'three': ExpensiveObject(three),
 'two': ExpensiveObject(two)}
Before, cache contains: ['three', 'two', 'one']
  three = ExpensiveObject(three)
  two = ExpensiveObject(two)
  one = ExpensiveObject(one)
Cleanup:
After, cache contains: ['three', 'two', 'one']
  three = ExpensiveObject(three)
  two = ExpensiveObject(two)
  one = ExpensiveObject(one)
demo returning
(Deleting ExpensiveObject(three))
(Deleting ExpensiveObject(two))
(Deleting ExpensiveObject(one))

CACHE TYPE: weakref.WeakValueDictionary
all_refs ={'one': ExpensiveObject(one),
 'three': ExpensiveObject(three),
 'two': ExpensiveObject(two)}
Before, cache contains: ['three', 'two', 'one']
  three = ExpensiveObject(three)
  two = ExpensiveObject(two)
  one = ExpensiveObject(one)
Cleanup:
(Deleting ExpensiveObject(three))
(Deleting ExpensiveObject(two))
(Deleting ExpensiveObject(one))
After, cache contains: []
demo returning

WeakKeyDictionary は同様に動作しますが、ディクショナリの値ではなく、キーの弱参照を使用します。

weakref の標準ライブラリドキュメントには次の警告があります。

Warning

注意: WeakValueDictionary は Python のディクショナリの上で作成されるので、繰り返し処理するときにサイズを変更してはいけません。 WeakValueDictionary が繰り返し処理されるとき、プログラムが行うアクションがディクショナリの要素を (ガベージコレクションの副作用として) “魔法のように” 消してしまう可能性があるので、サイズ変更を保証するのが難しくなります。

See also

weakref
本モジュールの標準ライブラリドキュメント
gc
インタープリタのガベージコレクタのインタフェース
Bookmark and Share