Weak ref

Weak references are a way to help the Python interpreter remove unused data more easily. This module shows how it can be used to keep a server registry up-to-date as it explicitly sets up and implicitly tears down servers as the program enters and leaves a function scope.

import weakref
from uuid import uuid4

# Module-level constants
_CLOUD_PROVIDER = "aws"
_CLOUD_APPS = ["yelp", "pinterest", "uber", "twitter"]
_CLOUD_APP_COMPONENTS = ("db", "web", "cache")


class Server:
    """General server."""

    @classmethod
    def create(cls, role, provider=_CLOUD_PROVIDER):
        """Create server with autogenerated SSID."""
        return cls(uuid4().hex, role, provider)

    def __init__(self, ssid, role, provider):
        self.ssid = ssid
        self.role = role
        self.provider = provider


class ServerRegistry:
    """Server registry with weak references."""

    def __init__(self):
        self._servers = weakref.WeakSet()

    @property
    def servers(self):
        """Get set of added servers."""
        return {s for s in self._servers}

    @property
    def server_count(self):
        """Get count of added servers."""
        return len(self.servers)

    def add(self, server):
        """Add server to registry."""
        self._servers.add(server)


def setup_and_teardown_servers(registry):
    """Explicitly setup and implicitly teardown servers."""
    app_servers = {}

    # Let's create all of the servers and store them properly
    for app in _CLOUD_APPS:
        app_servers[app] = set()
        for component in _CLOUD_APP_COMPONENTS:
            server = Server.create(f"{app}_{component}")
            registry.add(server)
            app_servers[app].add(server)

    # All of these counts are equivalent. This is no surprise since our
    # for loop unconditionally creates a server for every permutation of
    # apps and components. The loop also adds each server to the registry
    # and dictionary unconditionally
    assert (
        registry.server_count
        == len(_CLOUD_APPS) * len(_CLOUD_APP_COMPONENTS)
        == len([(app, server)
                for app, servers in app_servers.items()
                for server in servers])
    )

    # What's really interesting is that servers go away when we leave the
    # scope of this function. In this function, each server is created and
    # strongly referenced by the `app_servers` variable. When we leave this
    # function, the `app_servers` variable no longer exists which brings
    # the reference count for each server from 1 to 0. A reference count of
    # 0 for each server triggers the garbage collector to run the cleanup
    # process for all of the servers in this function scope


def main():
    # Initialize a server registry
    registry = ServerRegistry()

    # Setup and teardown servers with the registry
    setup_and_teardown_servers(registry)

    # Notice that our registry does not remember the servers because
    # it uses weak references. Because there are no strong references
    # to the created servers in `setup_and_teardown_servers`, the
    # garbage collector cleans up the servers. This behavior is usually
    # desired if we want to keep our software memory-efficient
    assert registry.servers == set()
    assert registry.server_count == 0


if __name__ == "__main__":
    main()