Skip to content

Conversation

@ncoghlan
Copy link
Contributor

Changing the frame API semantics based on whether or not a
tracing function is active is tricky to implement and hard
to document clearly, so this simplifies the proposal by
instead having the frame API always expose a write-through
proxy at function scope, and restricting the dynamic
snapshot behaviour to the locals() builtin.

Changing the frame API semantics based on whether or not a
tracing function is active is tricky to implement and hard
to document clearly, so this simplifies the proposal by
instead having the frame API always expose a write-through
proxy at function scope, and restricting the dynamic
snapshot behaviour to the locals() builtin.
@ncoghlan
Copy link
Contributor Author

@njsmith attempting to implement the dynamic frame semantics for PEP 558 convinced me that you were right and it wasn't a good idea in the first place, so this switches to the simpler design with more consistent API behaviour.

@njsmith
Copy link
Contributor

njsmith commented May 14, 2019

The other big question for me is whether it makes more sense to keep locals() acting differently from f_locals, or to always return the proxy object no matter how you're accessing the locals.

The main arguments I see for potentially changing locals() would be

  • the current semantics are pretty confusing and not particularly useful
  • if we stop exposing the underlying storage dict at the python level, we could potentially skip allocating it entirely for most frames (except in very rare cases like someone writing to a key that's not in the locals array), and delete the whole LocalsToFast/FastToLocals machinery.

I think this could also address the concerns the PEP raises about the cost of allocating the proxy object. I guess right now the proposal is that references go:

proxy -> frame -> backing dict

Suppose we stop allocating the backing dict except in special cases. Then we have a free pointer field in the frame object. We could repurpose that pointer to hold a cached, lazily allocated proxy object. So a frame starts out:

frame -> NULL

and then in the rare cases where someone accesses f_locals or locals(), we allocate the proxy and cache it in the frame:

frame -> proxy

and in the even rarer case where someone writes to a proxy key that doesn't have a corresponding fast-local entry, then we need a backing store:

frame -> proxy -> backing dict

The key trick here is that since the proxy is cached, that not only saves on allocations if it's being accessed repeatedly, it also makes it ok for it to hold state, because we know that repeated accesses will hit the same proxy object.

A downside is that this would introduce a circular reference, since the proxy also needs to hold a reference to the frame. Maybe this would be fine because it would only affect frames where the locals were actually introspected, which is rare, and we have a GC? But having locals introspection delay deallocation would be user visible, and people do rely on prompt deallocation no matter how often we say that it's not guaranteed by the language. Maybe there's some way to break the reference? Or I guess we could continue to keep the backing store in the frame object (but lazily allocate it to get the speedup on most frame creation), and just use a freelist to make proxy object access cheap, if it's really a problem?

@ncoghlan ncoghlan merged commit 44f2986 into python:master May 21, 2019
@ncoghlan
Copy link
Contributor Author

ncoghlan commented May 21, 2019

Changing locals() would be a backwards incompatible change, so it's a complete non-starter.

That isn't at all obvious though, so I'll add a new design discussion section about that.

@njsmith
Copy link
Contributor

njsmith commented May 21, 2019 via email

@ncoghlan
Copy link
Contributor Author

Folks do it all the time by calling exec() and eval() without explicit namespaces at function scope - that implicitly passes globals() and locals() to the executed code, and the presence of locals() keeps the module globals from being mutated, while the fact locals() is a dynamic snapshot keeps the function locals from being mutated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants