15

I have found some vaguely related questions to this question, but not any clean and specific solution for CPython. And I assume that a "valid" solution is interpreter specific.

First the things I think I understand:

  • locals() gives a non-modifiable dictionary.
  • A function may (and indeed does) use some kind of optimization to access its local variables
  • frame.f_locals gives a locals() like dictionary, but less prone to hackish things through exec. Or at least I have been less able to do hackish undocumented things like the locals()['var'] = value ; exec ""
  • exec is capable to do weird things to the local variables, but it is not reliable --e.g. I read somewhere that it doesn't work in Python 3. Haven't tested.

So I understand that, given those limitations, it will never be safe to add extra variables to the locals, because it breaks the interpreter structure.

However, it should be possible to change a variable already existing, isn't it?

Things that I considered

  • In a function f, one can access the f.func_code.co_nlocals and f.func_code.co_varnames.
  • In a frame, the variables can be accessed / checked / read through the frame.f_locals. This is in the use case of setting a tracer through sys.settrace.
  • One can easily access the function in which a frame is --cosidering the use case of setting a trace and using it to "do things" in with the local variables given a certain trigger or whatever.

The variables should be somewhere, preferably writeable... but I am not capable of finding it. Even if it is an array (for interpreter efficient access), or I need some extra C-specific wiring, I am ready to commit to it.

How can I achieve that modification of variables from a tracer function or from a decorated wrapped function or something like that?

A full solution will be of course appreciated, but even some pointers will help me greatly, because I'm stuck here with lots of non writeable dictionaries :-/


Edit: Hackish exec is doing things like this or this

Community
  • 1
  • 1
MariusSiuram
  • 3,380
  • 1
  • 21
  • 40
  • 1
    The dictionary returned by `locals()` can be modified: you are doing it in your example! And what's "hackish and undocumented" in `locals()['var'] = value ; exec ""` ? What's the point of exec-ing an empty string? And why does `exec` do weird things? In Python 3 `exec` is simply a function (like `print`), nothing special – Andrea Corbellini Jan 07 '16 at 08:56
  • 3
    it looks like [XY problem](http://meta.stackexchange.com/q/66377/137096). What is your actual issue? Provide more context. To change a local variable e.g., a list `x`, just call its methods:`x.append("something")` or `a = 1` "changes" local variable `a` to `1`. – jfs Jan 07 '16 at 09:05
  • 1
    @AndreaCorbellini According to [official documentation](https://docs.python.org/3/library/functions.html?highlight=locals#locals): __Note__ The contents of this dictionary should not be modified; changes may not affect the values of local and free variables used by the interpreter. -- And you may find some questions that take about it, like [this answer](http://stackoverflow.com/a/5958992/1433901) – MariusSiuram Jan 07 '16 at 09:17
  • Why don't you just store your variables in a dict? Or `if change=='a': a=v; elif change=='b'...` – Kijewski Jan 07 '16 at 09:22
  • 1
    @J.F.Sebastian I would like a technical answer. However, a plausible use case would be an automatic checkpointing framework. Which is capable to (through `trace` functions) store the state of locals and globals and proceed to resume execution in a certain point (line number) while leaving the locals and globals "correct". Specially critical if there are lines with side effects which the framework wants to avoid executing again. – MariusSiuram Jan 07 '16 at 09:23
  • 1
    @Kay I cannot change "my" variables. This is for a middleware-like application, or a framework, or something like that. For this question, I do not assume ownership on the code of the function. – MariusSiuram Jan 07 '16 at 09:26
  • @MariusSiuram: if you are a debugger; you can generate whatever code you like on the fly. Code in Python is an object; like most of the rest. Also, I see that `pdb` can change local variables (add `import pdb; pdb.set_trace()`), though [there were/are some issues](http://bugs.python.org/issue5215). – jfs Jan 07 '16 at 10:15
  • @J.F.Sebastian if you can generate code on the fly but you cannot change the local variables because of some cache issues (at least I understood that), then generating code seems like no use (in this context). Thanks for the link, I will dig a little more to see what happens in 2.7 and 3.x versions... – MariusSiuram Jan 07 '16 at 11:35
  • @MariusSiuram: cache issues (with `frame.f_locals`) are unrelated to the case when you can inject code. – jfs Jan 11 '16 at 11:07

2 Answers2

14

It exists an undocumented C-API call for doing things like that:

PyFrame_LocalsToFast

There is some more discussion in this PyDev blog post. The basic idea seems to be:

import ctypes

...

frame.f_locals.update({
    'a': 'newvalue',
    'b': other_local_value,
})
ctypes.pythonapi.PyFrame_LocalsToFast(
    ctypes.py_object(frame), ctypes.c_int(0))

I have yet to test if this works as expected.

Note that there might be some way to access the Fast directly, to avoid an indirection if the requirements is only modification of existing variable. But, as this seems to be mostly non-documented API, source code is the documentation resource.

MariusSiuram
  • 3,380
  • 1
  • 21
  • 40
7

Based on the notes from MariusSiuram, I wrote a recipe that show the behavior.

The conclusions are:

  1. we can modify an existing variable
  2. we can delete an existing variable
  3. we can NOT add a new variable.

So, here is the code:

import inspect
import ctypes

def parent():
    a = 1
    z = 'foo'

    print('- Trying to add a new variable ---------------')
    hack(case=0)  # just try to add a new variable 'b'
    print(a)
    print(z)
    assert a == 1
    assert z == 'foo'

    try:
        print (b)
        assert False  # never is going to reach this point
    except NameError, why:
        print("ok, global name 'b' is not defined")

    print('- Trying to remove an existing variable ------')
    hack(case=1)
    print(a)
    assert a == 2
    try:
        print (z)
    except NameError, why:
        print("ok, we've removed the 'z' var")

    print('- Trying to update an existing variable ------')
    hack(case=2)
    print(a)
    assert a == 3


def hack(case=0):
    frame = inspect.stack()[1][0]
    if case == 0:
        frame.f_locals['b'] = "don't work"
    elif case == 1:
        frame.f_locals.pop('z')
        frame.f_locals['a'] += 1
    else:
        frame.f_locals['a'] += 1

    # passing c_int(1) will remove and update variables as well
    # passing c_int(0) will only update
    ctypes.pythonapi.PyFrame_LocalsToFast(
        ctypes.py_object(frame),
        ctypes.c_int(1))

if __name__ == '__main__':
    parent()

The output would be like:

- Trying to add a new variable ---------------
1
foo
ok, global name 'b' is not defined
- Trying to remove an existing variable ------
2
foo
- Trying to update an existing variable ------
3
asterio gonzalez
  • 1,056
  • 12
  • 12
  • formally you have 'constant' declaration in Python such C++ or other languages. I hope [this](https://stackoverflow.com/questions/2682745/how-do-i-create-a-constant-in-python#2682752) helps. – asterio gonzalez Jun 29 '20 at 18:07