Decorator to cache a function result for some time I recently played around with a script that got some data from the Google API. As I didn't want to spam requests at the service (and potentially get blocked), I made this decorator, which caches the result of a function for a specified amount of time. Any call after the time-to-live (TTL) will call the function again.

This is the first decorator I wrote that takes an optional argument (the time to keep the cache). That code was taken from this StackOverflow answer by @Eric. I also couldn't abstain from using the new walrus operator (Python 3.8+), since I'm always looking for opportunities to use it in order to get a better feel for it.

Any and all advice on how to make this better or more readable are welcome.

from datetime import datetime
from functools import wraps

DEBUG = True    

def temporary_cache(*args, ttl=60):
    """A decorator that ensures that the result of the function call
    is cached for `ttl` seconds (default: 60).

    Warning: The returned object is stored directly, mutating it also mutates the
    cached object. Make a copy if you want to avoid that.
    """
    def decorator(func):
        func.cache = None
        func.cache_time = datetime.fromordinal(1)

        @wraps(func)
        def inner(*args, **kwargs):
            if ((now := datetime.now()) - func.cache_time).total_seconds() &gt; ttl:
                func.cache = func(*args, **kwargs)
                func.cache_time = now
            elif DEBUG:
                # for debugging, disable in production
                print("Cached", func.__name__)
            return func.cache
        return inner
    if len(args) == 1 and callable(args[0]):
        return decorator(args[0])
    elif args:
        raise ValueError("Must supply the decorator arguments as keywords.")
    return decorator


Example usages:

import time

@temporary_cache
def f():
    return datetime.now()

@temporary_cache(ttl=1)
def g():
    return datetime.now()

if __name__ == "__main__":
    print(f())
    # 2020-05-12 10:41:18.633386
    time.sleep(2)
    print(f())
    # Cached f
    # 2020-05-12 10:41:18.633386

    print(g())
    # 2020-05-12 10:41:20.635594
    time.sleep(2)
    print(g())
    # 2020-05-12 10:41:22.636782    


Note that f was still cached, while g was not, because the TTL is shorter than the time between calls.
Rather than using *args you can supply a default positional only argument.

def temporary_cache(fn=None, *, ttl=60):
    ...
    if fn is not None:
        return decorator(fn)
    return decorator

If you feel following "flat is better than nested" is best, we can use functools.partial to remove the need to define decorator.

def temporary_cache(fn=None, *, ttl=60):
    if fn is None:
        return functools.partial(temporary_cache, ttl=ttl)

    @functools.wraps(fn)
    def inner(*args, **kwargs):
    ...


  for debugging, disable in production


You can use logging for this. I will leave actually implementing this as an exercise.

  I also couldn't abstain from using the new walrus operator (Python 3.8+), since I'm always looking for opportunities to use it in order to get a better feel for it.


A very reasonable thing to do. Abuse the new feature until you know what not to do. +1

However, I don't think this is a good place for it.
Given all the brackets in such a small space I'm getting bracket blindness.
I can't tell where one bracket ends and the others starts.
I am not a fan of func.cache = ... and func.cache_time. You can stop assigning to a function by using nonlocal.


Bringing this all together, and following some of my personal style guide, gets the following. I'm not really sure which is better, but it's food for thought.

from datetime import datetime
import functools


def temporary_cache(fn=None, *, ttl=60):
    if fn is None:
        return functools.partial(temporary_cache, ttl=ttl)

    cache = None
    cache_time = datetime.fromordinal(1)

    @functools.wraps(fn)
    def inner(*args, **kwargs):
        nonlocal cache, cache_time
        now = datetime.now()
        if ttl &lt; (now - cache_time).total_seconds():
            cache = fn(*args, **kwargs)
            cache_time = now
        elif DEBUG:
            # for debugging, disable in production
            print("Cached", fn.__name__)
        return cache
    return inner