ITPub博客

首页 > 数据库 > 数据库开发技术 > Python效率秘籍:掌握这8个内存优化技巧,让你的代码飞起来!!!

Python效率秘籍:掌握这8个内存优化技巧,让你的代码飞起来!!!

原创 数据库开发技术 作者:jackzhang 时间:2024-05-14 06:30:00 0 删除 编辑

当你的项目日益庞大时,高效地管理计算资源变得不可避免。不幸的是,与C或C++等底层语言相比,Python在内存效率方面似乎并不够高。

那你是不是想着干脆换一门编程语言呢?Of course not!❌

事实上,有许多方法可以显著优化Python程序的内存使用,从优秀的模块和工具到高级的数据结构和算法。

本文将重点关注Python的内置机制,并介绍8种基础但非常有效的内存优化技巧。掌握这些技巧可以显著提升你的Python编程技能。

OK,话不多说,我们直接上干货👇👇👇!请抓紧上车,并系好安全带,拉好扶手💪💪💪,我们即刻发车🚗🚗🚗

1. 使用生成器

生成器不会一次性将加载所有元素到内存中,而是按需生成元素。这在处理大数据时可以显著减少内存消耗。这种特性叫做“惰性求值”,所以有人也将生成器成为列表的“惰性求值”版本。

与列表推导(List Comprehensions)相比,生成器的主要优势在于它不会一次性将所有元素加载到内存中。而是按需一次生成一个元素,并在迭代中使用完该元素后立即释放相关内存。这种特性(惰性求值)使得生成器在处理大规模数据时内存效率更高。

生成器的实现通常有两种方式:含有 yield 关键字的函数和生成器表达式(Generator Expressions)(将列表推导式的方括号换成圆括号即可)。这两种形式都有助于提高内存使用效率,特别是在处理大数据集时。

import sys

# 生成器函数
def number_generator():
    for number in range(100):
        yield number

# 生成器表达式
gen_exp_numbers = (number for number in range(100))

# 列表推导式
lst_comp_numbers = [number for number in range(100)]

# for 循环
for_loop_numbers = []
for number in range(100):
    for_loop_numbers.append(number)
    
print(f'Generator function: {sys.getsizeof(number_generator)}')
print(f'Generator expression: {sys.getsizeof(gen_exp_numbers)}')
print(f'List comprehension: {sys.getsizeof(lst_comp_numbers)}')
print(f'For loop: {sys.getsizeof(for_loop_numbers)}')

上面的代码中,我们采用4种方式生成同一个序列,然后分别计算生成序列所占的内存大小,可以看出,占用内存——生成器函数<生成器表达式<列表推导式=for循环。而且随着生成的序列增大,对于生成器函数和生成器表达式,其占用内存空间不便,但是后两种会随之增大。比如,我们将数值换成100000:

总之,生成器表达式可以有效减少在处理大型数据集时的内存占用,特别适用于读取大文件、处理大数据流或在数据分析中进行初步筛选的场景。

2. 尽可能使用内置数据类型

Python 的内置数据类型,如整数(int)、浮点数(float)、字符串(str)等,是在 Python 的底层(C语言层面)实现的。这些类型针对性能和内存使用进行了优化。相比于自定义类或者使用其他复杂数据结构,内置数据类型在处理基本数据操作时通常更高效。

内置类型优势:

  1. 速度: 内置类型的操作(如数学运算、字符串操作)在 C 语言层面执行,速度快。
  2. 内存: 由于优化过的内存分配策略,内置类型使用的内存通常比自定义数据结构更少。
  3. 功能丰富: Python的内置类型自带许多实用的方法和属性,用于数据处理很方便。

项目案例:

假设我们正在开发一个财务软件,需要处理大量的货币交易数据。每笔交易记录了金额、时间和交易方的信息。考虑到性能和内存效率,我们应该如何选择数据类型?

错误示例: 使用字符串或自定义类存储金额。

class Transaction:
    def __init__(self, amount, timestamp, parties):
        self.amount = str(amount)  # 将金额存储为字符串
        self.timestamp = timestamp
        self.parties = parties

正确示例: 使用内置的 float 或 int 存储金额。

class Transaction:
    def __init__(self, amount, timestamp, parties):
        self.amount = float(amount)  # 使用内置类型 float 存储金额
        self.timestamp = timestamp
        self.parties = parties

在这个例子中,使用 float 来存储金额,可以利用 Python 的内置数值处理优势,比如快速的数学运算和减少内存占用。同时,内置类型的数值在进行比较和算术运算时表现更为稳定和高效。

