一、问题背景
遇到了一段代码,如下所示,第一眼的直觉是变量ins
是一个局部变量,重复调用test()
函数会反复创建新的实例ins
。
import sys
def test():
ins = object()
print(id(ins))
while True:
test()
但实际情况却是在进程运行期间局部变量ins
的id
一直没有发生改变。
140076922921824
140076922921824
140076922921824
140076922921824
二、原因分析
2.1 分析引用计数
在上面示例代码中补充一行代码看一下实际的ins
引用变量计数情况,代码如下所示:
mport sys
def test():
ins = object()
print(id(a))
ref = sys.getrefcount(ins)
print(f'引用计数: {ref}')
while True:
test()
执行此示例代码的输出结果是:
139765934961536
引用计数: 2
139765934961536
引用计数: 2
139765934961536
引用计数: 2
这里读者可能会产生一个疑问:这里只有变量ins
引用到object()实例,为什么引用计数是2?
这个结果是因为调用sys.getrefcount()
函数的时候会多产生一个引用计数,具体gc的管理机制可以点这里。
从上面引用计数输出看,实际唯一的一个引用计数就是ins
变量,那我们删除一下此变量引用后再看object()
实例id是否发生变化?
import sys
def test():
ins = object()
print(id(ins))
del ins
while True:
test()
执行上述代码看ins
的实例ins
的id还是没有发生任何变化,实际输出如下所示。
这里和我判断的预期情况不一样,我的预期情况是ins
唯一引用被删除后就会触发GC
行为,ins
对应的内存块就会被回收。
140306047644512
140306047644512
140306047644512
进一步修改示例代码查看实际的实例删除动作是否进行了?如下所示。
import sys
import gc
class Son(object):
def __del__(self):
print('Deleting instance')
def test():
ins = Son()
print(id(ins))
while True:
test()
执行上述代码后的输出结果如下,从结果上看可以确认__del__
确实是执行了,那这样还有一种推测就是每一次创建ins
实例都是在原有内存中创建出来而导致实例id未发生改变。
140221875591104
Deleting instance
140221875591104
Deleting instance
140221875591104
Deleting instance
实际查看id()的帮助文档发现,其实没有时间重合度的对象有可能分配同个内存地址而导致id()
值一致。我翻看了CPython
的解释过程(参见2.2)和内存创建环节可以确认是简单调用malloc()
函数进行内存创建活动。
2.2 分析字节码
我们换个思路重新审视一下:从字节码入手看是否能看出一些蛛丝马迹或者排除一些无关项。
使用python3 -m dis test.py
可以输出python代码对应的字节码信息:
1 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (None)
4 IMPORT_NAME 0 (sys)
6 STORE_NAME 0 (sys)
4 8 LOAD_CONST 2 (<code object test at 0x7fece3319a80, file "test.py", line 4>)
10 LOAD_CONST 3 ('test')
12 MAKE_FUNCTION 0
14 STORE_NAME 1 (test)
10 >> 16 LOAD_NAME 1 (test)
18 CALL_FUNCTION 0
20 POP_TOP
22 JUMP_ABSOLUTE 16
Disassembly of <code object test at 0x7fece3319a80, file "test.py", line 4>:
5 0 LOAD_GLOBAL 0 (object)
2 CALL_FUNCTION 0
4 STORE_FAST 0 (ins)
6 6 LOAD_GLOBAL 1 (print)
8 LOAD_GLOBAL 2 (id)
10 LOAD_FAST 0 (ins)
12 CALL_FUNCTION 1
14 CALL_FUNCTION 1
16 POP_TOP
18 LOAD_CONST 0 (None)
20 RETURN_VALUE
从上面输出的字节码来看,和ins
变量相关的只有两处指令操作:
CALL_FUNCTION
: 调用object()
创建出相关的实例;STORE_FAST
和LOAD_FAST
: 对ins
变量进行存取操作。 排查这三个指令对应cpython代码可以确认都是正常的指令调用没有涉及GC相关活动。
2.2.1 CALL_FUNCTION
CPython 编译器内的指令代码如下所示,实际这个指令的动作就是调用C层面的函数执行,无内存管理相关动作。
case TARGET(CALL_FUNCTION): {
PREDICTED(CALL_FUNCTION);
PyObject **sp, *res;
sp = stack_pointer;
res = call_function(tstate, &trace_info, &sp, oparg, NULL);
stack_pointer = sp;
PUSH(res);
if (res == NULL) {
goto error;
}
CHECK_EVAL_BREAKER();
DISPATCH();
}
2.2.2 STORE_FAST/LOAD_FAST
CPython 编译器内的指令代码如下所示,实际这两个指令的动作就是在fastlocals数组
中存取相关变量。
case TARGET(STORE_FAST): {
PREDICTED(STORE_FAST);
PyObject *value = POP();
SETLOCAL(oparg, value);
DISPATCH();
}
case TARGET(LOAD_FAST): {
PyObject *value = GETLOCAL(oparg);
if (value == NULL) {
format_exc_check_arg(tstate, PyExc_UnboundLocalError,
UNBOUNDLOCAL_ERROR_MSG,
PyTuple_GetItem(co->co_varnames, oparg));
goto error;
}
Py_INCREF(value);
PUSH(value);
DISPATCH();
}
参考附录
Puzzled with LOAD_FAST/STORE_FAST of python
How can two Python objects have same id but 'is' operator returns False?