6. Frame Objects
Frame objects provide the contextual environment for executing bytecode instructions.
Take the set of bytecode instructions in listing 6.0 for example, LOAD_COST loads values on to a stack, but it has no notion of where or what this stack is. The code object also has no information on the thread or interpreter state that is vital for execution.
0 LOAD_CONST 0 (<code object f at 0x102a028a0, file "fizzbuzz.py",\
line 1>)
2 LOAD_CONST 1 ('f')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (f)
8 LOAD_CONST 2 (None)
10 RETURN_VALUE
Executing code objects requires another data structure that provides such contextual information, and this is where the frame objects come into play.
One can think of the frame object as a
container in which the code object is executed - it knows about the code object and has references to
data and values required during the execution of some code object. As usual, Python does
provide us with some facilities to inspect frame objects using the sys._getframe() function, as shown in the Listing 6.1 snippet.
>>> import sys
>>> f = sys._getframe()
>>> f
<frame object at 0x10073ed48>
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'f_' is not defined
>>> dir(f)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__',
'__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__',
'__lt__', '__ne__', '__new__','__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'clear',
'f_back', 'f_builtins', 'f_code', 'f_globals', 'f_lasti', 'f_lineno',
'f_locals', 'f_trace']
Before a code object can be executed, a frame object within which the execution of such a code object
takes place is created.
Such a frame object contains all the namespaces required for the execution of a code object (local, global,
and builtin), a reference to the current thread of execution, stacks for evaluating byte code and other housekeeping information that are important for executing byte code. To get a better understanding
of the frame object, let us look at the definition of the frame object data structure from
the Include/frame.h module and reproduced in listing 6.2.
typedef struct _frame {
PyObject_VAR_HEAD
struct _frame *f_back; /* previous frame, or NULL */
PyCodeObject *f_code; /* code segment */
PyObject *f_builtins; /* builtin symbol table (PyDictObject) */
PyObject *f_globals; /* global symbol table (PyDictObject) */
PyObject *f_locals; /* local symbol table (any mapping) */
PyObject **f_valuestack; /* points after the last local */
PyObject **f_stacktop;
PyObject *f_trace; /* Trace function */
/* fields for handling generators*/
PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;
/* Borrowed reference to a generator, or NULL */
PyObject *f_gen;
int f_lasti; /* Last instruction if called */
int f_lineno; /* Current line number */
int f_iblock; /* index in f_blockstack */
char f_executing; /* whether the frame is still executing */
PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */
} PyFrameObject;
The fields coupled with the documentation within the frame are not difficult to understand but we provide a bit more detail about these fields and how they relate to the execution of bytecode.
-
f_back: This field is a reference to the frame of the code object that was executing before the current code object. Given a set of frame objects, thef_backfields of these frames together form a stack of frames that goes back to the initial frame. This initial frame then has aNULLvalue in thisf_backfield. This implicit stack of frames forms what we refer to as thecall stack. -
f_code: This field is a reference to a code object. This code object contains the bytecode that is executed within the context of this frame. -
f_builtins: This is a reference to the builtin namespace. This namespace contains names such asprint,enumerateetc. and their corresponding values. -
f_globals: This is a reference to the global namespace of a code object. -
f_locals: This is a reference to the local namespace of a code object. As previously mentioned, these names are defined within the scope of a function. When we discuss thef_localplusfield, we will see an optimization that Python does when working with locally defined names. -
f_valuestack: This is a reference to the evaluation stack for the frame. Recall that the Python virtual machine is a stack-based virtual machine so, during the evaluation of bytecode, values are read from the top of this stack and results of evaluating the byte code are stored on the top of this stack. This field is the stack that is used during code object execution. Thestacksizeof a frame’s code object gives the maximum depth to which this data structure can grow. -
f_stacktop: As the name suggests, the field points to the next free slot of the evaluation value stack. When a frame is newly created, this value is set to the value stack - this is the first available space on the stack as there are no items on the stack. -
f_trace: This field references a function that used for tracing the execution of python code. -
f_exc_type,f_exc_value,f_exc_traceback,f_gen: are fields used for bookkeeping to be able to execute generator code cleanly. More on this when we discuss python generators. -
f_localplus: This is a reference to an array that contains enough space for storing cell and local variables. This field enables the evaluation loop to optimize loading and storing values of names to and from the value stack with theLOAD_FASTandSTORE_FASTinstructions. TheLOAD_FASTandSTORE_FASTopcodes provide faster name access than their counterpartLOAD_NAMEandSTORE_NAMEopcodes because they use array indexing for accessing the value of names and this is done in approximately constant time, unlike their counterparts that search a mapping for a given name. When we discuss the evaluation loop, we see how this value is set up during the frame bootstrapping process. -
f_blockstack: This field references a data structure that acts as a stack used to handle loops and exception handling. This is the second stack in addition to the value stack that is of utmost importance to the virtual machine, but this does not receive as much attention as it rightfully should. The relationship between the block stack, exceptions and looping constructs is quite complicated, and we look at that in the coming chapters.
6.1 Allocating Frame Objects
Frame objects are ubiquitous during python code evaluation - every executed code block needs a frame object that provides some context. New frame objects are created by invoking the PyFrame_New
function in the Objects/frameobject.c module. This function is invoked so many times - whenever a
code object is executed, that two main optimizations are used to reduce the overhead of invoking this
function, and we briefly look at these optimizations.
First, code objects have a field, the co_zombieframe which references an inert frame
object. When a code object is executed, the frame within which it was executed is not immediately deallocated. The frame is rather maintained in the co_zombieframe so when next the same code object executed, time is not spent allocating memory for a new execution frame. The ob_type, ob_size, f_code, f_valuestack
fields retain their value; f_locals, f_trace, f_exc_type, f_exc_value, f_exc_traceback are
NULL and f_localplus retains its allocated space but with the local variables nulled out. The remaining fields do not hold a reference to any object. The second optimization that is used by the virtual machine is to maintain a free list of pre-allocated frame objects from which frames can be obtained for the execution of code objects.
The source code for frame objects is a gentle read and one can see how the zombie frame and
freelist concepts are implemented by looking at how allocated frames are deallocated after the execution of the enclosed code object. The
interesting part of the code for frame deallocation is shown in listing 6.3.
if (co->co_zombieframe == NULL)
co->co_zombieframe = f;
else if (numfree < PyFrame_MAXFREELIST) {
++numfree;
f->f_back = free_list;
free_list = f;
}
else
PyObject_GC_Del(f);
Careful observation shows that the freelist will only ever grow when a recursive call is made, i.e. a code object tries to execute itself as that is the only time the zombieframe field is NULL.
This small optimization of using the freelist helps eliminate to a certain degree, the repeated memory allocations for such recursive calls.
This chapter covers the main points about the frame object without delving into the evaluation loop, which is tightly integrated with the frame objects. A few things left out of this chapter are covered in subsequent chapters. For example,
- How are values passed on from one frame to the next when code execution hits a
returnstatement? - What is the thread state, and where does the thread state come from?
- How are exceptions bubble down the stack of frames when an exception is thrown in the executing frame? Etc.
Most of these question are answered when we look at the interpreter and thread state data structures in the next chapter, and then the evaluation loop in subsequent chapters.