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_dict
をweakref.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
モジュールは、WeakValuDictionary
とWeakKeyDictionary
の2つの辞書オブジェクトだけで事足りるほどのシンプルさではあるが、非常に強力なモジュールである。
工夫次第で、他にもいろいろな使い方がありそうだ。
知名度が非常に低いモジュールなので、知らなかった人には非常に力になると思う。
他に有効な使い方を見つけたら、是非とも記事にしてほしい。