[Python-ideas] Delay evaluation of annotations
Nick Coghlan
ncoghlan at gmail.com
Mon Sep 26 00:57:36 EDT 2016
On 26 September 2016 at 03:42, Stephen J. Turnbull
<turnbull.stephen.fw at u.tsukuba.ac.jp> wrote:
> Nick Coghlan writes:
>
> > This code works as a doctest today:
> >
> > >>> def func(a: "Expected output"):
> > ... pass
> > ...
> > >>> print(func.__annotations__["a"])
> > Expected output
> >
> > Any change that breaks that currently valid doctest is necessarily a
> > compatibility break for the way annotations are handled at runtime. It
> > doesn't matter for that determination how small the change to fix the
> > second command is, it only matters that it *would* have to change in
> > some way.
>
> This is a bit unfair to אלעזר, although it's been a long thread so I
> can understand why some of his ideas have gone missing. His proposals
> have gotten a bit incoherent because he has been answering all the
> different objections one by one rather than organizing things into a
> single design, but I think eventually he would organize it as follows:
>
> (1) Add __raw_annotations__ and save the thunked expressions there,
> whether as code objects or AST.
> (2) Turn __annotations__ into a property which evaluates (and
> memoizes?) the thunks and returns them. (First explicitly
> suggested by Alexander Belopol, I think.)
>
> He claims that (2) solves the backward compatibility problem, I don't
> have the knowledge to figure out whether it is that simple or not. It
> seems plausible to me, so I'd love to hear an explanation. New ideas
> like DBC would of course be supported by the new __raw_annotations__
> since there's no backward compatibility issue there.
OK, that does indeed make more sense, and significantly reduces the
scope for potential runtime compatibility breaks related to
__annotations__ access. Instead, it changes the discussion to focus on
the following main challenges:
- the inconsistency introduced between annotations (lazily evaluated)
and default arguments (eagerly evaluated)
- the remaining compatibility breaks (depending on implementation details)
- the runtime overhead of lazy evaluation
- the debugging challenges of lazy evaluation
The inconsistency argument is simply that people will be even more
confused than they are today if default arguments are evaluated at
definition time while annotations aren't. There is a lot of code out
there that actively relies on eager evaluation of default arguments,
so changing that is out of the question, which then provides a strong
consistency argument in favour of keeping annotations eagerly
evaluated as well.
There would likely still be some compatibility breaks around name
access in method annotation definitions, and compatibility would also
break for any code that actually did expect to trigger a side-effect
at definition time. This is a much smaller scope for breakage than
breaking __annotations__ access, but we can't assume it won't affect
anyone as there's a lot of code out there that we'd judge to be
questionable from the point of view of maintainability and good design
aesthetics that nevertheless still solves the problem the author was
aiming to solve.
The runtime memory overhead of lazy evaluation isn't trivial. Using a
naive function based approach:
>>> import sys
>>> sys.getsizeof("")
49
>>> sys.getsizeof(lambda: "")
136
And that's only the function object itself - it's not counting all the
other objects hanging off the function object like the attribute
dictionary. A more limited thunk type could reduce that overhead, but
it's still going to be larger in most cases than just storing the
evaluation result.
The impact on runtime speed overhead is less certain, but also likely
to be a net negative - defining functions isn't particularly cheap
(especially compared to literal references or a simple name lookup),
and calling them if you actually access __annotations__ isn't going to
be particularly cheap either.
The debugging challenge is the same one that arises with any form of
delayed evaluation: by default, the traceback you get will point you
to the location where the delayed evaluation took place *not* the
location where the flawed expression was found. That problem can be
mitigated through an exception chaining design that references the
likely location of the actual error, but it's never going to be as
easy to figure out as cases where the traceback points directly at the
code responsible for the problem.
So I'm still -1 on the idea, but it's not as straightforward as the
argument against the naive version of the proposal that also broke
__annotations__ lookup.
Cheers,
Nick.
P.S. As an illustration of that last point, the PEP 487 implementation
currently makes problems with __set_name__ attribute definitions quite
hard to figure out since the traceback points at the class definition
header, rather than the offending descriptor assignment:
http://bugs.python.org/issue28214
--
Nick Coghlan | ncoghlan at gmail.com | Brisbane, Australia
More information about the Python-ideas
mailing list