5

I'm trying to find out if it's possible to resolve variables in stack frames (as returned by inspect.currentframe()).

In other words, I'm looking for a function

def resolve_variable(variable_name, frame_object):
    return value_of_that_variable_in_that_stackframe

For an example, consider the following piece of code:

global_var = 'global'

def foo():
    closure_var = 'closure'

    def bar(param):
        local_var = 'local'

        frame = inspect.currentframe()
        assert resolve_variable('local_var', frame) == local_var
        assert resolve_variable('param', frame) == param
        assert resolve_variable('closure_var', frame) == closure_var
        assert resolve_variable('global_var', frame) == global_var

    bar('parameter')

foo()

Local and global variables are trivially looked up through the f_locals and f_globals attributes of the frame object:

def resolve_variable(variable_name, frame_object):
    try:
        return frame_object.f_locals[variable_name]
    except KeyError:
        try:
            return frame_object.f_globals[variable_name]
        except KeyError:
            raise NameError(varname) from None

But the problem are closure variables. They aren't stored in a dictionary like the local and global variables, as far as I know. To make things even worse, variables only become closure variables if the function actually accesses them (for example by reading its value like _ = closure_var or writing to it with nonlocal closure_var; closure_var = _). So there are actually 3 different cases:

global_var = 'global'

def foo():
    unused_cvar = 'unused'  # actually not a closure variable at all
    readonly_cvar = 'closure'
    nonlocal_cvar = 'nonlocal'

    def bar(param):
        nonlocal nonlocal_cvar

        local_var = 'local'
        _ = readonly_cvar
        nonlocal_cvar = 'nonlocal'

        frame = inspect.currentframe()
        assert resolve_variable('local_var', frame) == local_var
        assert resolve_variable('param', frame) == param
        assert resolve_variable('unused_cvar', frame) == 'unused'
        assert resolve_variable('readonly_cvar', frame) == readonly_cvar
        assert resolve_variable('nonlocal_cvar', frame) == nonlocal_cvar
        assert resolve_variable('global_var', frame) == global_var

    bar('parameter')

foo()

How can I rewrite my resolve_variable function to support all of these? Is it even possible?

6
  • 1
    Are you looking to get the closure cells, or the values in those cells? Commented Apr 4, 2018 at 23:28
  • @abarnert I want the value in the cell. Obscure cpython objects like closure cells aren't of much use to me :) Commented Apr 4, 2018 at 23:36
  • Obscure? I think as of 3.7 they're finally mentioned in the docs somewhere. And, even better, the set-contents method is exposed to Python code, so you grub through a function's __closure__ and change the captured values. Commented Apr 4, 2018 at 23:50
  • PS, doesn’t nonlocal closure_var capture it even if you never reference or assign to it? It did in 3.0; I don’t know if I’ve ever checked since then… Commented Apr 4, 2018 at 23:56
  • But meanwhile, for this particular use case, where you’re actually calling bar from within the same function it was defined in, you can cheat and look at f_back.f.locals to see all the locals of foo, captured or not. I don’t know if that’ll help for your real use case, if you have one, but it’ll work here. Commented Apr 4, 2018 at 23:58

1 Answer 1

5

Not generally possible. Python only holds onto closure variables that closures actually refer to.

>>> import inspect
>>> class Demo(object):
...     def __del__(self):
...         print("Too late, it's gone.")
... 
>>> def f():
...     a = Demo()
...     def g():
...         return inspect.currentframe()
...     return g
... 
>>> frame = f()()
Too late, it's gone.

As you can see from this example, there's no hope of inspecting a from the frame frame. It's gone.

As for closure variables the frame actually used, those usually show up in f_locals. I know of one weird case where they won't, which is if the frame is for a class statement with closure variables:

>>> def f():
...     a = 1
...     class Foo(object):
...         print(a)
...         print(inspect.currentframe().f_locals)
...     return Foo
... 
>>> f()
1
{'__module__': '__main__', '__qualname__': 'f.<locals>.Foo'}
<class '__main__.f.<locals>.Foo'>

After digging through the CPython implementation (specifically frame objects, the LOAD_CLASSDEREF opcode, and inspect.getclosurevars), I think the only way to access class frame closure variables is going to be with ctypes, gc.get_referents, or similarly nasty means.

Also, note that the f_locals dict may not be up to date if the closure variable values have changed since it was accessed; accessing frame.f_locals again refreshes the contents, but it might be out of date again by the time you look.

Sign up to request clarification or add additional context in comments.

7 Comments

Makes sense, for unused variables. What about actual closure variables though? Is it possible for those?
@Aran-Fey: Those show up in f_locals, which confusingly, is not just locals.
Huh, so they do. Most curious. Could I ask you to add that to the answer? It doesn't feel like a complete answer without that.
f_locals is really a faked useless thing. You can distinguish real locals from captured nonlocals by looking at f_code.co_varnames, co_cellvars, and co_freevars for the names of uncaptured locals, locals captured by an inner function, and closure variables handed down by an outer function. These also give you the indices into the real locals array on the frame (which has cell objects for cellvars and freevars), but you can't do much with that without the C API.
@Aran-Fey: That holds closure cells, which is what Python actually uses for closure variable lookup; it doesn't look in f_locals for closure variables.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.