总结:使用 Python 的内置数据类型是优化内存和提高运算效率的一种简单而有效的策略。在设计程序时,应当尽量使用内置类型来存储和处理数据,尤其是在性能敏感的应用中。

3. 使用__slots__限制实例属性

每个类的实例默认都有一个__dict__属性,这是一个字典,用来存储实例的所有属性。虽然这种动态属性赋值的方式提供了极高的灵活性,但它也使得对象占用更多的内存。通过使用__slots__声明,可以在类中明确指定实例属性,这样Python会为实例使用一种更为紧凑的内部表示,从而减少内存使用。

如何使用 __slots__?

在类定义中,通过将 __slots__ 设置为一个字符串元组,列出所有允许的属性名称,可以防止动态创建其他属性,并优化内存使用。相当于创建了一个属性“白名单”,不在范围内的属性不允许新增。

案例

假设我们有一个用于表示网络中节点的类,这个类包含节点的ID和与之连接的节点列表。在大规模网络图中,可能会有成千上万个节点实例,因此内存效率至关重要。

不使用 __slots__ 的类定义:

# 不使用 __slots__  
class NodeWithoutSlots:
    def __init__(self, node_id, connections):
        self.node_id = node_id
        self.connections = connections

# 使用 __slots__        
class NodeWithSlots:
    # 定义属性“白名单”
    __slots__ = ('node', 'connections')
    def __init__(self, node_id, connections):
        self.node = node_id
        self.connections = connections
        
node = NodeWithoutSlots(111, [333, 444, 555])
node.address = 'Shanghai'
node_with_slots = NodeWithSlots(111, [333, 444, 555])

# 内存使用比较
memory_without_slots = sys.getsizeof(node) + sys.getsizeof(node.__dict__)
memory_with_slots = sys.getsizeof(node_with_slots)
print(f'Memory without slots: {memory_without_slots}')
print(f'Memory with slots: {memory_with_slots}')

print(f'node1.__dict__: {node.__dict__}')
print(f'node_with_slots.__dict__: {node_with_slots.__dict__}')

从上面的结果可以看出,相比,普通版本,使用__slots__占用内存明显更少。因为使用__slots__的属性并在底层并不通过字典存储,因此不用维护这个特殊的字典,所需内存自然更少。

在这个例子中,使用 __slots__ 明确地声明了实例将具有的属性,从而不再需要为每个实例创建一个__dict__。这不仅节省了内存,也提高了属性访问的速度。

总结:__slots__是一个高级的内存优化技巧,适用于需要创建大量实例且实例属性不会动态变化的情况。这种方法可以显著减少内存使用,并提高属性访问速度。

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

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

“

它实现了需求分页,因为不会立即从磁盘读取文件内容,因此最初根本不使用任何物理内存。只有在特定位置被访问后,才会以一种惰性方式从磁盘读取。

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

注意,是映射而不是加载整个文件,因此可以显著节省内存。

听起来是不是很复杂?幸运的是,Python 已经提供了一个实现了该技术的内置模块,所以我们可以不用考虑操作系统级别的实现方式的前提下轻松地利用它。

例如,下面这个示例演示了在 Python 中如何使用 mmap 进行文件处理的:

import mmap

with open('large_log_file.txt', 'r+b') as f:
    # 使用内存映射该文件,size=0表示整个文件
    with mmap.mmap(f.fileno(), 0) as mm:
        # 通过标准方法读取内容
        print(mm.read())
        # 通过切片符号读取内容
        part_content = mm[:10]
        print(part_content.decode('utf-8'))

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

5. 非必要不使用全局变量

全局变量自程序运行开始运行时就一直保留在内存中,直到整个程序结束时才会销毁,收回内存空间,因为它们在全局范围可见。

因此,如果全局变量保存了一个大型数据结构,它将在整个程序生命周期内占用更多的内存,可能导致潜在的内存使用效率低下问题。

所以,如果不是非必要的情况下,我们应该在 Python 代码中尽量减少全局变量的使用。

6. 利用逻辑运算符的短路计算原则

这一技巧似乎很微妙,但巧妙地使用它将极大地节省我们程序的内存使用。例如,在下面的实例中,根据两个函数返回的布尔值来获取最终结果:

result_a = computation_func_a()
result_b = computation_func_b()
result = result_a if result_a else result_b

上述代码可以正常运行,但实际上执行了两个内存使用效率低下的函数。

获取相同结果的一种性能更好的方法如下:

result =  computation_func_a() or computation_func_b()

由于逻辑运算符遵循短路求值规则,如果computation_func_b()为 True 时,以上代码中的computation_func_a()不会执行。这样就可以节省不必要的内存使用。

