为什么Python中新创建的变量的ref-count为4?

时间:2017-07-10 21:28:50

标签: python

我一直在为同事们做一个演示文稿来解释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层)。

我知道当我出现时,我会至少得到一个关于此问题的问题,而实际上我对输出感到非常困惑。任何人都可以向我解释这种行为吗?

1 个答案:

答案 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只是当你认为它不再混淆你时;)