Issue #17934: Add a clear() method to frame objects, to help clean up expensive details (local variables) and break reference cycles.

This commit is contained in:
Antoine Pitrou 2013-08-05 23:26:40 +02:00
parent c53204b947
commit 58720d6145
10 changed files with 80 additions and 10 deletions

View File

@ -846,6 +846,10 @@ index of the current line within that list.
finally:
del frame
If you want to keep the frame around (for example to print a traceback
later), you can also break reference cycles by using the
:meth:`frame.clear` method.
The optional *context* argument supported by most of these functions specifies
the number of lines of context to return, which are centered around the current
line.

View File

@ -934,6 +934,20 @@ Internal types
frame). A debugger can implement a Jump command (aka Set Next Statement)
by writing to f_lineno.
Frame objects support one method:
.. method:: frame.clear()
This method clears all references to local variables held by the
frame. Also, if the frame belonged to a generator, the generator
is finalized. This helps break reference cycles involving frame
objects (for example when catching an exception and storing its
traceback for later use).
:exc:`RuntimeError` is raised if the frame is currently executing.
.. versionadded:: 3.4
Traceback objects
.. index::
object: traceback

View File

@ -36,6 +36,8 @@ typedef struct _frame {
non-generator frames. See the save_exc_state and swap_exc_state
functions in ceval.c for details of their use. */
PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;
/* Borrowed referenced to a generator, or NULL */
PyObject *f_gen;
PyThreadState *f_tstate;
int f_lasti; /* Last instruction if called */
@ -46,6 +48,7 @@ typedef struct _frame {
bytecode index. */
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;

View File

@ -36,6 +36,8 @@ PyAPI_FUNC(PyObject *) PyGen_New(struct _frame *);
PyAPI_FUNC(int) PyGen_NeedsFinalizing(PyGenObject *);
PyAPI_FUNC(int) _PyGen_FetchStopIterationValue(PyObject **);
PyObject *_PyGen_Send(PyGenObject *, PyObject *);
PyAPI_FUNC(void) _PyGen_Finalize(PyObject *self);
#ifdef __cplusplus
}

View File

@ -764,7 +764,7 @@ class SizeofTest(unittest.TestCase):
nfrees = len(x.f_code.co_freevars)
extras = x.f_code.co_stacksize + x.f_code.co_nlocals +\
ncells + nfrees - 1
check(x, vsize('12P3i' + CO_MAXBLOCKS*'3i' + 'P' + extras*'P'))
check(x, vsize('13P3ic' + CO_MAXBLOCKS*'3i' + 'P' + extras*'P'))
# function
def func(): pass
check(func, size('12P'))

View File

@ -150,11 +150,17 @@ class SyntaxTracebackCases(unittest.TestCase):
class TracebackFormatTests(unittest.TestCase):
def test_traceback_format(self):
try:
def some_exception(self):
raise KeyError('blah')
def check_traceback_format(self, cleanup_func=None):
try:
self.some_exception()
except KeyError:
type_, value, tb = sys.exc_info()
if cleanup_func is not None:
# Clear the inner frames, not this one
cleanup_func(tb.tb_next)
traceback_fmt = 'Traceback (most recent call last):\n' + \
''.join(traceback.format_tb(tb))
file_ = StringIO()
@ -183,12 +189,22 @@ class TracebackFormatTests(unittest.TestCase):
# Make sure that the traceback is properly indented.
tb_lines = python_fmt.splitlines()
self.assertEqual(len(tb_lines), 3)
banner, location, source_line = tb_lines
self.assertEqual(len(tb_lines), 5)
banner = tb_lines[0]
location, source_line = tb_lines[-2:]
self.assertTrue(banner.startswith('Traceback'))
self.assertTrue(location.startswith(' File'))
self.assertTrue(source_line.startswith(' raise'))
def test_traceback_format(self):
self.check_traceback_format()
def test_traceback_format_with_cleared_frames(self):
# Check that traceback formatting also works with a clear()ed frame
def cleanup_tb(tb):
tb.tb_frame.clear()
self.check_traceback_format(cleanup_tb)
def test_stack_format(self):
# Verify _stack functions. Note we have to use _getframe(1) to
# compare them without this frame appearing in the output

View File

@ -10,6 +10,9 @@ Projected Release date: 2013-09-08
Core and Builtins
-----------------
- Issue #17934: Add a clear() method to frame objects, to help clean up
expensive details (local variables) and break reference cycles.
Library
-------

View File

@ -488,7 +488,7 @@ frame_traverse(PyFrameObject *f, visitproc visit, void *arg)
}
static void
frame_clear(PyFrameObject *f)
frame_tp_clear(PyFrameObject *f)
{
PyObject **fastlocals, **p, **oldtop;
Py_ssize_t i, slots;
@ -500,6 +500,7 @@ frame_clear(PyFrameObject *f)
*/
oldtop = f->f_stacktop;
f->f_stacktop = NULL;
f->f_executing = 0;
Py_CLEAR(f->f_exc_type);
Py_CLEAR(f->f_exc_value);
@ -519,6 +520,25 @@ frame_clear(PyFrameObject *f)
}
}
static PyObject *
frame_clear(PyFrameObject *f)
{
if (f->f_executing) {
PyErr_SetString(PyExc_RuntimeError,
"cannot clear an executing frame");
return NULL;
}
if (f->f_gen) {
_PyGen_Finalize(f->f_gen);
assert(f->f_gen == NULL);
}
frame_tp_clear(f);
Py_RETURN_NONE;
}
PyDoc_STRVAR(clear__doc__,
"F.clear(): clear most references held by the frame");
static PyObject *
frame_sizeof(PyFrameObject *f)
{
@ -538,6 +558,8 @@ PyDoc_STRVAR(sizeof__doc__,
"F.__sizeof__() -> size of F in memory, in bytes");
static PyMethodDef frame_methods[] = {
{"clear", (PyCFunction)frame_clear, METH_NOARGS,
clear__doc__},
{"__sizeof__", (PyCFunction)frame_sizeof, METH_NOARGS,
sizeof__doc__},
{NULL, NULL} /* sentinel */
@ -566,7 +588,7 @@ PyTypeObject PyFrame_Type = {
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,/* tp_flags */
0, /* tp_doc */
(traverseproc)frame_traverse, /* tp_traverse */
(inquiry)frame_clear, /* tp_clear */
(inquiry)frame_tp_clear, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
@ -708,6 +730,8 @@ PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals,
f->f_lasti = -1;
f->f_lineno = code->co_firstlineno;
f->f_iblock = 0;
f->f_executing = 0;
f->f_gen = NULL;
_PyObject_GC_TRACK(f);
return f;

View File

@ -15,8 +15,8 @@ gen_traverse(PyGenObject *gen, visitproc visit, void *arg)
return 0;
}
static void
gen_finalize(PyObject *self)
void
_PyGen_Finalize(PyObject *self)
{
PyGenObject *gen = (PyGenObject *)self;
PyObject *res;
@ -140,6 +140,7 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc)
Py_XDECREF(t);
Py_XDECREF(v);
Py_XDECREF(tb);
gen->gi_frame->f_gen = NULL;
gen->gi_frame = NULL;
Py_DECREF(f);
}
@ -505,7 +506,7 @@ PyTypeObject PyGen_Type = {
0, /* tp_weaklist */
0, /* tp_del */
0, /* tp_version_tag */
gen_finalize, /* tp_finalize */
_PyGen_Finalize, /* tp_finalize */
};
PyObject *
@ -517,6 +518,7 @@ PyGen_New(PyFrameObject *f)
return NULL;
}
gen->gi_frame = f;
f->f_gen = (PyObject *) gen;
Py_INCREF(f->f_code);
gen->gi_code = (PyObject *)(f->f_code);
gen->gi_running = 0;

View File

@ -1182,6 +1182,7 @@ PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
stack_pointer = f->f_stacktop;
assert(stack_pointer != NULL);
f->f_stacktop = NULL; /* remains NULL unless yield suspends frame */
f->f_executing = 1;
if (co->co_flags & CO_GENERATOR && !throwflag) {
if (f->f_exc_type != NULL && f->f_exc_type != Py_None) {
@ -3206,6 +3207,7 @@ fast_yield:
/* pop frame */
exit_eval_frame:
Py_LeaveRecursiveCall();
f->f_executing = 0;
tstate->frame = f->f_back;
return retval;