7. 选择正确的数据存储格式

在Python中,选择合适的数据类型和结构对于优化内存使用和提高程序性能至关重要。不同的数据类型和结构有不同的内存占用特性和性能表现,合理选择可以显著减少内存占用并提高处理速度。

选择更合适的数据类型:

对于数值数据,Python提供了多种数据类型,如整型int,浮点型float,以及通过标准库如numpy提供的更多具体类型,例如int32、int64、float32、float64等。在不需要很大范围或精度的情况下,选择较小的数据类型(如int32而非int64)可以减少内存使用。

使用更紧凑的数据结构:

例如,使用数组(array)代替列表(list)来存储大量的数值数据。数组在存储同一数据类型的多项数据时比列表的内存使用效率更高,因为数组为数据分配连续的内存空间,而列表则存储指向各个独立对象的指针。

案例:

假设我们需要存储一个包含百万级别整数的序列,我们可以比较使用列表和数组的内存效率。

import sys
import array

# 使用列表
big_list = list(range(1000000))
# 使用数组
big_array = array.array('i', range(1000000))

print(sys.getsizeof(big_list))   # 输出列表的总内存大小
# 8000056
print(sys.getsizeof(big_array))  # 输出数组的总内存大小
# 4091948

从结果可以看出,数组使用的内存明显少于列表(将近2倍的差距),因为数组直接在内存中存储数值数据,而不是对象引用。

总结:通过选择合适的数据类型和更紧凑的数据结构,可以优化程序的内存使用和性能。这种优化在处理大规模数据集、进行科学计算或任何性能敏感的应用中尤为重要。

8. 使用interning技术共享字符串

Interning 是一种用于优化内存使用的技术,主要应用于字符串。在Python中,interning 指的是重用相同的不可变对象,从而避免存储多个相同对象。这样,当多个变量引用相同的字符串时,它们实际上指向内存中的同一个位置,从而减少内存的使用。

字符串Interning的原理:

Python自动对小的和(或)在编译时遇到的字符串(长度不大于4096)进行 interning。例如,小的字符串和在源代码中硬编码的字符串。但是,对于在运行时通过某些操作(如拼接、用户输入等)生成的字符串(长度超过4096),Python可能不会自动进行interning。因此,4096是一个魔法数字,它是Python是否将两个字符串自动interning的阈值标准。

手动进行interning:可以使用sys.intern()函数来强制intern任何字符串。这在处理大量重复的字符串数据时特别有用,例如,在处理大量网络数据、日志文件或在NLP任务中。

案例:

import sys

a = 'Jack' * 1024
b = 'Jack' * 1024
c = 'hello' * 1000
d = 'hello' * 1000

print(a == b)  # True, 但是比较的是内容
print(a is b)  # True, 因为二者的长度都为 4096
print(c == d)  # True
print(c is d)  # False, 因为长度大于 4096
# 手动 Interning
print(sys.intern(c) is sys.intern(d))   # True,同一个对象

在这个例子中,虽然a和b包含相同的字符串,并且它们是相同的对象(因为他们的长度都不大于4096)。而c和d则不是是同一个对象,要想它们指向同一个对象,需要强制使用sys.intern()函数进行 interning。

总结:使用字符串interning可以在处理大量重复字符串时节省大量内存。这在处理文本数据集、日志数据或任何包含大量重复字符串的场景中非常有用。

结论

好啦!今天的分享就到此结束啦!感谢你的阅读,希望每一个阅读过后的你都有所收获,在成为Python高手的路上愈行愈远。Good Luck!

今日赠语:欲穷千里目,更上一层楼!


推荐阅读👇👇👇

Python——函数定义中的“/”和“*”到底有什么用?一次性讲清楚~~

Python魔法符号:20秒搞懂@在装饰器中的作用原理

掌握Pandas数据分段:精确解读cut与qcut函数的神秘力量

成为Python高手:掌握这5个核心技能,迈入编程精英行列!

Python魔法参数:深入解析*args和**kwargs的强大用途

- END -

“

🌟 喜欢这篇文章吗?给个赞支持一下吧!🌟

如果你发现今天的内容对你有帮助,不妨点个赞并关注我,以便不错过更多关于Python、数据分析和机器学习的深入分析和最新趋势。我们一起探索数据的力量,解锁AI的潜能。您的每一次互动都是对我的极大鼓励,也是我们共同成长的动力!

“

👇 关注并开启星标提醒,让知识与技能的火花不断在你的职业道路上燃烧吧!

 

原文作者:数据派探险家;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2017-12-19

  • 博文量
    119
  • 访问量
    55941