百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

python散装笔记——111: 垃圾回收

itomcoil 2025-03-25 14:27 26 浏览

1: 原始对象的重用

一个有助于优化应用程序的有趣现象是,原始类型实际上在底层也是通过引用计数来管理的。我们来看看数字:对于所有介于 -5 和 256 之间的整数,Python 总是重用同一个对象:

>>> import sys
>>> sys.getrefcount(1)
1262
>>> a = 1
>>> b = 1
>>> sys.getrefcount(1)
1433

请注意引用计数增加了,这意味着当 ab 指向 1 这个原始类型时,它们引用的是同一个底层对象。然而,对于更大的数字,Python 实际上并不会重用底层对象:

>>> a = 999999999
>>> sys.getrefcount(999999999)
2
>>> b = 999999999
>>> sys.getrefcount(999999999)
2

由于将 999999999 分配给 ab 时引用计数没有变化,我们可以推断它们指向两个不同的底层对象,尽管它们都被分配了相同的原始类型。

2: del命令的效果

使用 del v 从作用域中移除变量名,或者使用 del v[item]del[i:j] 从集合中移除对象,或者使用 del v.name 移除属性,或者其他任何移除对对象引用的方式,并不会直接触发任何析构函数调用或释放任何内存。只有当对象的引用计数达到零时,对象才会被析构。

>>> import gc
>>> gc.disable() # 禁用垃圾回收器
>>> class Track:
    def __init__(self):
        print("Initialized")

    def __del__(self):
        print("Destructed")
        
>>> def bar():
    return Track()
  
>>> t = bar()
Initialized
>>> another_t = t # 分配另一个引用
>>> print("...")
...
>>> del t # 尚未析构——`another_t` 仍然引用它
>>> del another_t # 最后一个引用被移除,对象被析构
Destructed

3: 引用计数

Python 的大部分内存管理是通过引用计数来处理的。

每当一个对象被引用(例如被分配给一个变量)时,它的引用计数会自动增加。当它被取消引用(例如变量超出作用域)时,它的引用计数会自动减少。

当引用计数达到零时,对象会立即被销毁,内存也会立即被释放。因此,在大多数情况下,垃圾回收器甚至都不是必需的。

>>> import gc
>>> gc.disable() # 禁用垃圾回收器
>>> class Track:
    def __init__(self):
        print("Initialized")
    def __del__(self):
        print("Destructed")

>>> def foo():
    Track()
    # 由于不再有任何引用,立即被析构
    print("---")
    t = Track()
    # 变量被引用,因此尚未被析构
    print("---")
    # 当函数退出时,变量被析构
>>> foo()
Initialized
Destructed
---
Initialized
---
Destructed

为了进一步说明引用的概念:

>>> def bar():
  return Track()
>>> t = bar()
Initialized
>>> another_t = t # 分配另一个引用
>>> print("...")
...
>>> t = None # 尚未析构——`another_t` 仍然引用它
>>> another_t = None # 最后一个引用被移除,对象被析构
Destructed

4: 处理引用循环的垃圾回收器

垃圾回收器只有在存在引用循环时才是必需的。最简单的引用循环例子是 A 指向 B,而 B 指向 A,同时没有任何其他对象指向 A 或 B。A 和 B 都无法从程序的任何地方访问,因此可以安全地被析构,但由于它们的引用计数为 1,因此仅靠引用计数算法无法释放它们。

>>> import gc
>>> gc.disable() # 禁用垃圾回收器
>>> class Track:
    def __init__(self):
        print("Initialized")
    def __del__(self):
        print("Destructed")

>>> A = Track()
Initialized
>>> B = Track()
Initialized
>>> A.other = B
>>> B.other = A
>>> del A; del B # 由于引用循环,对象尚未被析构
>>> gc.collect() # 触发回收
Destructed
Destructed
20

引用循环可以任意长。如果 A 指向 B 指向 C 指向……指向 Z,而 Z 又指向 A,那么从 A 到 Z 都不会被回收,直到垃圾回收阶段:

>>> objs = [Track() for _ in range(10)]
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
>>> for i in range(len(objs)-1):
... objs[i].other = objs[i + 1]
...
>>> objs[-1].other = objs[0] # 完成循环
>>> del objs # 现在没有任何对象引用 `objs`——但仍未被析构
>>> gc.collect()
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
20

