Dictionary get method eager execution

It seems like dict.get is exactly the kind of place that eager execution would be useful. Should it be done?

Currently, dict.get is a way to lookup a value while providing a (usually sentinel) default. This allows some code to look neater by eschewing a try... except KeyError block. E.g.

val = cache.get(key, SENTINEL)
if val is SENTINEL:
    val = slow_recalculate(key)

It’s also useful in cases when the same key could be stored in multiple equivalent ways, e.g. looking up the mixed x and y terms in a polynomial:

term_index = terms.get(("x_1", "x_3"))
if term_index is None:
    term_index = terms[("x_3", "x_1")]

In either case, under eager execution, three lines could be combined with no loss of clarity:

val = cache.get(key, slow_recalculate(key))
term_index = terms.get(("x_1", "x_3"), terms[("x_3", "x_1")])

Under python 3.10 at least (and I saw no documentation indicating 3.11 or 3.12 changed this), this code block still executes slow_recalculate(key) and raises a KeyError if ("x_3", "x_1") isn’t a key, even if ("x_1", "x_3") is a key.

You can also express this today with two uses of .get: terms.get(key1, terms.get(key2, None)).

Or you can write a utility function that takes a lambda for the default value, so you can write lazy_get(terms, key1, lambda: terms[key2]).

What you are asking for is not usually called eager evaluation; that’s what we already have. All function arguments are eagerly evaluated before the function is called. You’re asking for lazy evaluation of an expression used as a function call argument. There has been some previous discussion of generalized syntax for lazy-evaluated expression which you can probably find by searching. The way to do it today is by wrapping in a lambda and then explicitly calling it when you want to evaluate the expression.

I don’t think we will ever have implicit lazy evaluation that depends on the function/method being called (eg “default argument to dict.get is always lazily evaluated.”) That’s too implicit and unpredictable. If we ever had the feature, it would require explicit syntax to request lazy evaluation.

8 Likes

One can also take advantage of the short-circuiting behavior of or, as long as the values are truth-y: terms.get(key) or slow_recalculate(key), and terms.get(key1) or terms[key2]

2 Likes

You guys are right - I meant short-circuited/implicit lazy evaluation, not eager execution, and in retrospect it makes no sense to special-case the behavior for get. It was a brain foggy day for me, I guess. Appreciate the help. :grin:

That’s still eager, though.

Sure. But it expresses the requested semantics (value at key1, if present, otherwise value at key2) safely, at some cost in efficiency (always does two lookups.)

I actually do want the KeyError if neither key is found, but not if either of the keys are found. The point I didn’t really emphasize in my use case is that the indexes pulled from the dictionary are used in slicing, where I care about the special semantics of None