Python 3: Deep Dive (Part 4 - OOP): インスタンス固有データ (セクション8-2/15)
Pythonのデスクリプタは属性の管理に便利ですが、インスタンス固有のデータを保存する際にメモリリークなどの問題が発生する可能性があります。
データをインスタンス自体(`dict`)に保存する方法と、デスクリプタ内の辞書に保存する方法がありますが、それぞれ固有の課題(`slots`との互換性や強参照による問題)があります。
`WeakKeyDictionary`を使用することで、メモリリークを防ぎながらインスタンス固有のデータを効果的に管理できますが、オブジェクトがハッシュ可能で弱参照可能である必要があります。
デスクリプタは、Pythonで属性の設定と取得を管理する強力な方法ですが、各インスタンスのデータを、競合を引き起こしたり、既存の属性を上書きしたり、メモリリークを起こしたりすることなく保存する方法を見つけることは難しい問題です。いくつかの戦略を見て、何が問題になるのか、そして弱参照を使用した解決策について検討してみましょう。
デスクリプタとインスタンスでのデータ保存
最初に取り組むべき質問の1つ:データをどこに置くか?
オプションA:データをインスタンス自体に置く(例:`instance.dict[some_name] = value`)
デメリット:クラスが`slots`を使用している場合や、選択した属性名が既存のものと衝突する場合、自由形式のインスタンス辞書に依存できません。
オプションB:データをデスクリプタオブジェクト内に保存する
デメリット:デスクリプタを使用するクラスのすべてのインスタンスが同じデスクリプタオブジェクトを共有します。`instance`をキーとしたインスタンスごとの記録を保持する必要があります。
主要な課題:インスタンス自体(強参照)を辞書のキーとして保存すると、参照カウントの問題が発生する可能性があります。
デスクリプタ内での辞書の使用
一般的なアプローチは、デスクリプタ内に辞書を作成することです。例えば:
class IntegerValue:
def __init__(self):
self.data = {} # インスタンス -> 値を保存
def __set__(self, instance, value):
self.data[instance] = int(value) # インスタンスがキー
def __get__(self, instance, owner_class):
if instance is None:
return self
return self.data.get(instance)
そして、以下のようにした場合:
class Point2D:
x = IntegerValue()
y = IntegerValue()
p = Point2D()
p.x = 100.1
`p`を`x.data`のキーとして保存し、`100`にマッピングします。良いですね—しかし、注意点があります。
メモリリークの問題
`p`を`x.data`に保存することで、同じ`Point2D`オブジェクトへの2つ目の強参照を導入しました。後で以下を実行しても:
del p
`Point2D`インスタンスが消えたと思うかもしれません。しかし、デスクリプタの辞書がまだ参照を保持しています—Pythonはオブジェクトがまだ使用中であると認識するため、ガベージコレクションされることはありません。
長時間実行されるアプリケーションでは、このシナリオはメモリリークにつながる可能性があります:デスクリプタ辞書に保存されたすべてのオブジェクトは、これらの残存する参照のために生き続けます。
インスタンス辞書への保存の試み
別のアイデアは、データをインスタンス自身の`dict`に隠すことです:
class IntegerValue:
def __set__(self, instance, value):
instance._some_hardcoded_attribute = int(value)
def __get__(self, instance, owner_class):
if instance is None:
return self
return getattr(instance, '_some_hardcoded_attribute', None)
しかし、複数のデスクリプタを同じクラスに配置する場合、互いに上書きしないように一意の属性名が必要です。次のようにすることができます:
class IntegerValue:
def __init__(self, name):
self.storage_name = '_' + name
def __set__(self, instance, value):
setattr(instance, self.storage_name, int(value))
def __get__(self, instance, owner_class):
if instance is None:
return self
return getattr(instance, self.storage_name, None)
しかし、ユーザーは名前を2回指定する必要があります:
class Point2D:
x = IntegerValue('x')
y = IntegerValue('y')
これは機能しますが、既存の属性と衝突する可能性があり、`slots`が使用されている場合は簡単に破綻し、ユーザーが`p1._x = ...`のようなことを決めた場合の上書きからも保護されません。
強参照と弱参照
強参照がデフォルトです。`p2 = p1`を実行すると、同じオブジェクトへの2つの参照ができます。参照カウントは2になり、両方の参照がなくならない限りガベージコレクションされません。
一方、弱参照は、オブジェクトの参照カウントを増やしません。Pythonのガベージコレクタは、オブジェクトを破棄できるかどうかを決定する際に弱参照を無視します。`weakref`モジュールがこの機能を提供します:
import weakref
p2 = weakref.ref(p1)
`p2`は、`p2()`を通じて呼び出されたときに基礎となるインスタンスを返す呼び出し可能オブジェクトになります。
すべての強参照が削除された場合、Pythonはインスタンスをガベージコレクトでき、`p2()`は`None`を返します。
デスクリプタで弱参照を使用する理由は?
上記の辞書ベースのアプローチでは、インスタンスを辞書のキーとして使用すると、余分な強参照が生まれます。弱参照を使用すると:
各インスタンスへの弱い参照を保存するため、ユーザーが唯一の強参照を削除しても、オブジェクトはまだガベージコレクトできます。
自動的に死んだエントリを辞書から削除できます。
from weakref import WeakKeyDictionary
class IntegerValue:
def __init__(self):
self.data = WeakKeyDictionary()
def __set__(self, instance, value):
self.data[instance] = int(value)
def __get__(self, instance, owner):
if instance is None:
return self
return self.data.get(instance)
注意:`WeakKeyDictionary`は、オブジェクトがガベージコレクトされた場合、自動的にキーを削除します。これによりメモリリークが解決されます。ただし、これはハッシュ可能で弱参照可能なオブジェクト(カスタムクラスは一般的に問題ありませんが、リストや辞書などの組み込み型は通常そうではありません)でのみ機能します。
ハッシュ可能性とその他の問題点
クラスが`eq`をオーバーライドするが`hash`をオーバーライドしない場合、インスタンスはハッシュ不可能になります。そうすると、(弱)辞書のキーにはなれません。
`slots`を使用するが`weakref`を含めない場合、オブジェクトは弱参照を持つことができません。
したがって、`WeakKeyDictionary`がシームレスに機能するためには、オブジェクトがハッシュ可能で弱参照を許可する必要があります。
これらの制約が満たされない場合、他の高度な解決策として、`id(instance)`を辞書のキーとして使用し、`(weakref, value)`のペアを保存する方法や、weakrefの終了時にコールバックを使用する方法があります。これらのアプローチは、ハッシュ不可能なオブジェクトや、独自の弱参照を保持するように設計されていないオブジェクトを処理します。
まとめ
基本的なアプローチ:デスクリプタ内部に`instance`をキーとする辞書を使用します。これにより`instance.dict`の上書きの問題は解決されますが、余分な強参照を作成してしまうため、メモリリークを引き起こす可能性があります。
弱参照:インスタンスごとのデータを参照カウントを増やすことなく保存するために`WeakKeyDictionary`に切り替えます。これにより通常のガベージコレクションが可能になります。
注意点:
インスタンスクラスは弱参照可能でなければなりません(多くの場合、`slots`がないか、ある場合は`weakref`を含める必要があります)。
インスタンスクラスは、`WeakKeyDictionary`のキーとして保存したい場合、ハッシュ可能でなければなりません。
これらの構成要素は、インスタンス辞書を乱雑にしたり、誤ってガベージコレクションを妨げたりすることなく、デスクリプタにインスタンスレベルのデータを保存するための基盤を形成します。次のステップは通常、ハッシュ不可能なオブジェクトや最終コールバックが必要な場合などのエッジケースの処理ですが、多くのユースケースでは、デスクリプタと`WeakKeyDictionary`の組み合わせは、すでにメモリリークのない堅牢な解決策です。