Search code examples
pythonintrospectionframesys

Is it possible to write to a python frame object as returned by sys._getframe() from python code running within the interpreter?


Apropos of This question, there is a bit of scaffolding within the interpreter to inspect frame objects, which can be retrieved by sys._getframe(). The frame objects appear to be read only, but I can't find anything obvious in the docs that explicitly states this. Can someone confirm whether these objects are writeable (in some way) or read only?

import sys

def foobar():
    xx='foo'
    ff = sys._getframe()
    ff.f_locals['xx'] = 'bar'
    print xx

if __name__ == '__main__':
    foobar()

This prints out 'foo' when run but the post below demonstrates the variable being writable when run from the current frame in an interactive shell.


Solution

  • From CPython source, Objects/frameobject.c:

    static PyMemberDef frame_memberlist[] = {
        {"f_back",      T_OBJECT,       OFF(f_back),    RO},
        {"f_code",      T_OBJECT,       OFF(f_code),    RO},
        {"f_builtins",  T_OBJECT,       OFF(f_builtins),RO},
        {"f_globals",   T_OBJECT,       OFF(f_globals), RO},
        {"f_lasti",     T_INT,          OFF(f_lasti),   RO},
        {"f_exc_type",  T_OBJECT,       OFF(f_exc_type)},
        {"f_exc_value", T_OBJECT,       OFF(f_exc_value)},
        {"f_exc_traceback", T_OBJECT,   OFF(f_exc_traceback)},
        {NULL}    /* Sentinel */
    };
    ...
    static PyGetSetDef frame_getsetlist[] = {
        {"f_locals",    (getter)frame_getlocals, NULL, NULL},
        {"f_lineno",    (getter)frame_getlineno,
                        (setter)frame_setlineno, NULL},
        {"f_trace",     (getter)frame_gettrace, (setter)frame_settrace, NULL},
        {"f_restricted",(getter)frame_getrestricted,NULL, NULL},
        {0}
    };
    

    For the PyMemberDef, the flags RO or READONLY means it's attributes are read-only. For the PyGetSetDef, if it only has a getter, it's read only. This means all attributes but f_exc_type, f_exc_value, f_exc_traceback and f_trace are read-only after creation. This is also mentioned in the docs, under Data model.

    The objects referred to by the attributes is not necessarily read-only. You could do this:

    >>> f = sys._getframe()
    >>> f.f_locals['foo'] = 3
    >>> foo
    3
    >>>
    

    Though this works in the interpreter, it fails inside functions. The execution engine uses a separate array for local variables (f_fastlocals), which is merged into f_locals on access, but the converse is not true.

    >>> def foo():
    ...   x = 3
    ...   f = sys._getframe()
    ...   print f.f_locals['x']
    ...   x = 4
    ...   print f.f_locals['x']
    ...   d = f.f_locals
    ...   x = 5
    ...   print d['x']
    ...   f.f_locals
    ...   print d['x']
    ...
    >>> foo()
    3
    4
    4
    5
    >>>
    

    On the global frame, f_local refers to f_globals, which makes this trick work in the interpreter. Modifying f_globals works, but affects the whole module.