为什么调用locals()会添加引用?

buk*_*zor 5 python refcounting python-internals

我不明白以下行为.

  • 如何locals()产生新的参考?
  • 为什么gc.collect没有删除它?我没有在locals()任何地方分配结果.

X

import gc

from sys import getrefcount

def trivial(x): return x

def demo(x):
    print getrefcount(x)
    x = trivial(x)
    print getrefcount(x)
    locals()
    print getrefcount(x)
    gc.collect()
    print getrefcount(x)


demo(object())
Run Code Online (Sandbox Code Playgroud)

输出是:

$ python demo.py 
3
3
4
4
Run Code Online (Sandbox Code Playgroud)

buk*_*zor 6

这与“快速局部变量”有关,它存储为一对匹配的元组,用于快速整数索引(一个用于名称f->f_code->co_varnames,一个用于值f->f_localsplus)。当locals()被调用时,快速局部变量被转换为标准字典并附加到框架结构上。cpython 代码的相关部分如下。

这是 的实现函数locals()。它的作用只不过是调用PyEval_GetLocals

static PyObject *
builtin_locals(PyObject *self)
{
    PyObject *d;

    d = PyEval_GetLocals();
    Py_XINCREF(d);
    return d;
}   
Run Code Online (Sandbox Code Playgroud)

反过来,PyEval_GetLocals除了调用之外,几乎没有什么作用PyFrame_FastToLocals

PyObject *
PyEval_GetLocals(void)
{   
    PyFrameObject *current_frame = PyEval_GetFrame();
    if (current_frame == NULL)
        return NULL;
    PyFrame_FastToLocals(current_frame);
    return current_frame->f_locals;
}
Run Code Online (Sandbox Code Playgroud)

这是为框架的局部变量分配普通旧字典并将任何“快速”变量填充到其中的位。由于新的字典被附加到框架结构上(如f->f_locals),任何“快速”变量在调用 locals() 时都会获得额外的引用。

void
PyFrame_FastToLocals(PyFrameObject *f)
{
    /* Merge fast locals into f->f_locals */
    PyObject *locals, *map;
    PyObject **fast;
    PyObject *error_type, *error_value, *error_traceback;
    PyCodeObject *co;
    Py_ssize_t j;
    int ncells, nfreevars;
    if (f == NULL)
        return;
    locals = f->f_locals;
    if (locals == NULL) {
        /* This is the dict that holds the new, additional reference! */
        locals = f->f_locals = PyDict_New();  
        if (locals == NULL) {
            PyErr_Clear(); /* Can't report it :-( */
            return;
        }
    }
    co = f->f_code;
    map = co->co_varnames;
    if (!PyTuple_Check(map))
        return;
    PyErr_Fetch(&error_type, &error_value, &error_traceback);
    fast = f->f_localsplus;
    j = PyTuple_GET_SIZE(map);
    if (j > co->co_nlocals)
        j = co->co_nlocals;
    if (co->co_nlocals)
        map_to_dict(map, j, locals, fast, 0);
    ncells = PyTuple_GET_SIZE(co->co_cellvars);
    nfreevars = PyTuple_GET_SIZE(co->co_freevars);
    if (ncells || nfreevars) {
        map_to_dict(co->co_cellvars, ncells,
                    locals, fast + co->co_nlocals, 1);
        /* If the namespace is unoptimized, then one of the
           following cases applies:
           1. It does not contain free variables, because it
              uses import * or is a top-level namespace.
           2. It is a class namespace.
           We don't want to accidentally copy free variables
           into the locals dict used by the class.
        */
        if (co->co_flags & CO_OPTIMIZED) {
            map_to_dict(co->co_freevars, nfreevars,
                        locals, fast + co->co_nlocals + ncells, 1);
        }
    }
    PyErr_Restore(error_type, error_value, error_traceback);
}
Run Code Online (Sandbox Code Playgroud)


ani*_*haw -1

这是因为 locals() 创建了一个实际的字典并将 x 放入其中,因此增加了 x 的引用计数,该字典可能被缓存。

所以我通过添加两行来更改代码

import gc

from sys import getrefcount

def trivial(x): return x

def demo(x):
   print getrefcount(x)
   x = trivial(x)
   print getrefcount(x)
   print "Before Locals ",  gc.get_referrers(x)
   locals()
   print "After Locals ",  gc.get_referrers(x)
   print getrefcount(x)
   gc.collect()
   print getrefcount(x)
   print "After garbage collect", gc.get_referrers(x)

demo(object())   
Run Code Online (Sandbox Code Playgroud)

这是代码的输出

3
3
Before Locals  [<frame object at 0x1f1ee30>]
After Locals  [<frame object at 0x1f1ee30>, {'x': <object object at 0x7f323f56a0c0>}]
4
4
After garbage collect [<frame object at 0x1f1ee30>, {'x': <object object at 0x7f323f56a0c0>}]
Run Code Online (Sandbox Code Playgroud)

似乎即使在垃圾收集之后它也会缓存 dict 值以供将来调用 locals() 。

  • Ned Batcheler,coverage.py 的维护者,[写了关于 locals 的文章](http://nedbatchelder.com/blog/201211/tricky_locals.html)“locals() 函数比乍一看要复杂。返回的值是字典是本地符号表的副本。这就是为什么更改字典实际上可能不会更改本地变量。” (3认同)
  • @bukzor:你所期望的和 CPython 实际所做的是两件不同的事情;-p。事实上,局部变量“不”存储在 CPython 的字典中,我想我引用的是 Tim Peters,这是其中最重要的优化之一。 (3认同)
  • OP 知道“当地人”会这样做。问题是字典及其引用预计会消失,因为没有存储对该字典的显式引用。 (2认同)