Preventing python observer zombies behaving badly

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.
# 


if you want to give feedback on an article so that it is updated, you can write to editorial@freecodecamp.org

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 weakreference set. 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()