Python 内存优化技巧

当您的项目规模越来越大时,高效管理内存资源就成为必然要求。遗憾的是,Python,尤其是与 C 或 C++ 等低级语言相比,似乎内存效率不够高。现在是否应该更换编程语言?当然不是。事实上,从优秀的模块和工具到先进的数据结构和算法,有很多方法可以显著优化 Python 程序的内存使用。

本文将重点介绍 Python 的内置机制,并介绍 7 种原始但有效的内存优化技巧。掌握这些技巧将大大提高你的 Python 编程能力。

1. 在类定义中使用 __slots__

Python 作为一种动态类型编程语言,在 OOP 方面有更大的灵活性。在运行时向 Python 类中添加额外的属性和方法就是一个很好的例子。

例如,下面的代码定义了一个名为 Author 的类。它最初有两个属性 name 和 age。但我们可以在稍后轻松地添加一个额外的属性:

python 复制代码
class Author:
    def __init__(self, name, age):
        self.name = name
        self.age = age


me = Author('Yang Zhou', 30)
me.job = 'Software Engineer'
print(me.job)
# Software Engineer

然而,任何硬币都有两面。这种灵活性会浪费更多内存。

因为 Python 中每个类的实例都会维护一个特殊的字典 (__dict__),用于存储实例变量。由于字典的底层是基于哈希表的实现,因此字典本身的内存效率很低,因此字典会消耗大量内存。

在大多数情况下,我们不需要在运行时更改实例的变量或方法,而且在类定义之后,__dict__ 也不会被更改。因此,我们最好不要维护 __dict__ 字典。

Python 为此提供了一个神奇的属性:__slots__

它通过指定类的所有有效属性的名称,起到白名单的作用:

python 复制代码
class Author:
    __slots__ = ('name', 'age')

    def __init__(self, name, age):
        self.name = name
        self.age = age


me = Author('Yang Zhou', 30)
me.job = 'Software Engineer'
print(me.job)
# AttributeError: 'Author' object has no attribute 'job'

就像上面的代码,我们不能再在运行时添加工作属性了。因为 __slots__ 白名单只定义了 nameage 两个有效属性。

从理论上讲,由于属性是固定的,Python 不需要为它维护一个字典。它只需为 __slots__ 中定义的属性分配必要的内存空间即可。

让我们写一个简单的比较程序来看看它是否真的可以这样工作:

python 复制代码
import sys


class Author:
    def __init__(self, name, age):
        self.name = name
        self.age = age


class AuthorWithSlots:
    __slots__ = ['name', 'age']

    def __init__(self, name, age):
        self.name = name
        self.age = age


# Creating instances
me = Author('Yang', 30)
me_with_slots = AuthorWithSlots('Yang', 30)

# Comparing memory usage
memory_without_slots = sys.getsizeof(me) + sys.getsizeof(me.__dict__)
memory_with_slots = sys.getsizeof(me_with_slots)  # __slots__ classes don't have __dict__

print(memory_without_slots, memory_with_slots)
# 152 48
print(me.__dict__)
# {'name': 'Yang', 'age': 30}
print(me_with_slots.__dict__)
# AttributeError: 'AuthorWithSlots' object has no attribute '__dict__'

如上面的代码所示,由于使用了 __slots__,实例 me_with_slots 没有__dict__ 字典。与需要保存额外字典的 me 实例相比,它有效地节省了内存资源。

2. 使用生成器

生成器是 Python 中列表的懒版本。它们的工作方式类似于元素生成工厂:每当调用 next() 方法时就生成一个项,而不是一次性计算所有项。因此,在处理大型数据集时,它们非常节省内存。

python 复制代码
def number_generator():
    for i in range(100):
        yield i

numbers = number_generator()
print(numbers)
# <generator object number_generator at 0x104a57e40>
print(next(numbers))
# 0
print(next(numbers))
# 1

上面的代码展示了一个编写和使用生成器的基本示例。关键字 yield 是生成器定义的核心。使用它意味着只有在调用 next() 方法时,才会产生项目 i

现在,让我们比较一下生成器和列表,看看哪一个更节省内存:

python 复制代码
import sys

numbers = []
for i in range(100):
    numbers.append(i)

def number_generator():
    for i in range(100):
        yield i

numbers_generator = number_generator()
print(sys.getsizeof(numbers_generator))
# 112
print(sys.getsizeof(numbers))
# 920

上述程序的结果证明,使用生成器可以大大节省内存使用量。

顺便提一下,如果我们把 list 理解的方括号转换成小括号,它就会变成一个生成器表达式。这是在 Python 中定义生成器的一种更简单的方法:

python 复制代码
import sys

numbers = [i for i in range(100)]
numbers_generator = (i for i in range(100))

print(sys.getsizeof(numbers_generator))
# 112
print(sys.getsizeof(numbers))
# 920

3. 利用内存映射文件支持大文件处理

内存映射文件 I/O,简称 mmap,是一种操作系统级优化。

维基百科:它实现了需求分页,因为文件内容不会立即从磁盘读取,最初根本不使用物理 RAM。从磁盘实际读取的操作是在访问特定位置后,以一种懒惰的方式进行的。

简单地说,当使用 mmap 技术对文件进行内存映射时,它会直接在当前进程的虚拟内存空间中创建文件的映射,而不是将整个文件加载到内存中。映射而不是加载整个文件可以节省大量内存。

看起来很复杂?幸运的是,Python 已经提供了使用这种技术的内置模块,因此我们可以轻松利用它,而无需考虑操作系统级的实现。

例如,在 Python 中如何使用 mmap 进行文件处理:

python 复制代码
import mmap


