我一直在为同事们做一个演示文稿来解释GIL背后的基本行为和推理,并发现了一些我无法解释的内容,同时快速解释了引用计数。似乎新声明的变量有四个引用,而不是我期望的那个。例如,以下代码:
the_var = 'Hello World!'
print('Var created: {} references'.format(sys.getrefcount(the_var)))
此输出中的结果:
Var created: 4 references
如果我使用整数>我验证输出是相同的。如果我在函数范围或循环中声明了变量,则100(< 100是预先创建并具有更大的引用计数)或浮点数。结果是一样的。在2.7.11和3.5.1中,行为似乎也是相同的。
我试图调试sys.getrefcount以查看它是否正在创建其他引用,但无法进入该函数(我假设它是直接向下到C层)。
我知道当我出现时,我会至少得到一个关于此问题的问题,而实际上我对输出感到非常困惑。任何人都可以向我解释这种行为吗?
答案 0 :(得分:15)
有几种情况会产生不同的引用计数。最简单的是REPL控制台:
>>> import sys
>>> the_var = 'Hello World!'
>>> print(sys.getrefcount(the_var))
2
理解这个结果非常简单 - 本地堆栈中有一个引用,sys.getrefcount()
函数有一个临时/本地(甚至documentation警告它 - The count returned is generally one higher than you might expect
)。但是,当您将其作为独立脚本运行时:
import sys
the_var = 'Hello World!'
print(sys.getrefcount(the_var))
# 4
正如您所注意到的,您获得了4
。什么给出了什么?好吧,让我们调查......垃圾收集器有一个非常有用的接口 - gc
模块 - 所以如果我们在REPL控制台中运行它:
>>> import gc
>>> the_var = 'Hello World!'
>>> gc.get_referrers(the_var)
[{'__builtins__': <module '__builtin__' (built-in)>, '__package__': None, 'the_var': 'Hello
World!', 'gc': <module 'gc' (built-in)>, '__name__': '__main__', '__doc__': None}]
没有奇迹, - 基本上只是当前的命名空间(locals()
),因为变量在其他任何地方都不存在。但是当我们将它作为一个独立的脚本运行时会发生什么:
import gc
import pprint
the_var = 'Hello World!'
pprint.pprint(gc.get_referrers(the_var))
打印出来(YMMV,基于你的Python版本):
[['gc',
'pprint',
'the_var',
'Hello World!',
'pprint',
'pprint',
'gc',
'get_referrers',
'the_var'],
(-1, None, 'Hello World!'),
{'__builtins__': <module '__builtin__' (built-in)>,
'__doc__': None,
'__file__': 'test.py',
'__name__': '__main__',
'__package__': None,
'gc': <module 'gc' (built-in)>,
'pprint': <module 'pprint' from 'D:\Dev\Python\Py27-64\lib\pprint.pyc'>,
'the_var': 'Hello World!'}]
果然,正如sys.getrefcount()
告诉我们的那样,我们在列表中还有两个引用,但那到底是什么?好吧,当Python解释器正在解析你的脚本时,它首先需要compile它到字节码 - 当它执行时,它将所有字符串存储在一个列表中,因为它也提到你的变量,它被声明为一个引用它。
第二个更神秘的条目((-1, None, 'Hello World!')
)来自peep-hole optimizer并且只是优化访问(在这种情况下是字符串引用)。
这两个都是纯粹的临时和可选的 - REPL控制台正在进行上下文分离,因此如果你要外包,那么你就不会看到这些引用。根据您当前的背景进行编译:
import gc
import pprint
exec(compile("the_var = 'Hello World!'", "<string>", "exec"))
pprint.pprint(gc.get_referrers(the_var))
你得到:
[{'__builtins__': <module '__builtin__' (built-in)>,
'__doc__': None,
'__file__': 'test.py',
'__name__': '__main__',
'__package__': None,
'gc': <module 'gc' (built-in)>,
'pprint': <module 'pprint' from 'D:\Dev\Python\Py27-64\lib\pprint.pyc'>,
'the_var': 'Hello World!'}]
如果您要回到最初通过sys.getreferencecount()
获取引用计数的尝试:
import sys
exec(compile("the_var = 'Hello World!'", "<string>", "exec"))
print(sys.getrefcount(the_var))
# 2
就像在REPL控制台中一样,正如预期的那样。由于它是就地发生的窥孔优化所引起的额外引用,可以通过在计算引用之前强制进行垃圾收集(gc.collect()
)来立即丢弃。
但是,在整个文件被解析和编译之前,无法释放在编译期间创建的字符串列表,这就是为什么要在另一个脚本中导入脚本然后计算对{{1}的引用的原因从你那里得到the_var
而不是3
只是当你认为它不再混淆你时;)