我想要一种快速而又肮脏的方法来获取一些文件名而无需在shell中键入,所以我有以下这段代码:
from tkinter.filedialog import askopenfile
file = askopenfile()
现在,一切正常,但是确实创建了一个多余的tkinter
GUI,需要将其关闭。我知道我可以这样做来压制它:
import tkinter as tk
tk.Tk().withdraw()
但这并不意味着它没有装在背面。这只是意味着现在有一个Tk()
对象,我无法关闭/销毁该对象。
似乎每次创建Tk()
时,无论我del
还是destroy()
,都不会释放内存。见下文:
import tkinter as tk
import os, psutil
process = psutil.Process(os.getpid())
def mem(): print(f'{process.memory_info().rss:,}')
# initial memory usage
mem()
# 21,475,328
for i in range(20):
root.append(tk.Tk())
root[-1].destroy()
mem()
# 24,952,832
# 26,251,264
# ...
# 47,591,424
# 48,865,280
# try deleting the root instead
del root
mem()
# 50,819,072
可见,即使Tk()
的每个实例都被销毁并删除了root
,python也无法释放使用。但是,其他对象则不是这种情况:
class Foo():
def __init__(self):
# create a list that takes up approximately the same size as a Tk() on average
self.lst = list(range(11500))
for i in range(20):
root.append(Foo())
del root[-1]
mem()
# 52,162,560
# 52,162,560
# ...
# 52,162,560
所以我的问题是,Tk()
和我的Foo()
之间为什么有区别,为什么不破坏/删除创建的Tk()
会释放占用的内存?>
有什么明显的我想念的吗?我的测试不足以证实我的怀疑吗?我已经在这里和Google上进行了搜索,但没有找到答案。
编辑:以下是我尝试过(但失败了)的其他一些方法,其中包括注释中的建议:
# Force garbage collection
import gc
gc.collect()
# quit() method
root.quit()
# delete the entire tkinter reference
del tk
答案 0 :(得分:2)
这里有三个问题,其中一个是tkinter
的过错,其中一个是您的过错,其中一个是您的预期行为。
三个问题是:
tkinter
在注册其清理处理程序时会创建一个无法检测到的参考周期,只有通过显式调用destroy
才能打破该参考周期(如果不这样做,则参考周期永远是 已清理,并且资源被永久保留)Tk
个对象,也要坚持使用它们destroy
问题1意味着,如果有恢复内存的机会,您必须 destroy
明确创建的任何Tk
。
问题2意味着,如果要使该内存可用于其他用途,则必须在创建新内存之前明确删除对Tk
的任何引用(在destroy
之后)。在某些情况下,您还希望显式设置tk.NoDefaultRoot()
来防止创建的第一个Tk
作为默认根被缓存在tkinter
上(即,显式调用{{ 1}}在这样的对象上将清除缓存的默认根,因此在很多情况下这不会成为问题。
问题#3意味着您必须热切地删除引用,而不是等到程序结束后才删除destroy
root
;如果等到最后删除它,是的,内存将返回到堆,而不是返回到操作系统,因此看起来您仍在使用所有内存。但这不是一个真正的问题。如果操作系统需要RAM,则未使用的内存将分页到磁盘上(通常在活动页面之前先分页空闲页面),保留该页面可以提高大多数代码的性能。
具体地说,即使您明确list
.tk
实例Tk实例的destroy
属性也没有被清除>。您可以通过更改循环以摆脱对Tk
对象的最后引用来限制内存的增长,或者如果您只想释放低级C资源,请在{{1 }}新增Tk
元素**:
.tk
根据我经过稍微修改的脚本的输出,明确清除引用可以清除基础资源:
destroy
第一个对象从未完全回收内存的事实不足为奇。除非分配巨大(足够大以触发模式切换,该切换会向操作系统发出独立的请求,以独立于“小对象堆”)。否则,它们将维护一个空闲的内存列表,该列表不再使用并且可以重复使用。
这里大约6 MB的“浪费”可能是创建Tk
对象本身及其管理的对象树所涉及的一堆小分配,这些分配随后又返回堆以供重用,直到程序退出后才返回操作系统(也就是说,如果不再使用堆的那一部分,如果内存不足,操作系统可能会优先将未使用的部分分页到磁盘上)。您可以注意到内存使用几乎立即稳定,可以看到此优化如何提供帮助。新的# Not necessary, but avoids caching any Tk as a root when you don't want it
tk.NoDefaultRoot()
root = [] # Missing in your original code, but I'm assuming it was a plain list
for i in range(20):
root.append(tk.Tk())
root[-1].destroy()
# Either drop the reference to the `Tk` completely:
root[-1] = None
# or just drop the reference to its C level worker object
root[-1].tk = None
# Optionally, call gc.collect() here to forcibly reclaim memory faster
# otherwise you're likely to see memory usage grow by a few KB as uncleaned
# cycles aren't reclaimed in time so we see phantom leaks (that would
# eventually be cleaned)
mem()
对象只是在重用与第一个对象相同的内存(完全稳定的缺乏可能是由于堆碎片导致需要少量额外分配)。
答案 1 :(得分:2)
创建Tk
的实例时,您所创建的不仅仅是小部件。您正在创建一个具有多个属性的对象(嵌入式tcl解释器,小部件列表等)。当您执行root.destroy()
时,只破坏了该对象拥有的某些数据。对象本身仍然存在并占用内存。由于您在列表中保留了对该对象的引用,因此该对象永远不会被垃圾回收,因此内存会四处徘徊。
使用root = tk.Tk()
创建根窗口时,将返回一个对象(root
)。如果使用vars查看该对象的属性,则会看到以下内容:
>>> root = tk.Tk()
>>> vars(root)
{'children': {}, '_tkloaded': 1, 'master': None, '_tclCommands': ['tkerror', 'exit', '4463962184destroy'], 'tk': <_tkinter.tkapp object at 0x10a1d7f30>}
调用root.destroy()
时,您只是销毁了小部件本身(实际上是_tclCommands
列表中的元素)。该对象的其他部分保持不变。
>>> root.destroy()
>>> vars(root)
{'children': {}, '_tkloaded': 1, 'master': None, '_tclCommands': None, 'tk': <_tkinter.tkapp object at 0x10a1d7f30>}
请注意如何将_tclCommands
设置为None
,但是其余属性仍然占用内存。其中之一,tk
占用了大量的内存,从未回收。
要完全删除对象,您需要删除它。在您的情况下,您需要从列表中删除该项目,以便不再有对该对象的引用。然后,您可以等待垃圾收集器正常工作,也可以显式调用垃圾收集器。
这可能不会回收100%的内存,但它应该使您接近。
话虽如此,tkinter并非旨在以这种方式使用。基本的期望是您在程序开始时创建Tk
的单个实例,并使该单个实例保持活动状态,直到程序退出。
对于您的情况,我建议您在程序启动时一次创建根窗口,然后将其隐藏。然后,您可以在整个程序中随意调用askopenfile()
。如果您需要更多通用的功能,请创建一个函数,该函数在首次调用根窗口时创建该窗口并缓存该窗口,因此它只需创建一次即可。