with open('test.txt', "r+b") as f:
    # memory-map the file, size 0 means whole file
    with mmap.mmap(f.fileno(), 0) as mm:
        # read content via standard file methods
        print(mm.read())
        # read content via slice notation
        snippet = mm[0:10]
        print(snippet.decode('utf-8'))

如上所述,Python 使内存映射文件 I/O 技术的使用变得非常方便。我们需要做的仅仅是应用 mmap.mmap() 方法,然后使用标准文件方法甚至切片符号来处理打开的对象。

4. 尽量少用全局变量

全局变量具有全局作用域,因此只要程序运行,全局变量就会一直保留在内存中。

因此,如果一个全局变量包含一个大型数据结构,它就会在整个程序生命周期中占用内存,从而可能导致内存使用效率低下。

我们应该在 Python 代码中尽量减少全局变量的使用。

5. 利用逻辑操作符

这个技巧看似微妙,但巧妙地使用它将极大地节省程序的内存使用量。

例如,下面是一个简单的代码片段,它根据两个函数返回的布尔值得到最终结果:

python 复制代码
result_a = expensive_function_a()
result_b = expensive_function_b()
result = result_a if result_a else result_b

上述代码可以正常运行,但它实际上执行了两个内存不足的函数。

获得相同结果的更聪明的方法如下:

python 复制代码
result = expensive_function1() or expensive_function2()

由于逻辑运算符遵循短路评估规则,如果 expensive_function1()True,则不会执行上述代码中的 expensive_function2()。这将节省不必要的内存使用。

6. 谨慎选择数据类型

资深的 Python 开发人员会谨慎而精确地选择数据类型。因为在某些情况下,使用一种数据类型比使用另一种数据类型更节省内存。

元组比列表更节省内存

鉴于元组是不可变的(创建后不能更改),它允许 Python 在内存分配方面进行优化。然而,列表是可变的,因此需要额外的空间来容纳潜在的修改。

python 复制代码
import sys

my_tuple = (1, 2, 3, 4, 5)
my_list = [1, 2, 3, 4, 5]

print(sys.getsizeof(my_tuple))
# 80
print(sys.getsizeof(my_list)) 
# 120

如上面的代码段所示,即使包含相同的元素,元组 my_tuple 使用的内存也比 list 少。

因此,如果在创建后不需要更改数据,我们应该首选元组而不是列表。

数组比 list 更节省内存

Python 中的数组要求元素具有相同的数据类型(例如,所有整数或所有浮点数),但列表可以存储不同类型的对象,这就不可避免地需要更多内存。

因此,如果列表的元素都是同一类型,使用数组会更节省内存:

python 复制代码
import sys
import array

my_list = [i for i in range(1000)]

my_array = array.array('i', [i for i in range(1000)])

print(sys.getsizeof(my_list))  
# 8856
print(sys.getsizeof(my_array)) 
# 4064

优秀的数据科学模块比内置数据类型更高效

Python 是数据科学的统治语言。有许多强大的第三方模块和工具提供了更多的数据类型,如 NumPy 和 Pandas。

如果我们只需要一个简单的一维数组,而不需要 NumPy 提供的广泛功能,那么 Python 的内置数组可能是一个不错的选择。

但如果需要进行复杂的矩阵操作,使用 NumPy 提供的数组可能是所有数据科学家的首选,也可能是最佳选择。

7. 对相同字符串应用字符串互文技术

下面的代码会让很多开发人员感到困惑:

python 复制代码
>>> a = 'Y'*4096
>>> b = 'Y'*4096
>>> a is b
True

>>> c = 'Y'*4097
>>> d = 'Y'*4097
>>> c is d
False

我们知道,is 运算符用于检查两个变量是否指向内存中的同一个对象。它与 == 运算符不同,后者用于比较两个对象是否具有相同的值。

那么,为什么 a is b 得到的是 True,而 c is d 得到的却是 False 呢?

如果有几个小字符串的值相同,Python 就会隐式地对它们进行内联,并引用内存中的同一个对象。

定义小字符串的神奇数字是 4096。因为 c 和 d 的长度都是 4097,所以它们在内存中是两个对象,而不是一个。不再有隐式字符串互调。因此,当执行 c 是 d 时,我们会得到一个 False。

字符串互调是一种优化内存使用的强大技术。如果我们想显式地进行字符串互调,sys.intern() 方法就很好用:

python 复制代码
>>> import sys
>>> c = sys.intern('Y'*4097)
>>> d = sys.intern('Y'*4097)
>>> c is d
True

顺便说一下,除了字符串互调,Python 还将互调技巧应用于小整数。我们还可以利用它来优化内存。

相关推荐
笨鸟先飞,勤能补拙9 小时前
AI 赋能网络安全:技术全景、成熟度评估与实战案例
人工智能·python·安全·web安全·网络安全·sqlite·github
长和信泰光伏储能10 小时前
京津冀光伏发电:绿色能源的未来之路
python·能源
浦信仿真大讲堂10 小时前
从重复操作到自动化闭环:如何让 CST 与 Python 真正协同起来
python·自动化·cst·仿真软件·达索软件
Gu Gu Study11 小时前
ScoutLoop开放域深度研究引擎(agent的初步设计想法)
人工智能·python
卷无止境11 小时前
写代码这件事,到底该讲究点什么?
后端·python
卷无止境11 小时前
循环复杂度到底在算什么,Python 代码怎么才能写得让人一看就懂
后端·python
lpfasd12311 小时前
MediaCrawler 项目深度分析
chrome·python·chrome devtools
Dxy123931021612 小时前
Python项目打包成EXE完整教程(PyInstaller实战避坑)
开发语言·python
bamb0012 小时前
一个项目带你入门AI应用开发01
python
05664613 小时前
Python康复训练——常用标准库
开发语言·python·学习