python - 当变量超出范围时,Python不会删除变量
问题描述
考虑以下代码:
import random
class Trie:
def __init__(self, children, end):
self.children = children
self.end = end
def trie_empty():
return Trie(dict(), False)
def trie_insert(x, t):
if not x:
t.end = True
return
try:
t2 = t.children[x[0]]
except KeyError:
t2 = trie_empty()
t.children[x[0]] = t2
trie_insert(x[1:], t2)
def fill_dict(root):
memo = dict()
def fill(pfx='', depth=0):
try:
memo[pfx]
except KeyError:
pass
else:
return
if depth > 6:
return
for ci in range(ord('a'), ord('d') + 1):
fill(pfx + chr(ci), depth + 1)
bw = None
memo[pfx] = None, bw
fill()
# del memo
def random_word():
l = int(random.random() * 10)
w = ''.join([chr(int(random.random() * 26) + ord('a')) for _ in range(l)])
return w
def main():
t = trie_empty()
for _ in range(10000):
trie_insert(random_word(), t)
while True:
fill_dict(t)
if __name__ == '__main__':
main()
当我运行它时,它会继续使用更多内存,直到我杀死它。如果我取消注释del memo
,它会在使用恒定内存量的同时运行。由此,我得出结论,返回memo
时没有清理局部变量fill_dict
。
这种行为对我来说真的很神秘,特别是因为基本上上面所有的代码都是看到这种行为所必需的。即使是完全未使用的参数 tofill_dict
也不能省略,以使程序使用无限内存。
这真是令人沮丧。当然,现代的垃圾收集语言可以清理自己的变量,我不应该手动删除函数局部变量。甚至 C 也可以在函数返回时清理堆栈。为什么 Python 不能(在这种情况下)?
解决方案
我认为这个问题值得回答,现在我和 Program man 之间——并且match 在评论中提到了相同的起点——我们已经弄清楚了。
模块级函数fill_dict
有一个内部函数fill
:
def fill_dict(root):
memo = dict()
def fill(pfx='', depth=0):
此内部名称fill
绑定到通过编译其内容创建的实体。该实体引用memo
在入口处绑定到新的空字典的名称fill_dict
,因此该实体本身就是一个闭包。
现在,闭包可以进行垃圾收集,Python 确实有一个垃圾收集器。但 CPython 特别有一个两层收集器:有一种主要的、永远在线的、基于引用计数的收集器,然后是运行频率低得多的真正的标记和清除样式的 GC。(请参阅CPython 何时进行垃圾收集?为什么 python 对 gc 使用引用计数和标记和清除?)
边栏:引用计数收集器有什么问题?
引用计数收集器被循环打败:
>>> x = []
>>> x.append(x)
>>> x
[[...]]
Herex
绑定到一个列表,其第一个元素是绑定到的列表x
。即 x[0] 为 x,x[0][0] 为 x,以此类推:
>>> x[0] is x
True
>>> x[0][0] is x
True
对于这种循环,删除x
没有帮助,因为列表引用了它自己。但是,我们可以制作一个更漂亮的循环:
>>> a = dict()
>>> b = dict()
>>> a['link-to-b'] = b
>>> b['link-to-a'] = a
>>> a
{'link-to-b': {'link-to-a': {...}}}
>>> b
{'link-to-a': {'link-to-b': {...}}}
现在,如果我们关闭其中一个链接,循环就会消失:
>>> a['link-to-b'] = None
>>> a
{'link-to-b': None}
>>> b
{'link-to-a': {'link-to-b': None}}
一切都会好起来的。
回到手头的问题
在这种特殊情况下,在其外部实例中fill
具有对实例的引用,并且其中的条目之一是:memo
fill_dict
memo
memo[pfx] = None, bw
变量bw
本身是在闭包内部定义的,所以memo[pfx]
指的是闭包(或者更准确地说,是指闭包中的一个实体),而闭包指的是memo
,这就是我们的循环引用。
因此,即使fill_dict
返回,闭包上的引用计数也没有下降到零。
推荐阅读
- node.js - Socket.io 仅向房间中的某些用户发送消息
- c# - 创建新文件时自动更改“复制到输出目录”
- webpack - 使用 Webpack Encore 引入 RequireJS AMD 模块
- knex.js - knex.raw() 和 knex.schema.raw() 有什么区别?
- python - Django 2.13:/basic_app/user_login/ 的 NoReverseMatch
- c# - 调用 MySQL 过程给出“找不到过程”的异常
- php - 选择非括号和非引号逗号
- python - 为什么 cv2.imread 会改变像素值?
- java - 爪哇 | 移出边界索引,直到它在边界内
- python - 无法将操作“compute_qmap_grid/map/while/PrintV2”转换为张量