『Python』为什么调用函数会令引用计数+2

一、问题描述

Python中的垃圾回收是以引用计数为主,分代收集为辅,引用计数的缺陷是循环引用的问题。在Python中,如果一个对象的引用数为0,Python虚拟机就会回收这个对象的内存。

sys.getrefcount(a)可以查看a对象的引用计数,但是比正常计数大1,因为调用函数的时候传入a,这会让a的引用计数+1

导致引用计数+1的情况:

  1. 对象被创建,例如a=23
  2. 对象被引用,例如b=a
  3. 对象被作为参数,传入到一个函数中,例如func(a)
  4. 对象作为一个元素,存储在容器中,例如list1=[a,a]

导致引用计数-1的情况:

  1. 对象的别名被显式销毁,例如del a
  2. 对象的别名被赋予新的对象,例如a=24
  3. 一个对象离开它的作用域,例如f函数执行完毕时,func函数中的局部变量(全局变量不会)
  4. 对象所在的容器被销毁,或从容器中删除对象

在网上看到一段有意思的例子:

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,这是为什么?

init 153
after a=11 154
after b=a 155
in func function 157
after func(a) 155
after list1=[a,12,14] 158
after a=12 155
after del a 155
after del b 154
after del list1 153

我们对函数进行修改:

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 函数内部操作:

  4           0 LOAD_GLOBAL              0 (print)
2 LOAD_CONST 1 ('in func function')
4 LOAD_GLOBAL 1 (sys)
6 LOAD_ATTR 2 (getrefcount)
8 LOAD_FAST 0 (c)
10 CALL_FUNCTION 1
12 LOAD_CONST 2 (1)
14 BINARY_SUBTRACT
16 CALL_FUNCTION 2
18 POP_TOP
20 LOAD_CONST 0 (None)
22 RETURN_VALUE

这里会读取变量 c(偏移量8的操作码),最终导致了增加计数为 2。

上一篇:zabbix一些高级功能介绍


下一篇:[Ecmall]ECMALL目录结构设置与数据库表