Builtins.lazy for lazy arguments

You described them in your post, lambdas, and functools.partial, or just wrap stuff in a normal function. This doesn’t cover all the bases where lazy evaluation could be used of course, but your proposal doesn’t really say how those things should be covered either afaict

This has nothing to do with None.

At method implementation level, yes, naturally:

def get(self, key, default=None):
    if type(default) is lazy:
        default = default()
   ...

But at user level, this would be:

result = {}.get('a', default=lazy(math.factorial, 100_000))

This idea is a flagging mechanism for functions/methods that resolve lazy arguments within.

Yes, and I have laid out reasons why they don’t work or are inferior. I am open to be shown where I am wrong, but I would like to see some code, examples, reasoning to be able to understand.

Yes, this proposal is about “lazy default arguments”.

I think you should take some time to clarify the OP, it’s really hard to understand IMO.

1 Like

PEP 671 says hi.

You keep using this example. It still fails in generality, since now there’s a magic object type that cannot ever be your default, and it completely ignores the other way you could write this:

try: result = {}["a"]
except KeyError: result = math.factorial(100_000)

Why do you need to pass a second argument to .get() instead of catching the exception?

3 Likes

Do others think so too?
I did make an effort to be precise and clear…
Maybe some advice how it could be improved?

OK, I now get the point.
Looking back, I still don’t think you actually explained this in the OP.

I’m also not a fan.
You’re pushing the responsibility of resolving deferred expressions to the .get method. Which functions and methods should resolve deferred expressions when they’re passed in as function arguments? All of them? Then you lose a lot of the benefit. Some of them? Then how am I supposed to know whether a particular function or method does so?
I think it’s better for deferred expressions to be resolved explicitly.

1 Like

There is a workaround:

get('key', default=[lazy(...)])

But I can not see many cases where this might be needed.

For this specific case, yes. But not for say defaultdict, and many more complex ones.

And I would not write compound statement in 2 lines either, thus it would be a 4 line block. :slight_smile:

Well, the cases for which it does make sense can opt in.

Same as finding out argument types and functionality for any other function: documentation, help(foo) and other standard methods - nothing new here.

I am aware of PEP 671. I hope it passes some point before 2030 :confused:
There’s not much I can do about that though, is there? Other than be excited…

1 Like

The issue for me there is that it’d be easy to introduce a silent bug due to improperly reading the documentation. And I know of myself that I read only the argument names and type hints if I (think I) can get away with it. I suspect a lot of other people do too, though I shouldn’t project too much. Reading the docstrings of every function takes too long.

1 Like

Also, this is just not an issue. lazy is a flag-wrapper which one uses to signal before passing it into the function.

If one needs to pass its contents on, then it is exactly what one should do, without wrapping it in lazy. E.g. get('key', default=(foo, args, kwds))

So sensible code logic does seem to eliminate this issue altogether.

How would that look like? To me this seems quite straight forward.

This is fairly explicit and much less bug-prone to say Backquotes for deferred expression, where types are being hidden, etc…

Not sure I follow. Now you’ve just wrapped it in a list, and you have to know to unwrap it. It’s still a loss of generality, which doesn’t happen with the try/except version.

Two lines or four, doesn’t make a lot of difference; this is something that already works without requiring any changes to the .get() method. The point of dict.get() is to be simple and convenient for the cases where it works, and it doesn’t need to be completely general - because try/except provides full generality.

And everything that you’ve said about dict.get() applies identically to getattr(), which has a default parameter with the same purpose. And others. How many of them now would need to magically support this lazy object with special semantics? Are maintainers of third-party modules now going to have people reporting bugs to them because they don’t special-case the new lazy type? How much turmoil does this create, for how much benefit?

The try/except strategy will undoubtedly work, whether you like it or not, so you need to explain how this is better than it.

Can you elaborate on what wouldn’t work with defaultdict? I’ve read through the whole thread and I still don’t understand what the problem there is. It’s already lazy. Is the entire problem that you don’t want to have to import it?

I don’t understand. It’s an object, right? lazy is a regular Python object? Then it is an object that cannot be your default. You cannot have a lazy object be the default, because it would be magically evaluated upon use. Consider:

def lookup_or_retain(x):
    return xlat.get(x, x)

Currently, you have a complete guarantee that this will return either xlat[x] or x, and not anything else. That is no longer the case when you introduce this lazy object - it would return either xlat[x] or x() which is quite surprising.

1 Like
import some_lib

d = some_lib.their_dict({1:1, 2:2})
keys = [1,2,3]
for key in keys:
  a = d.get(key, lazy(lambda:3))
  if a == 3:
    ...
    #oops this never triggers

The problem I saw with the Backquotes for deferred expression, is that they too wanted to resolve the deferred expression automatically, rather than explicitly. I don’t see how there can be a good way to do that. Any time you want to work with deferred expressions and you want things to be well-defined and not insidiously surprising, you have to have a mechanic like

real_exp = resolve(def_exp)

or

real_exp = def_exp()

See my next post. In short this is unlikely to be needed and if it is, there are sensible ways to deal with it.

Yup, this is probably the most valid criticism of this. To put it simply: “is it beneficial enough to be worth it?”

Not exactly like that. It could be used instead of defaultdict via dict.setdefault if one wishes so, but it can just as well be used on a defaultdict, when different default is needed:

a = coll.defaultdict(list)
result = a.get('key', default=lazy(dict))
print(result)    # {}

try-except fails here, only if 'key' in a: ... else is valid, but that is much less efficient.

Also, this is not specifically about dict.get (although it would probably be the most common use case), but more about having a generic mechanism to flag “lazy arguments”, which anyone can re-use anywhere. And I don’t think there is much risk here of misuse. Risk of people using this in wrong places is not much greater than for any other object.

Yes, but why would anyone ever do that? What problem does this solve? lazy contains 3 members: func, args, kwds. If one wants to retrieve tham as default, one just passes them without wrapping them into lazy.

Yup, this is an object, which could be used by some_lib if it wishes to do so. It is what it is. As @Rosuav pointed out, this might be unnecessary fuss with insufficient benefits.

I personally found benefit in using such object across libraries - it isn’t necessarily the case for more global standard.

If a function wants to support this approach, why not just add a second argument:

result = {}.get('a', lazy_default=(math.factorial, 100_000))

That’s still just as disruptive for method implementers, but avoids the need for any new builtin.

Note that I’m not arguing in favour of this (I don’t think there’s a problem that needs solving here). I’m simply suggesting that the lazy builtin isn’t the essential part of your proposal. What’s actually key to your proposal is “functions should add a means for callers to pass an unevaluated function call as an alternative to an evaluated value”. And the cost of doing that is why I don’t think this proposal is viable[1].


  1. I assumed you’d get enough pushback on that aspect of your idea that you’d either fix it, or abandon the proposal. Sadly, that doesn’t appear to have happened… ↩︎

7 Likes

Yes, it does work, but not as elegantly.
Needs extra argument and extra argument validation, decisions of handling various input cases, etc.

This is similar to having extra argument lazy_default=True/False, which is more straight forward compared to lazy_default=(callable, args) and does not cause that much unnecessary complexity.

But as I said, I personally used it for a fair while before switching to what I am proposing here. API felt less elegant than it could be.

Implementation cost and API niceness compromise is as good as I could get with this over a fairly long period of time.

This is my favourite among all that have been mentioned by myself and others in this thread.

No hard flaws of this have been identified so far - so I am happy to continue using it.
But I am not saying that this HAS TO be implemented.
I am just proposing it for discussion.