Step 4: The Cache

Now the payoff: a Cache that knows nothing about how eviction is decided. It just holds data and delegates the choice to whatever policy it was handed. The same cache class works with FIFO, LRU, or LFU, and that is the whole point of the Strategy pattern.

  • get(key): return the value (and record an access) or None on a miss
  • put(key, value): insert or update; when a new key would exceed capacity, ask the policy for a victim and evict it first
Hints
Starter code
class EvictionPolicy:
    def on_insert(self, key):
        raise NotImplementedError

    def on_access(self, key):
        raise NotImplementedError

    def evict(self):
        raise NotImplementedError


class FIFOPolicy(EvictionPolicy):
    def __init__(self):
        self._order = []

    def on_insert(self, key):
        self._order.append(key)

    def on_access(self, key):
        pass

    def evict(self):
        return self._order.pop(0)


class LRUPolicy(EvictionPolicy):
    def __init__(self):
        self._order = []

    def on_insert(self, key):
        self._order.append(key)

    def on_access(self, key):
        if key in self._order:
            self._order.remove(key)
        self._order.append(key)

    def evict(self):
        return self._order.pop(0)


class LFUPolicy(EvictionPolicy):
    def __init__(self):
        self._counts = {}
        self._order = []

    def on_insert(self, key):
        self._counts[key] = 1
        self._order.append(key)

    def on_access(self, key):
        self._counts[key] = self._counts.get(key, 0) + 1

    def evict(self):
        victim = min(self._order, key=lambda k: self._counts[k])
        self._order.remove(victim)
        del self._counts[victim]
        return victim


class Cache:
    def __init__(self, capacity, policy):
        self.capacity = capacity
        self.policy = policy        # any EvictionPolicy
        self._store = {}

    def get(self, key):
        """Return the value for key, recording an access; None if absent."""
        # TODO: a hit is a key present in self._store. On a hit, tell the policy
        # the key was accessed so recency and frequency stay accurate, then hand
        # back the stored value. A miss tells the policy nothing and yields None.
        pass

    def put(self, key, value):
        """Insert or update key; evict via the policy when over capacity."""
        # TODO: a key already in self._store is an update, overwrite its value,
        # count it as an access on the policy, and never evict. A brand-new key
        # that would take self._store past self.capacity means asking the policy
        # for a victim and dropping it before storing and registering the new key.
        pass
Write it on a laptop

The editor and the test runner need a wide screen. Open this page on a desktop browser and your progress will be waiting.