<div dir="ltr"><div>OK, I apologize for not catching on to the changed semantics of f_locals (which in the proposal is always a proxy for the fast locals and the cells). I don't know if I just skimmed that part of the PEP or that it needs calling out more.</div><div><br></div><div>I'm assuming that there are backwards compatibility concerns, and the PEP is worried that more code will break if one of the simpler options is chosen. In particular I guess that the choice of returning the same object is meant to make locals() in a function frame more similar to locals() in a module frame. And the choice of returning a plain dict rather than a proxy is meant to make life easier for code that assumes it's getting a plain dict.</div><div><br></div><div>Other than that I agree that returning a proxy (i.e. just a reference to f_locals) seems to be the most attractive option...<br></div></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Mon, May 27, 2019 at 9:41 AM Nathaniel Smith <<a href="mailto:njs@pobox.com">njs@pobox.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">On Mon, May 27, 2019 at 9:16 AM Guido van Rossum <<a href="mailto:guido@python.org" target="_blank">guido@python.org</a>> wrote:<br>
><br>
> I re-ran your examples and found that some of them fail.<br>
><br>
> On Mon, May 27, 2019 at 8:17 AM Nathaniel Smith <<a href="mailto:njs@pobox.com" target="_blank">njs@pobox.com</a>> wrote:<br>
[...]<br>
>> The interaction between f_locals and and locals() is also subtle:<br>
>><br>
>>   def f():<br>
>>       a = 1<br>
>>       loc = locals()<br>
>>       assert "loc" not in loc<br>
>>       # Regular variable updates don't affect 'loc'<br>
>>       a = 2<br>
>>       assert loc["a"] == 1<br>
>>       # But debugging updates do:<br>
>>       sys._getframe().f_locals["a"] = 3<br>
>>       assert a == 3<br>
><br>
><br>
> That assert fails; `a` is still 2 here for me.<br>
<br>
I think you're running on current Python, and I'm talking about the<br>
semantics in the current PEP 558 draft, which redefines f_locals so<br>
that the assert passes. Nick has a branch here if you want to try it:<br>
<a href="https://github.com/python/cpython/pull/3640" rel="noreferrer" target="_blank">https://github.com/python/cpython/pull/3640</a><br>
<br>
(Though I admit I was lazy, and haven't tried running my examples at<br>
all -- they're just based on the text.)<br>
<br>
>><br>
>>       assert loc["a"] == 3<br>
>>       # But it's not a full writeback<br>
>>       assert "loc" not in loc<br>
>>       # Mutating 'loc' doesn't affect f_locals:<br>
>>       loc["a"] = 1<br>
>>       assert sys._getframe().f_locals["a"] == 1<br>
>>       # Except when it does:<br>
>>       loc["b"] = 3<br>
>>       assert sys._getframe().f_locals["b"] == 3<br>
><br>
><br>
> All of this can be explained by realizing `loc is sys._getframe().f_locals`. IOW locals() always returns the dict in f_locals.<br>
<br>
That's not true in the PEP version of things. locals() and<br>
frame.f_locals become radically different. locals() is still a dict<br>
stored in the frame object, but f_locals is a magic proxy object that<br>
reads/writes to the fast locals array directly.<br>
<br>
>><br>
>> Again, the results here are totally different if a Python-level<br>
>> tracing/profiling function is installed.<br>
>><br>
>> And you can also hit these subtleties via 'exec' and 'eval':<br>
>><br>
>>   def f():<br>
>>       a = 1<br>
>>       loc = locals()<br>
>>       assert "loc" not in loc<br>
>>       # exec() triggers writeback, and then mutates the locals dict<br>
>>       exec("a = 2; b = 3")<br>
>>       # So now the current environment has been reflected into 'loc'<br>
>>       assert "loc" in loc<br>
>>       # Also loc["a"] has been changed to reflect the exec'ed assignments<br>
>>       assert loc["a"] == 2<br>
>>       # But if we look at the actual environment, directly or via<br>
>>       # f_locals, we can see that 'a' has not changed:<br>
>>       assert a == 1<br>
>>       assert sys._getframe().f_locals["a"] == 1<br>
>>       # loc["b"] changed as well:<br>
>>       assert loc["b"] == 3<br>
>>       # And this *does* show up in f_locals:<br>
>>       assert sys._getframe().f_locals["b"] == 3<br>
><br>
><br>
> This works indeed. My understanding is that the bytecode interpreter, when accessing the value of a local variable, ignores f_locals and always uses the "fast" array. But exec() and eval() don't use fast locals, their code is always compiled as if it appears in a module-level scope.<br>
><br>
> While the interpreter is running and no debugger is active, in a function scope f_locals is not used at all, the interpreter only interacts with the fast array and the cells. It is initialized by the first locals() call for a function scope, and locals() copies the fast array and the cells into it. Subsequent calls in the same function scope keep the same value for f_locals and re-copy fast and cells into it. This also clears out deleted local variables and emptied cells, but leaves "strange" keys (like "b" in the examples) unchanged.<br>
><br>
> The truly weird case happen when Python-level tracers are present, then the contents of f_locals is written back to the fast array and cells at certain points. This is intended for use by pdb (am I the only user of pdb left in the world?), so one can step through a function and mutate local variables. I find this essential in some cases.<br>
<br>
Right, the original goal for the PEP was to remove the "truly weird<br>
case" but keep pdb working<br>
<br>
>><br>
>> Of course, many of these edge cases are pretty obscure, so it's not<br>
>> clear how much they matter. But I think we can at least agree that<br>
>> this isn't the one obvious way to do it :-).<br>
>><br>
>><br>
>> ##### What's the landscape of possible semantics?<br>
>><br>
>> I did some brainstorming, and came up with 4 sets of semantics that<br>
>> seem plausible enough to at least consider:<br>
>><br>
>> - [PEP]: the semantics in the current PEP draft.<br>
><br>
><br>
> To be absolutely clear this copies the fast array and cells to f_locals when locals() is called, but never copies back, except when Python-level tracing/profiling is on.<br>
<br>
In the PEP draft, it never copies back at all, under any circumstance.<br>
<br>
>><br>
>> - [PEP-minus-tracing]: same as [PEP], except dropping the writeback on<br>
>> Python-level trace/profile events.<br>
><br>
><br>
> But this still copies the fast array and cells to f_locals when a Python trace function is called, right? It just doesn't write back.<br>
<br>
No, when I say "writeback" in this email I always mean<br>
PyFrame_FastToLocals. The PEP removes PyFrame_LocalsToFast entirely.<br>
<br>
>> - [snapshot]: in function scope, each call to locals() returns a new,<br>
>> *static* snapshot of the local environment, removing all this<br>
>> writeback stuff. Something like:<br>
>><br>
>>   def locals():<br>
>>       frame = get_caller_frame()<br>
>>       if is_function_scope(frame):<br>
>>           # make a point-in-time copy of the "live" proxy object<br>
>>           return dict(frame.f_locals)<br>
>>       else:<br>
>>           # in module/class scope, return the actual local environment<br>
>>           return frame.f_locals<br>
><br>
><br>
> This is the most extreme variant, and in this case there is no point in having f_locals at all for a function scope (since nothing uses it). I'm not 100% sure that you understand this.<br>
<br>
Yes, this does suggest an optimization: you should be able to skip<br>
allocating a dict for every frame in most cases. I'm not sure how much<br>
of a difference it makes. In principle we could implement that<br>
optimization right now by delaying the dict allocation until the first<br>
time f_locals or locals() is used, but we currently don't bother. And<br>
even if we adopt this, we'll still need to keep a slot in the frame<br>
struct to allocate the dict if we need to, because people can still be<br>
obnoxious and do frame.f_locals["unique never before seen name"] =<br>
blah and expect to be able to read it back later, which means we need<br>
somewhere to store that. (In fact Trio does do this right now, as part<br>
of its control-C handling stuff, because there's literally no other<br>
place where you can store information that a signal handler can see<br>
when it's walking the stack.) We could deprecate writing new names to<br>
f_locals like this, but that's a longer-term thing.<br>
<br>
>><br>
>> - [proxy]: Simply return the .f_locals object, so in all contexts<br>
>> locals() returns a live mutable view of the actual environment:<br>
>><br>
>>   def locals():<br>
>>       return get_caller_frame().f_locals<br>
><br>
><br>
> So this is PEP without any writeback. But there is still copying from the fast array and cells to f_locals. Does that only happen when locals() is called? Or also when a Python-level trace/profiling function is called?<br>
> My problem with all variants except what's in the PEP is that it would leave pdb *no* way (short of calling into the C API using ctypes) of writing back local variables.<br>
<br>
No, this option is called [proxy] because this is the version where<br>
locals() and f_locals *both* give you magic proxy objects where<br>
__getitem__ and __setitem__ access the fast locals array directly, as<br>
compared to the PEP where only f_locals gives you that magic object.<br>
<br>
-n<br>
<br>
--<br>
Nathaniel J. Smith -- <a href="https://vorpus.org" rel="noreferrer" target="_blank">https://vorpus.org</a><br>
</blockquote></div><br clear="all"><br>-- <br><div dir="ltr" class="gmail_signature"><div dir="ltr"><div>--Guido van Rossum (<a href="http://python.org/~guido" target="_blank">python.org/~guido</a>)</div><div><i style="font-family:Arial,Helvetica,sans-serif;font-size:small;font-weight:400;letter-spacing:normal;text-align:start;text-indent:0px;text-transform:none;white-space:normal;word-spacing:0px;background-color:rgb(255,255,255);color:rgb(136,136,136)"><span>Pronouns</span>: he/him/his </i><a href="http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-change-the-world/" style="color:rgb(17,85,204);font-family:Arial,Helvetica,sans-serif;font-size:small;font-style:normal;font-weight:400;letter-spacing:normal;text-align:start;text-indent:0px;text-transform:none;white-space:normal;word-spacing:0px;background-color:rgb(255,255,255)" target="_blank"><i>(why is my <span>pronoun</span> here?)</i></a></div></div></div>