Weak Reference Patterns¶
Observer Pattern¶
The observer pattern often creates memory leaks because observables hold strong references to observers. Use WeakSet to allow observers to be garbage collected when no longer needed elsewhere.
Problem: Strong References¶
class Observable:
def __init__(self):
self._observers = set() # Strong references
def subscribe(self, observer):
self._observers.add(observer)
def notify(self):
for obs in self._observers:
obs.update()
# Problem: observers never garbage collected
# even when no other references exist
Solution: WeakSet¶
import weakref
class Observable:
def __init__(self):
self._observers = weakref.WeakSet()
def subscribe(self, observer):
self._observers.add(observer)
def unsubscribe(self, observer):
self._observers.discard(observer)
def notify(self):
# Dead observers automatically removed
for obs in self._observers:
obs.update()
# Usage
class Observer:
def update(self):
print("Notified!")
subject = Observable()
obs = Observer()
subject.subscribe(obs)
subject.notify() # "Notified!"
del obs # Observer can now be garbage collected
subject.notify() # Nothing happens — observer is gone
Self-Cleaning Cache¶
Caches can consume unbounded memory. Use WeakValueDictionary to automatically evict entries when cached objects are no longer referenced elsewhere.
Problem: Unbounded Cache¶
cache = {}
def get_object(key):
if key not in cache:
obj = create_expensive_object(key)
cache[key] = obj # Stays forever!
return cache[key]
# Cache grows without bound
Solution: WeakValueDictionary¶
import weakref
cache = weakref.WeakValueDictionary()
def get_object(key):
obj = cache.get(key)
if obj is None:
obj = create_expensive_object(key)
cache[key] = obj
return obj
# When no other references to obj exist,
# it's automatically removed from cache
Example: Image Cache¶
import weakref
class ImageCache:
def __init__(self):
self._cache = weakref.WeakValueDictionary()
def get_image(self, path):
image = self._cache.get(path)
if image is None:
image = load_image(path) # Expensive
self._cache[path] = image
return image
# Images are cached while in use
# Automatically evicted when no longer referenced
Parent-Child Relationships¶
Bidirectional relationships create reference cycles. Use weak references for back-references (child → parent) to allow garbage collection.
Problem: Reference Cycle¶
class Parent:
def __init__(self):
self.children = []
def add_child(self, child):
self.children.append(child)
child.parent = self # Strong back-reference
class Child:
def __init__(self):
self.parent = None # Creates cycle when set
# Cycle: parent -> child -> parent
# Requires cycle GC, delays cleanup
Solution: Weak Back-Reference¶
import weakref
class Parent:
def __init__(self):
self.children = []
def add_child(self, child):
self.children.append(child)
child.set_parent(self)
class Child:
def __init__(self):
self._parent_ref = None
def set_parent(self, parent):
self._parent_ref = weakref.ref(parent)
@property
def parent(self):
if self._parent_ref is None:
return None
return self._parent_ref() # Returns None if parent is dead
# No cycle: parent -> child -> weak_ref
# Parent can be garbage collected immediately
Tree Structure Example¶
import weakref
class TreeNode:
def __init__(self, value):
self.value = value
self.children = []
self._parent_ref = None
def add_child(self, child):
self.children.append(child)
child._parent_ref = weakref.ref(self)
@property
def parent(self):
if self._parent_ref is None:
return None
return self._parent_ref()
def path_to_root(self):
path = [self.value]
node = self.parent
while node is not None:
path.append(node.value)
node = node.parent
return path[::-1]
# Usage
root = TreeNode("root")
child = TreeNode("child")
grandchild = TreeNode("grandchild")
root.add_child(child)
child.add_child(grandchild)
print(grandchild.path_to_root()) # ['root', 'child', 'grandchild']
Summary¶
| Pattern | Weak Reference Type | Use Case |
|---|---|---|
| Observer | WeakSet |
Allow observers to be GC'd without explicit unsubscribe |
| Cache | WeakValueDictionary |
Auto-evict cached objects when no longer used |
| Parent-Child | weakref.ref() |
Break cycles in bidirectional relationships |
Key Principle: Use strong references for ownership (parent → child), weak references for back-references or non-owning relationships (child → parent, cache → cached object, observable → observer).