Python 内存优化小技巧

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

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

1. 在类定义中使用 __slots__

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

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

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__

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

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__ 中定义的属性分配必要的内存空间即可。

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

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() 方法时就生成一个项,而不是一次性计算所有项。因此,在处理大型数据集时,它们非常节省内存。

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

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

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 中定义生成器的一种更简单的方法:

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 进行文件处理:

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. 利用逻辑操作符

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

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

result_a = expensive_function_a()
result_b = expensive_function_b()
result = result_a if result_a else result_b


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

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

result = expensive_function1() or expensive_function2()


由于逻辑运算符遵循短路评估规则,如果 expensive_function1()True,则不会执行上述代码中的 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


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

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

数组比 list 更节省内存

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. 对相同字符串应用字符串互文技术

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

>>> 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() 方法就很好用:

>>> import sys
>>> c = sys.intern('Y'*4097)
>>> d = sys.intern('Y'*4097)
>>> c is d
True


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

---------------------------END---------------------------

#学习资源推荐

零基础Python学习资源介绍

👉Python学习路线汇总👈
Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。(学习教程文末领取哈)
在这里插入图片描述

👉Python必备开发工具👈
在这里插入图片描述

温馨提示:篇幅有限,已打包文件夹,获取方式在:文末

👉Python学习视频600合集👈
观看零基础学习视频,看视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。
在这里插入图片描述

👉实战案例👈
光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
在这里插入图片描述

👉100道Python练习题👈
检查学习结果。
在这里插入图片描述
👉面试刷题👈
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

资料领取

上述这份完整版的Python全套学习资料已经上传CSDN官方,朋友们如果需要可以微信扫描下方CSDN官方认证二维码输入“领取资料” 即可领取。

  • 39
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值