5: 强制释放对象

在 Python 2 和 3 中,即使对象的引用计数不为 0,你也可以强制释放对象。

两个版本都使用 ctypes 模块来实现。

警告:这样做会使你的 Python 环境变得不稳定,容易在没有堆栈跟踪的情况下崩溃!使用这种方法还可能引入安全问题(虽然不太可能)。只有当你确定你永远不会再引用某个对象时,才应该释放它。

>>> import ctypes
>>> deallocated = 12345
>>> ctypes.pythonapi._Py_Dealloc(ctypes.py_object(deallocated))
1440518144

运行后,对刚刚释放的对象的任何引用都将导致 Python 产生未定义行为或崩溃——而且不会显示堆栈跟踪。垃圾回收器没有移除那个对象,可能有其原因……

如果你释放了 None,你会收到一条特殊的消息——在崩溃之前显示“致命的 Python 错误:释放 None”。

111.6 节:查看对象的引用计数

>>> import sys
>>> a = object()
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
>>> del b
>>> sys.getrefcount(a)
2

111.7: 不要等待垃圾回收来清理

垃圾回收会清理内存这一事实并不意味着你应该等待垃圾回收周期来清理。

特别是你不应该等待垃圾回收来关闭文件句柄、数据库连接和打开的网络连接。

例如:

在下面的代码中,你假设文件将在下一个垃圾回收周期中被关闭,如果 f 是对文件的最后一个引用。

>>> f = open("test.txt")
>>> del f

一种更明确的清理方式是调用 f.close()。你可以更优雅地实现这一点,即使用 with 语句,也称为上下文管理器:

>>> with open("test.txt") as f:
... pass
... # 对 `f` 进行一些操作
>>> # 现在 `f` 对象仍然存在,但它已经被关闭了

with 语句允许你将代码缩进到打开的文件下。这使得代码更明确,更容易看出文件保持打开的时间有多长。即使在 with 块中引发异常,它也会始终关闭文件。

8: 管理垃圾回收

有两种方法可以影响何时执行内存清理。一种是影响自动进程的执行频率,另一种是手动触发清理。

可以通过调整回收阈值来操纵垃圾回收器,这些阈值会影响回收器运行的频率。Python 使用基于代的内存管理系统。新对象被保存在最新的代中——generation0,并且每次在回收中存活下来后,对象都会被提升到更老的代中。

达到最后一代——generation2 后,它们不再被提升。

可以使用以下代码片段更改阈值:

import gc
gc.set_threshold(1000, 100, 10) # 这些值仅用于演示目的

第一个参数表示收集 generation0 的阈值。每当分配的内存数量超过释放的内存数量 1000 时,垃圾回收器就会被调用。

为了避免每次运行时都清理较老的代以优化进程,清理较老代的频率可以通过第二和第三个参数控制,这两个参数是可选的。如果在没有清理 generation1 的情况下处理了 generation0 100 次,则会处理 generation1。同样,只有在没有清理 generation2 的情况下清理了 generation1 10 次时,才会处理 generation2 中的对象。

在程序分配了大量小对象且没有释放它们时,手动设置阈值是有益的,这会导致垃圾回收器运行得太频繁(每次分配 generation0_threshold 个对象时)。尽管回收器本身速度很快,但当它处理大量对象时,可能会出现性能问题。不过,没有一种通用的策略可以用于选择阈值,这取决于具体用例。

可以使用以下代码片段手动触发回收:

import gc
gc.collect()

垃圾回收是根据分配和释放的次数自动触发的,而不是根据消耗或可用内存量。因此,在处理大对象时,可能会在自动清理被触发之前耗尽内存。这为手动调用垃圾回收器提供了一个很好的用例。

尽管这是可能的,但这并不是一个被鼓励的做法。避免内存泄漏才是最佳选择。然而,在大型项目中,检测内存泄漏可能是一项艰巨的任务,手动触发垃圾回收可以作为一种快速解决方案,直到进一步调试。

对于长期运行的程序,可以根据时间或事件触发垃圾回收。第一个例子是,一个 web 服务器在固定数量的请求后触发回收。对于后者,一个 web 服务器在接收到某种类型的请求时触发垃圾回收。

