1

I'm trying to add a cache of sorts to an expensive function using a fixed dictionary. Something like this:

def func(arg):
    if arg in precomputed:
        return precomputed[arg]
    else:
        return expensive_function(arg)

Now it would be a bit cleaner if I could do something like this using dict.get() default values:

def func(arg):
    return precomputed.get(arg, expensive_function(arg))

The problem is, expensive_function() runs regardless of whether precomputed.get() succeeds, so we get all of the fat for none of the flavor.

Is there a way I can defer the call to expensive_function() here so it is only called if precomputed_get() fails?

TheEnvironmentalist
  • 2,694
  • 2
  • 19
  • 46
  • 1
    I'd think the most pythonic way would be `try: return pre[arg] except KeyError: return exp_fun(arg)`… Why does it need to be a one-liner? – deceze Feb 21 '20 at 07:28
  • 2
    Depending on the input the [`lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache) decorator might be an alternative solution. – Matthias Feb 21 '20 at 07:31
  • 1
    @deceze Why is try-catch so pythonic? I've seen it recommended in other scenarios, but I'm a bit stuck on the "avoid falling into error handling code whenever possible" mentality from other languages. Aren't there performance implications? – TheEnvironmentalist Feb 21 '20 at 07:31
  • 2
    You might want to read "[Idiomatic Python: EAFP versus LBYL](https://devblogs.microsoft.com/python/idiomatic-python-eafp-versus-lbyl/)" – Matthias Feb 21 '20 at 07:34
  • Dupe? [Avoid or delay evaluation of things which may not be used](https://stackoverflow.com/q/9802981/674039) – wim Feb 21 '20 at 07:40
  • @wim Using a generator here would hardly make the code more readable in this case :P – Amadan Feb 21 '20 at 07:41
  • @wim I think this turned into a deeper discussion on what is pythonic, and I'm learning a ton. I can only imagine others would benefit from the discussion. I'm happy to edit the question to better emphasize that fact if you like, let me know, or feel free to make the edits yourself if you prefer – TheEnvironmentalist Feb 21 '20 at 07:42
  • 2
    I mean, the solutions in that other question don't really help you - but I can tell you that, with 8 years of experience later, the simple and readable if-statement that you originally had was the best way in the first place :) – wim Feb 21 '20 at 07:44
  • 1
    @TheEnvironmentalist You're writing Python, performance has already fled the building (unless you're cheating by using C-backed libraries like numpy). You are probably not aware, but e.g. every time you use `for` loop, you are relying on an [exception](https://docs.python.org/3/library/exceptions.html#StopIteration) to stop it. So fighting against exceptions in Python is kind of futile. – Amadan Feb 21 '20 at 07:52

2 Answers2

3

If it's cleanliness you are looking for, I suggest using library rather than reinventing the wheel:

from functools import lru_cache

@lru_cache
def expensive_function(arg):
    # do expensive thing
    pass

Now all calls to expensive_function are memoised, and you can call it without dealing with cache yourself. (If you are worried about memory consumption, you can even limit the cache size.)

To answer the literal question, Python has no way of creating functions or macros that lazily evaluate their parameters, like Haskell or Scheme do. The only way to defer calculation in Python is to wrap it in a function or a generator. It would be less, not more, readable than your original code.

Amadan
  • 191,408
  • 23
  • 240
  • 301
1
def func(arg):
    result = precomputed[arg] if arg in precomputed else expensive_function(arg)
    return result
Artur Kasza
  • 356
  • 1
  • 9