最近在用Python原生的API写一些逻辑,被维护PyObject引用计数搞得很是头疼,这里做些简单的总结,说明在什么时候一个PyObject会增加引用计数。
Python有几个存储操作:
- STORE_FAST
- STORE_GLOBAL
- STORE_NAME
- STORE_MAP
- ...
其实比较常见的也就是前两个,解析STORE_FAST说明一下Python是怎样操作引用计数的。
g_a = 1
def func(b):
global g_a
g_a = 1
b = 2
c = 3
上面func对应的字节码如下:
3 0 LOAD_CONST 1 (1)
3 STORE_GLOBAL 0 (g_a)
4 6 LOAD_CONST 2 (2)
9 STORE_FAST 0 (b)
5 12 LOAD_CONST 3 (3)
15 STORE_FAST 1 (c)
18 LOAD_CONST 0 (None)
21 RETURN_VALUE
看一下STORE_FAST源码:
TARGET(STORE_FAST)
{
v = POP();
SETLOCAL(oparg, v);
FAST_DISPATCH();
}
#define SETLOCAL(i, value) do { PyObject *tmp = GETLOCAL(i); \
GETLOCAL(i) = value; \
Py_XDECREF(tmp); } while (0)
#define GETLOCAL(i) (fastlocals[i])
#define BASIC_POP() (*--stack_pointer)
#define POP() BASIC_POP()
可以看出来SETLOCAL只做减引用,不做加引用,我们可以认为stack_pointer里面的Object已经加过引用计数了,因此很容易去想到看一下LOAD_FAST的实现:
TARGET(LOAD_FAST)
{
x = GETLOCAL(oparg);
if (x != NULL) {
Py_INCREF(x); [1]
PUSH(x);
FAST_DISPATCH();
}
format_exc_check_arg(PyExc_UnboundLocalError,
UNBOUNDLOCAL_ERROR_MSG,
PyTuple_GetItem(co->co_varnames, oparg));
break;
}
在[1]出会进行加引用,再PUSH到stack_pointer中。
好的,基本的逻辑也说搞明白了,总结如下:
任何的Object在PUSH到stack前,会有个地方进行加引用(这个地方可以是call_function内部,也可能就是字节码的内部实现,例如LOAD_CONST、LOAD_FAST等), POP处理的Object是不需要再加引用的!
那么什么时候对POP处理的Object解引用呢?这种情况就比较多了,比如如下代码:
TARGET(STORE_GLOBAL)
{
w = GETITEM(names, oparg);
v = POP();
err = PyDict_SetItem(f->f_globals, w, v);
Py_DECREF(v); [1]
if (err == 0) DISPATCH();
break;
}
我们看到[1]处解引用了,因为SetItem的过程中会增加引用,因此v变量已经是个临时变量了,需要将v再减一次引用。