为啥调用 locals() 会添加引用?

Posted

技术标签:

【中文标题】为啥调用 locals() 会添加引用?【英文标题】:why does a call to locals() add a reference?为什么调用 locals() 会添加引用? 【发布时间】:2014-03-08 00:00:16 【问题描述】:

我不明白以下行为。

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())

输出是:

$ python demo.py 
3
3
4
4

【问题讨论】:

我的猜测是函数调用的局部变量dict是在第一次需要时创建的,然后被缓存。不过,我不太了解 Python 的内部结构,不知道 Python 是否真的这样做了。 @user2357112:你的猜测是正确的,在我的回答中得到了证明。 【参考方案1】:

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

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

static PyObject *
builtin_locals(PyObject *self)

    PyObject *d;

    d = PyEval_GetLocals();
    Py_XINCREF(d);
    return d;
   

反过来,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;

这是为框架的局部变量分配一个普通的旧字典并将任何“快速”变量填充到其中的位。由于新 dict 被附加到框架结构上(如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);

【讨论】:

不错!你在哪里找到这些功能的?我很难弄清楚大多数内置函数是在哪里实现的。 @user2357112 我的经理是 cpython 大师,把它们粘贴给我 =X 就是说,bltinmodule.c 实现了 __builtins__frameobject.c 实现了 python 的框架对象。【参考方案2】:

我在您的演示代码中添加了一些打印:

#! /usr/bin/python

import gc

from sys import getrefcount

def trivial(x): return x

def demo(x):
    print getrefcount(x)
    x = trivial(x)
    print getrefcount(x)
    print id(locals())
    print getrefcount(x)
    print gc.collect(), "collected"
    print id(locals())
    print getrefcount(x)


demo(object())

然后输出是(在我的机器上):

3
3
12168320
4
0 collected
12168320
4

locals() 实际上创建了一个包含 x 上的 ref 的 dict,因此 ref inc。 gc.collect() 不收集本地字典,你可以通过打印 id 来查看它,它是两次返回的同一个对象,它以某种方式为这一帧记忆,因此没有被收集。

【讨论】:

【参考方案3】:

这是因为 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())   

这是代码的输出

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>]

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

【讨论】:

OP 知道 locals 这样做。问题是 dict 及其引用会消失,因为没有存储对 dict 的显式引用。 另外,locals() 并没有说它创建一个字典。由于本地命名空间只是从名称到值的映射,我希望 dict 已经存在。编辑 locals() 的返回值的事实也符合这种直觉。 Ned Batcheler,coverage.py 的维护者,wrote about locals "locals() 函数比乍一看更复杂。返回值是一个字典,它是本地符号表的副本。这就是为什么更改 dict 可能实际上不会更改局部变量的原因。” @bukzor:你所期望的和 CPython 实际做的是两件不同的事情;-p。局部变量存储在 CPython 中的 dict 中的事实是,我想我引用了 Tim Peters,这是其中最重要的优化之一。

以上是关于为啥调用 locals() 会添加引用?的主要内容,如果未能解决你的问题,请参考以下文章

为啥添加额外的标头会导致 AJAX 调用失败

为啥当我在 cfoutput 中调用函数时,ColdFusion 会添加空格?

C#中想用messageBox这个类,为啥要添加引用才能使用求解答

为啥添加 DataTables 会破坏我的页面? [关闭]

Visual Studio调试器指南---Locals窗口

为啥要使用 DllImport 属性来代替添加引用?