相关推荐

python创建文件夹,轻松搞定,喝咖啡去了

最近经常在录视频课程,一个课程下面往往有许多小课,需要分多个文件夹来放视频、PPT和案例,这下可好了,一个一个手工创建,手酸了都做不完。别急,来段PYTHON代码,轻松搞定,喝咖啡去了!import...

如何编写第一个Python程序_pycharm写第一个python程序

一、第一个python程序[掌握]python:python解释器,将python代码解释成计算机认识的语言pycharm:IDE(集成开发环境),写代码的一个软件,集成了写代码,...

Python文件怎么打包为exe程序?_python3.8打包成exe文件

PyInstaller是一个Python应用程序打包工具,它可以将Python程序打包为单个独立可执行文件。要使用PyInstaller打包Python程序,需要在命令行中使用py...

官方的Python环境_python环境版本

Python是一种解释型编程开发语言,根据Python语法编写出来的程序,需要经过Python解释器来进行执行。打开Python官网(https://www.python.org),找到下载页面,选择...

[编程基础] Python配置文件读取库ConfigParser总结

PythonConfigParser教程显示了如何使用ConfigParser在Python中使用配置文件。文章目录1介绍1.1PythonConfigParser读取文件1.2Python...

Python打包exe软件,用这个库真的很容易

初学Python的人会觉得开发一个exe软件非常复杂,其实不然,从.py到.exe文件的过程很简单。你甚至可以在一天之内用Python开发一个能正常运行的exe软件,因为Python有专门exe打包库...

2025 PyInstaller 打包说明(中文指南),python 打包成exe 都在这里

点赞标记,明天就能用上这几个技巧!linux运维、shell、python、网络爬虫、数据采集等定定做,请私信。。。PyInstaller打包说明(中文指南)下面按准备→基本使用→常用...

Python自动化办公应用学习笔记40—文件路径2

4.特殊路径操作用户主目录·获取当前用户的主目录路径非常常用:frompathlibimportPathhome_dir=Path.home()#返回当前用户主目录的Path对象...

Python内置tempfile模块: 生成临时文件和目录详解

1.引言在Python开发中,临时文件和目录的创建和管理是一个常见的需求。Python提供了内置模块tempfile,用于生成临时文件和目录。本文将详细介绍tempfile模块的使用方法、原理及相关...

python代码实现读取文件并生成韦恩图

00、背景今天战略解码,有同学用韦恩图展示各个产品线的占比,效果不错。韦恩图(Venndiagram),是在集合论数学分支中,在不太严格的意义下用以表示集合的一种图解。它们用于展示在不同的事物群组之...

Python技术解放双手,一键搞定海量文件重命名,一周工作量秒搞定

摘要:想象一下,周五傍晚,办公室的同事们纷纷准备享受周末,而你,面对着堆积如山的文件,需要将它们的文件名从美国日期格式改为欧洲日期格式,这似乎注定了你将与加班为伍。但别担心,Python自动化办公来...

Python路径操作的一些基础方法_python路径文件

带你走进@机器人时代Discover点击上面蓝色文字,关注我们Python自动化操作文件避开不了路径操作方法,今天我们来学习一下路径操作的一些基础。Pathlib库模块提供的路径操作包括路径的...

Python爬取下载m3u8加密视频,原来这么简单

1.前言爬取视频的时候发现,现在的视频都是经过加密(m3u8),不再是mp4或者avi链接直接在网页显示,都是经过加密形成ts文件分段进行播放。今天就教大家如果通过python爬取下载m3u8加密视频...

探秘 shutil:Python 高级文件操作的得力助手

在Python的标准库中,shutil模块犹如一位技艺精湛的工匠,为我们处理文件和目录提供了一系列高级操作功能。无论是文件的复制、移动、删除,还是归档与解压缩,shutil都能以简洁高效的方式完成...

怎么把 Python + Flet 开发的程序,打包为 exe ?这个方法很简单!

前面用Python+Flet开发的“我的计算器v3”,怎么打包为exe文件呢?这样才能分发给他人,直接“双击”运行使用啊!今天我给大家分享一个简单的、可用的,把Flet开发的程序打包为...