[Python-ideas] Delayed Execution via Keyword

Steven D'Aprano steve at pearwood.info
Fri Feb 17 06:10:05 EST 2017


On Fri, Feb 17, 2017 at 12:24:53AM -0500, Joseph Hackman wrote:

> I propose a keyword to mark an expression for delayed/lazy execution, for
> the purposes of standardizing such behavior across the language.
> 
> The proposed format is:
> delayed: <expr>
> i.e. log.info("info is %s", delayed: expensiveFunction())

Keywords are difficult: since by definition they are not backwards 
compatible, they make it hard for people to write version independent 
code, and will break people's code. Especially something like "delayed", 
I expect that there is lots of code that used "delayed" as a regular 
name.

    if status.delayed:
        ...

A new keyword means it can't be back-ported to older versions, and will 
break code.


> Unlike 'lambda' which returns a function (so the receiver must be
> lambda-aware), delayed execution blocks are for all purposes values. The
> first time the value (rather than location) is read, 

What counts as "reading" a value? Based on your example below, I can't 
tell if passing the object to *any* function is enough to trigger 
evaluation, or specifically print is the magic that makes it happen.


> or any method on the delayed object is called, 

I don't think that can work -- it would have to be any attribute access, 
surely, because Python couldn't tell if the attribute was a method or 
not until it evaluated the lazy object. Consider:

spam = delayed: complex_calculation()
a = spam.thingy

What's `a` at this point? Is is still some sort of lazy object, waiting 
to be evaluated? If so, how is Python supposed to know if its a method?

result = a()


> the expression is executed and the delayed
> expression is replaced with the result. (Thus, the delayed expression is
> only every evaluated once).

That's easily done by having the "delayed" keyword cache each expression 
it sees, but that seems like a bad idea to me:

spam = delayed: get_random_string()
eggs = delayed: get_random_string()  # the same expression

spam.upper()  # convert to a real value

assert spam == eggs  # always true, as they are the same expression

Worse, suppose module a.py has:

spam = delayed: calculate(1)

and module b.py has:

eggs = delayed: calculate(1)

where a.calculate and b.calculate do completely different things. The 
result you get will depend on which happens to be evaluated first and 
cached, and would be a nightmare to debug. Truely spooky action-at-a-
distance code.

I think it is better to stick to a more straight-forward, easily 
understood and debugged system based on object identity rather than 
expressions.


> Ideally:
> a = delayed: 1+2
> b = a
> print(a) #adds 1 and 2, prints 3
> # a and b are now both just 3
> print(b) #just prints 3

That would work based on object identity too.

By the way, that's probably not the best example, because the keyhole 
optimizer will likely have compiled 1+2 as just 3, so you're effectively 
writing:

a = delayed: 3

At least, that's what I would want: I would argue strongly against lazy 
objects somehow defeating the keyhole optimizer. If I write:

a = delayed: complex_calculation(1+2+3, 4.5/3, 'abcd'*3)

what I hope will be compiled is:

a = delayed: complex_calculation(6, 0.6428571428571429, 'abcdabcdabcd')

same as it would be now (at least in CPython), apart from the "delayed:" 
keyword.
a = delayed: complex_calculation(1+2+3, 4.5/3, 'abcd'*3)


> Mechanically, this would be similar to the following:
> 
> class Delayed():
>     def __init__(self, func):
>         self.__func = func
>         self.__executed = False
>         self.__value = None
> 
>     def __str__(self):
>         if self.__executed:
>             return self.__value.__str__()
>         self.__value = self.__func()
>         self.__executed = True
>         return self.__value.__str__()

So you're suggesting that calling str(delayed_object) is the one way to 
force evaluation?



I have no idea what the following code is supposed to mean.


> def function_print(value):
>     print('function_print')
>     print(value)
> 
> def function_return_stuff(value):
>     print('function_return_stuff')
>     return value
> 
> function_print(function_return_stuff('no_delay'))
> 
> function_print(Delayed(lambda: function_return_stuff('delayed')))
> 
> delayed = Delayed(lambda: function_return_stuff('delayed_object'))
> function_print(delayed)
> function_print(delayed)




-- 
Steve


More information about the Python-ideas mailing list