掘金 后端 ( ) • 2024-04-27 11:19

highlight: a11y-dark theme: channing-cyan

随着您的项目规模逐步扩大,对内存资源有效管理的需求也随之增长。遗憾地是,相较于像CC++这样的底层语言,Python在内存效率方面似乎略显不足。那么,是否现在就该转投其他编程语言的怀抱呢?答案是否。其实,从强大的模块和工具、到高效的数据结构和算法,Python提供了众多优化内存使用的途径。

本文将突出阐述Python的内置特性,并向您展示七种基础且高效的内存优化技巧。掌握了这些技巧之后,您的Python编程技术将会有显著提升。

1.在类定义中使用 __slots__

作为动态类型语言的Python,在面向对象编程(OOP)方面具有很大的灵活性。例如,下面这段代码初步定义了一个Author类,包含了属性nameage。您会发现,在类的实例已创建之后,仍旧可以轻松地添加额外的属性:

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

p = Author('Zhang san', 30)
p.job = 'Software Engineer'
print(me.job)  # 输出: Software Engineer

然而,这种灵活性的背面是在内存使用上的效率损失。正是因为Python的每个类实例都会有一个特殊的字典(__dict__)来存储实例变量,而内部基于哈希表的实现使这种字典结构在内存使用上效率偏低。

大多数情况下,我们不需要在运行时动态地改变类实例的属性。考虑到这一点,我们没有必要为每个实例维护一个字典(__dict__)。

为了解决这个问题,Python引入了__slots__这一神奇特性。通过这一属性,您可以事先声明类中哪些属性有效,如下所示:

class Author:
    __slots__ = ('name', 'age')  # 声明有效属性名

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

p = Author('Zhang san', 30)
p.job = 'Software Engineer'  # 尝试添加未声明属性时会报错
print(p.job)  # 抛出AttributeError异常: 'Author'对象没有'job'属性

正如代码示例所展现的,由于声明了__slots__,在运行时便不可再添加job属性。因此,Python不再为__slots__里未声明的属性维护一个字典,相反为其分配必要的内存即可。

为了更直观地说明这一优化策略的效果,让我们通过一个内存使用对比实例进行观察:

import sys

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

class AuthorWithSlots:
    __slots__ = ['name', 'age']  # 使用__slots__定义属性

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

# 实例化
p = Author('Zhang san', 30)
p_with_slots = AuthorWithSlots('Zhang san', 30)

# 内存使用对比
memory_without_slots = sys.getsizeof(p) + sys.getsizeof(me.__dict__)
memory_with_slots = sys.getsizeof(p_with_slots)  # 使用__slots__的类不含有__dict__

print(memory_without_slots, memory_with_slots)  # 输出内存占用对比
# 使用__slots__的实例确实节约了内存

随着Python实例使用__slots__优化后,不仅节省了内存,还提高了程序的整体性能。

2.使用生成器以节省内存

生成器是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

让我们比较一下生成器和普通列表在内存上的占用差异:

import sys

# 使用列表
numbers_list = [i for i in range(100)]
numbers_gen = (i for i in range(100))  # 使用生成器表达式

print(sys.getsizeof(numbers_gen))  # 输出生成器内存占用
# 112
print(sys.getsizeof(numbers_list))  # 输出列表内存占用
# 920

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

内存映射文件I/O是由操作系统级别提供的一种高效文件处理方式。简而言之,它利用当前进程的虚拟内存空间映射文件内容,而不是一次性将文件全部载入内存。这种映射方式而非完全加载的方法极大地节约了内存消耗。

Python为我们提供了一个简单使用内存映射文件I/O的模块,从而无需处理操作系统层面的复杂实现。以下是一个使用mmap模块处理文件的示例:

import mmap

# 打开文件
with open('test.txt', "r+b") as f:
    # 映射整个文件
    with mmap.mmap(f.fileno(), 0) as mm:
        # 使用文件方法读取内容
        print(mm.read())  # 输出映射内容
        # 使用切片语法读取部分内容
        snippet = mm[0:10]  # 获取前10个字符
        print(snippet.decode('utf-8'))

以上就是内存映射文件I/O技术的简介以及它如何能够帮助我们处理大型文件而无须在内存上支付昂贵代价。

4.尽可能减少全局变量的使用

全局变量在整个程序中都可见,一旦被创建便在内存中持续存在。

因此,当一个全局变量绑定到一个大型数据结构时,它将在程序的整个生命周期内占用内存空间,潜在地降低内存使用效率。

为了提升内存效率,我们应避免或减少在Python代码中使用全局变量。

5.通过逻辑操作符优化内存

这个技巧虽微妙,但能通过巧妙的应用显著降低内存消耗。

以下是一段基于两个功能函数返回值的简单代码示例:

# 逻辑操作符的使用
result_a = expensive_function_a()  # 第一个函数
result_b = expensive_function_b()  # 第二个函数
result = result_a or result_b  # 使用逻辑操作符简化

原先的代码执行了两个可能会消耗大量内存的函数。然而,更高效的做法如下:

# 逻辑操作符的简化应用
result = expensive_function1() or expensive_function2()  # 有效减少内存消耗

由于逻辑运算符的短路特性,如果expensive_function1()返回真值,代码将不会执行expensive_function2()。这在不影响结果的情况下省去了额外的内存使用。

6.谨慎地选择合适的数据类型

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

显然,在不需要修改数据的情况下,元组是比列表更为内存高效的选择。

  • 数组相对于列表更节省内存

数组类型要求所有元素采用同一数据类型,这在内存效率上超越了普通列表。

# 数组与列表的内存使用对比
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框架,如NumPyPandas,提供了高效的数据类型选项。

使用NumPy的数组可以在处理矩阵运算时提供优势,并成为数据科学家的首选。

7.应用字符串互存技术以节省内存

Python中的字符串互存技术在处理相同的字符串时,能够极大地优化内存使用。

# 探索字符串互存现象
>>> a = 'Y'*4096
>>> b = 'Y'*4096
>>> a is b
True

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

is运算符用于检查两个变量是否指向相同内存对象,区别于比较值等同性的==运算符。

而上述现象中,由于Python4096以下的字符串上应用了字符串互存,所以ab返回True,而cd由于超出了这个界限,代表了不同的内存对象。

# 显示应用字符串互存技术
>>> import sys
>>> c = sys.intern('Y'*4097)
>>> d = sys.intern('Y'*4097)
>>> c is d
True

除了字符串,Python还对小整数应用了类似的互存技术,这也可以用来提升内存效率。

结论

总体来看,深入了解和运用这些内存优化策略对于开发高效的Python程序至关重要。通过智能选择数据结构,例如优先使用内存占用更小的元组而不是列表,以及采用数组来存储同类型数据,可以明显降低程序的内存占用。此外,逻辑操作符的短路行为不仅提升了代码效率,也避免了不必要的内存消耗。理解Python的内存管理机制,特别是字符串和整数的互存技术,进一步揭示了在编程时可用于内存优化的深层次功能。正确应用这些优化手段将有助于开发出性能更强、资源占用更少的优质Python应用程序。

c085f2a8698743159dbf2cbbff86d9cf~noop.jpg

关注gzh不灵兔,Python学习不迷路,私信【_-nfyn-_】,可进wx交流群,进群暗号【人生苦短】~~~