A recent freeCodeCamp article described the "observer” pattern and how to implement it in python. It showed how an object can subscribe to be notified about (“observe”) changes to another object. The subscriber can also unsubscribe.
The article does not address the problem of active zombies. A zombie happens when an observer is deleted with del or when it goes out of scope without already having been unsubscribed from observing. The observing object’s name may be unusable, but the underlying object still exists with its methods and data. When the observed object changes, it invokes the zombie observer’s notification method. The zombie executes what the notification says to do. This is a memory leak, but worse, with the zombie behaving badly.
The clearest solution is for an observer always to unsubscribe before being deleted.
Also, the example code can be enhanced to make doing this easier.
#Add two lines to class Blog:
class Blog:
# …
def subscribe(self, subscriber):
“”“Add a subscriber to the blog”“”
if subscriber not in self._subscribers:
self._subscribers.append(subscriber)
subscriber.subscribesto.add(self) # added by dakra
print(f"✓ {subscriber.email} subscribed to {self.name}")
def unsubscribe(self, subscriber):
"""Remove a subscriber from the blog"""
if subscriber in self._subscribers:
self._subscribers.remove(subscriber)
subscriber.subscribesto.discard(self) # added by dakra
print(f"✗ {subscriber.email} unsubscribed from {self.name}")
# ....
Add a subscribesto attribute and a del method to the subscriber
class EmailSubscriber:
def init(self, email):
self.email = email
self.subscribesto=set() # added by dakra
def __del__(self): # added by dakra
for n in set(self.subscribesto): # create a copy for the iteration
print (" Unsubscribing ", self, " from ", n)
n.unsubscribe(self)
# ....
and in usage:
Subscribe to the blog
tech_blog.subscribe(reader1)
tech_blog.subscribe(reader2)
tech_blog.subscribe(reader3)
reader3.del() # does cause deletion of the the subscriptions.
del reader3 # If called afterreader3.del(), then does cause deletion of the instance.
#
I did send this to the editorial email, and received a positive response.
I have since sent the editorial staff an even better solution:
Replace the list of subscribers with a weakreferenceset. This simple change eliminates the problem of invoking zombies and accumulating memory leaks.
Doing this requires only adding one statement and changing two:
Add an import statement
import weakref
Replace
self._subscribers =[]
with
self._subscribers=weakref.WeakSet()
Replace
self._subscribers.append(subscriber)
with
self._subscribers.add(subscriber)
The result, including a test usage is:
class Blog: # from https://www.freecodecamp.org/news/how-to-implement-the-observer-pattern-in-python/
def __init__(self, name): self.name = name
import weakref # added by dakra See https://docs.python.org/3/library/weakref.html
# self._subscribers = [] replaced by dakra
self._subscribers=weakref.WeakSet() # replacement by dakra
self._latest_post = None
def subscribe(self, subscriber):
"""Add a subscriber to the blog"""
if subscriber not in self._subscribers:
# self._subscribers.append(subscriber) # replaced by dakra
self._subscribers.add(subscriber) # replacement by dakra
print(f"✓ {subscriber.email} subscribed to {self.name}")
def unsubscribe(self, subscriber):
"""Remove a subscriber from the blog"""
if subscriber in self._subscribers:
self._subscribers.remove(subscriber)
print(f"✗ {subscriber.email} unsubscribed from {self.name}")
def notify_all(self):
"""Send notifications to all subscribers"""
print(f"\nNotifying {len(self._subscribers)} subscribers...")
for subscriber in self._subscribers:
subscriber.receive_notification(self.name, self._latest_post)
def publish_post(self, title):
"""Publish a new post and notify subscribers"""
print(f"\n {self.name} published: '{title}'")
self._latest_post = title
class EmailSubscriber:
def __init__(self, email):
self.email = email
def receive_notification(self, blog_name, post_title):
print(f" Email sent to {self.email}: New post on {blog_name} - '{post_title}'")
# Create a blog
tech_blog = Blog("DevDaily")
# Create subscribers
reader1 = EmailSubscriber("anna@example.com")
reader2 = EmailSubscriber("betty@example.com")
reader3 = EmailSubscriber("cathy@example.com")
reader4 = EmailSubscriber("david@notazombie.com")
# Subscribe to the blog
tech_blog.subscribe(reader1)
tech_blog.subscribe(reader2)
tech_blog.subscribe(reader3)
tech_blog.subscribe(reader4)
del reader4
# Publish posts
tech_blog.publish_post("10 Python Tips for Beginners")
tech_blog.publish_post("Understanding Design Patterns")
quit()