Pythonでweakref(弱参照)モジュールを使いこなす

More than 1 year has passed since last update.

weakrefモジュールは実用的で、非常にパワフルなモジュールである。
おそらく、このモジュールほど実用性と知名度のバランスで不遇な扱いを受けているモジュールはないだろう。
この記事ではweakrefモジュールがいかに便利であるか紹介していく。

問題

ID情報(またはハッシュ可能な一意な情報)を持つ大量のオブジェクトがあるとする。
大量のオブジェクトをIDをキーにして検索できるよう、オブジェクト管理用の辞書を用意している。

class Member:
    def __init__(self, id, name):
        self.id = id
        self.name = name

# IDをキーとした辞書をオブジェクトを管理
members_dict = {
    x.id: x for x in [
        Member(1, "tim"),
        Member(2, "matthew"),
        Member(3, "Jack"),
        # ...
    ]
}

members_dict[1].name
>>> 'tim'

ここでidだけではなく、nameで検索がしたいという要望が出てきたので、名前をキーとした辞書を作成してみよう。(ここでは説明を簡単にするためにnameも重複がなく一意な情報とする)

# 名前をキーとした辞書を作成
names_dict = {x.name: x for x in members_dict.values()}

names_dict["tim"].id
>>> 1

ここでメンバーであるtimが退会したため、データを削除した。

# ID:1であるtimのデータを削除
del members_dict[1]

これでtimのオブジェクトデータはGC(ガベージコレクション)によって回収され、メモリが解放される事が望ましい。
しかし、現実はそうではない。

検索テーブル的な意味合いで追加したnames_dictによってオブジェクトが参照されてしまい、timのオブジェクトがGCに回収されずにメモリ上に存在し続けてしまうのだ。

# names_dictに参照が残っているためにGCが解放せずにリークしてしまう!
names_dict['tim']
>>> <__main__.Member at 0x1744e889ba8>

会員の生成と削除を繰り返していくとメモリが解放されずに溜まってしまうため、members_dictと共にnames_dictも同時に管理し続けなくてはならないのだ。
検索テーブルの種類が増えていくと、それにつれて同時に管理する辞書が増えていく、泥沼である。

この例のように簡単なデータ構造であれば、オブジェクト指向で設計すれば何とかなるだろう。
オブジェクト同士が参照しあったりする複雑なデータ構造になると、オブジェクトの生死判定に参照カウンタが必要になり、GCが載っている言語の上に、さらに自前でGCもどきを実装するという哲学的なコードができあがる。

そこで颯爽と登場する、weakrefモジュール!

そんな状況を救ってくれるのがweakrefモジュールである、names_dictweakref.WeakValuDictionaryに置き換えるだけで問題を解決してくれる。

from weakref import WeakValueDictionary

names_dict = WeakValueDictionary(
    {_.name: _ for _ in members_dict.values()}
)

names_dict['tim'].id
>>> 1

# ID:1であるtimのデータを削除
del members_dict[1]

names_dict['tim']
>>> KeyError: 'tim'

WeakValuDictionaryはバリューが弱参照となる、辞書に似たオブジェクトである。
弱参照とはGCの回収を阻害しない参照で、WeakValuDictionary以外の強参照がなくなると自動で辞書から削除してくれるのだ。

大量のオブジェクトを高速なキャッシュ/検索テーブルを使ってアクセスさせてパフォーマンスを上げたいシチュエーションはよくある。そんな時にWeakValuDictionaryは必須アイテムと言ってもよいだろう。

もちろんWeakKeyDictionaryもある

WeakValuDictionaryがあれば、もちろんキーが弱参照となるWeakKeyDictionaryもある。

WeakKeyDictionaryは参照と被参照の保持に使うこともできる。

from weakref import WeakKeyDictionary


class Member:
    def __init__(self, name, friend=None):
        self.name = name
        self._friend_dict = WeakKeyDictionary()
        self._referenced_dict = WeakKeyDictionary()
        if friend:
            self._friend_dict[friend] = None
            friend._referenced_dict[self] = None

    def referenced(self):
        referenced = list(self._referenced_dict.keys())
        return referenced[0] if referenced else None

    def friend(self):
        friends = list(self._friend_dict.keys())
        return friends[0] if friends else None

    def __str__(self):
        friend = self.friend()
        refer = self.referenced()
        return "{}: referenced={} friend={}".format(
            self.name,
            refer.name if refer else None,
            friend.name if friend else None
        )


tim = Member("tim")
matthew = Member("matthew", friend=tim)
jack = Member("jack", friend=matthew)

print(tim)
print(matthew)
print(jack)
# output:
#
# tim: referenced='matthew' friend='None'
# matthew: referenced='jack' friend='tim'
# jack: referenced='None' friend='matthew'

del matthew

print(tim)
print(jack)
# output:
#
# tim: referenced='None' friend='None'
# jack: referenced='None' friend='None'

他のオブジェクトの参照と被参照を自動的に更新してくれる辞書として使ってみた。
オブジェクトの削除により、辞書が自動で更新されているのが確認できるだろう。

参考

あとがき

weakrefモジュールは、WeakValuDictionaryWeakKeyDictionaryの2つの辞書オブジェクトだけで事足りるほどのシンプルさではあるが、非常に強力なモジュールである。
工夫次第で、他にもいろいろな使い方がありそうだ。

知名度が非常に低いモジュールなので、知らなかった人には非常に力になると思う。
他に有効な使い方を見つけたら、是非とも記事にしてほしい。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.