『Python』为什么调用函数会令引用计数+2
一、问题描述
Python中的垃圾回收是以引用计数为主,分代收集为辅,引用计数的缺陷是循环引用的问题。在Python中,如果一个对象的引用数为0,Python虚拟机就会回收这个对象的内存。
sys.getrefcount(a)
可以查看a对象的引用计数,但是比正常计数大1,因为调用函数的时候传入a,这会让a的引用计数+1
导致引用计数+1的情况:
- 对象被创建,例如
a=23
- 对象被引用,例如
b=a
- 对象被作为参数,传入到一个函数中,例如
func(a)
- 对象作为一个元素,存储在容器中,例如
list1=[a,a]
导致引用计数-1的情况:
- 对象的别名被显式销毁,例如
del a
- 对象的别名被赋予新的对象,例如
a=24
- 一个对象离开它的作用域,例如f函数执行完毕时,func函数中的局部变量(全局变量不会)
- 对象所在的容器被销毁,或从容器中删除对象
在网上看到一段有意思的例子:
import sys def func(c): print ('in func function', sys.getrefcount(c) - 1) print (id(func.__globals__['a'])) print ('init', sys.getrefcount(11) - 1) a = 11 # print (id(a)) print ('after a=11', sys.getrefcount(11) - 1) b = a print ('after b=a', sys.getrefcount(11) - 1) func(11) print ('after func(a)', sys.getrefcount(11) - 1) list1 = [a, 12, 14] print ('after list1=[a,12,14]', sys.getrefcount(11) - 1) a=12 print ('after a=12', sys.getrefcount(11) - 1) del a print ('after del a', sys.getrefcount(11) - 1) del b print ('after del b', sys.getrefcount(11) - 1) # list1.pop(0) # print 'after pop list1',sys.getrefcount(11)-1 del list1 print ('after del list1', sys.getrefcount(11) - 1)
输出的init不一定一致,作为计数基础即可(小数int 在python中会默认维护,因为python很多内置量都是小数int,即计数不可能为0),输出中有一点比较奇怪:在传入函数中后计数增加为2,而非设想的1,这是为什么?
我们对函数进行修改:
def func(c): print ('in func function', sys.getrefcount(c) - 1) # print (id(func.__globals__['a'])) for attr in dir(func): print (attr, getattr(func, attr))
替换掉之前的函数,运行之可以发现func.__globals__属性中记录了全局变量键值对 {'a': 11} 这样(以及其他信息),这就是额外的计数来历:局部变量和全局变量的值是相同的,这导致计数+2。
我们知道,函数也是对象,即使不在函数体内我们也可以调用函数的属性、方法,我们把下面一句从函数体中拿出来单独运行,就发现,由于脱离了函数作用域,函数的__globals__属性中对于全局变量的记载('a'、'b')都不见了,这可以理解,脱离了作用域,局部变量和全局变量都失去了意义(两者都是针对某个作用域的概念)。
for attr in dir(func):
print (attr, getattr(func, attr))
测试发现__globals__中记录的{'a': 11}和函数体外的变量 a 是同一个对象(id相同),且在外面增加 b 的时候引用计数差值并没有增加,所以这个解释是不对的,实际上另一个引用是函数栈保存了入参对形参的引用(知乎找到的解释)。
二、代码分析
看到了知乎的解释,我决定自行验证一下,测试代码如下:
import sys def func(c): print ('in func function', sys.getrefcount(c)-1) print ('init', sys.getrefcount(11) - 1) func(11) print ('init', sys.getrefcount(11) - 1)
init 106
in func function 108
init 106
进一步分析一下:
from dis import dis order = \ """ def func(c): print ('in func function', sys.getrefcount(c)-1) print ('init', sys.getrefcount(11) - 1) func(11) print ('init', sys.getrefcount(11) - 1) """ dis(order)
返回值如下,
2 0 LOAD_CONST 0 (<code object func at 0x0000029849AD5D20, file "<dis>", line 2>) 2 LOAD_CONST 1 ('func') 4 MAKE_FUNCTION 0 6 STORE_NAME 0 (func) 5 8 LOAD_NAME 1 (print) 10 LOAD_CONST 2 ('init') 12 LOAD_NAME 2 (sys) 14 LOAD_ATTR 3 (getrefcount) 16 LOAD_CONST 3 (11) 18 CALL_FUNCTION 1 20 LOAD_CONST 4 (1) 22 BINARY_SUBTRACT 24 CALL_FUNCTION 2 26 POP_TOP 6 28 LOAD_NAME 0 (func) 30 LOAD_CONST 3 (11) 32 CALL_FUNCTION 1 34 POP_TOP 7 36 LOAD_NAME 1 (print) 38 LOAD_CONST 2 ('init') 40 LOAD_NAME 2 (sys) 42 LOAD_ATTR 3 (getrefcount) 44 LOAD_CONST 3 (11) 46 CALL_FUNCTION 1 48 LOAD_CONST 4 (1) 50 BINARY_SUBTRACT 52 CALL_FUNCTION 2 54 POP_TOP 56 LOAD_CONST 5 (None) 58 RETURN_VALUE
着重看6:
6 28 LOAD_NAME 0 (func) 30 LOAD_CONST 3 (11) 32 CALL_FUNCTION 1 34 POP_TOP
这里将函数 func 和常量11压入了函数栈,会导致引用计数 +1。
我们再看下面代码:
dis(func)
返回的是 func 函数内部操作:
这里会读取变量 c(偏移量8的操作码),最终导致了增加计数为 2。