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?

Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
  • 1
    Are you looking to get the closure cells, or the values in those cells? – abarnert Apr 04 '18 at 23:28
  • @abarnert I want the value in the cell. Obscure cpython objects like closure cells aren't of much use to me :) – Aran-Fey Apr 04 '18 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. – abarnert Apr 04 '18 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… – abarnert Apr 04 '18 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. – abarnert Apr 04 '18 at 23:58
  • @abarnert You're right, nonlocal variables are included in `f_locals` even if they're never assigned to or read from. – Aran-Fey Apr 05 '18 at 00:01

1 Answers1

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.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • Makes sense, for unused variables. What about actual closure variables though? Is it possible for those? – Aran-Fey Apr 04 '18 at 23:28
  • @Aran-Fey: Those show up in `f_locals`, which confusingly, is not just locals. – user2357112 Apr 04 '18 at 23:29
  • 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. – Aran-Fey Apr 04 '18 at 23:33
  • 1
    `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. – abarnert Apr 04 '18 at 23:35
  • Okay, so `co_varnames`, `co_cellvars` and `co_freevars` only contain variable names, and the closure variables are actually included in `f_locals`. But then, what's the purpose of the `__closure__` attribute of function objects? What's that used for? – Aran-Fey Apr 04 '18 at 23:39
  • 1
    @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. – user2357112 Apr 04 '18 at 23:46
  • @Aran-Fey The `__closure__` attribute is a tuple of one cell per freevar, captured when the `MAKE_FUNCTION` opcode is executed in the outer function. When the function is called and a frame is created to run its code, the cells from `__closure__` are copied into the fast-locals array (and new cells are created for the cellvars). – abarnert Apr 04 '18 at 23:46