01
引言
在日常工作中,当大家的项目规模越来越大时,高效管理计算资源就成为了必然要求。遗憾的是,Python,尤其是与 C 或 C++等编程语言相比,内存效率似乎不高。但是从优秀的模块设计到先进的数据结构和算法,有很多方法可以显著优化 Python 程序的内存利用效率。
本文重点介绍三种有效的内存优化技巧,掌握这些技巧将大大提高大家的 Python 编程技能。闲话少说,我们直接开始吧!
02
类的定义中使用__slots__
Python 作为一种动态编程语言,在面向对象编程方面具有更大的灵活性。一个很好的例子是,Python 类可以在运行时添加额外的属性和方法。例如,下面的代码定义了一个名为 Author 的类。该类最初有两个属性 name 和 age。但我们可以很容易地再添加一个job属性,代码如下:
class Author:
def __init__(self, name, age):
self.name = name
self.age = age
me = Author('AI Way', 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('AI Way', 30)
me.job = 'Software Engineer'
print(me.job)
# AttributeError: 'Author' object has no attribute 'job'
如上面的代码所示,我们不能再在运行时添加job属性了。这是因为 __slots__ 白名单只定义了 name 和 age 两个有效属性。理论上,由于属性现在是固定的,Python 不需要为它维护一个字典。它只需为 __slots__ 中定义的属性分配必要的内存空间即可。
03
验证__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('AI Way', 30)
me_with_slots = AuthorWithSlots('AI Way', 30)
接着统计二者的内存占用,如下所示:
# Comparing memory usage
memory_without_slots = sys.getsizeof(me) + sys.getsizeof(me.__dict__)
# __slots__ classes don't have __dict__
memory_with_slots = sys.getsizeof(me_with_slots)
print(memory_without_slots, memory_with_slots)
# 152 48
print(me.__dict__)
# {'name': 'AI Way', 'age': 30}
print(me_with_slots.__dict__)
# AttributeError: 'AuthorWithSlots' object has no attribute '__dict__'
如上面的代码的输出所示,由于使用了__slots__,实例me_with_slots 没有__dict__字典。与需要保存额外字典的 me 实例相比,它有效地节省了内存资源开销。
04
使用生成器
生成器是 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
上述程序的结果证明,使用生成器可以大大节省内存使用量。
顺便说一下,如果我们把列表生成式中的方括号转换成小括号,它就会变成一个生成器表达式。这是在 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
05
谨慎选择数据类型
资深的 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 少。因此,如果在创建后不需要更改数据,我们应该选择元组而不是列表。
-
数组比列表更节省内存
Python中的数组array要求元素具有相同的数据类型(例如,所有整数或所有浮点数),但列表可以存储不同类型的对象,这就不可避免地需要更多内存。因此,如果列表中的元素类型相同,使用数组会更节省内存。
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
06
总结
本文重点介绍了三种常用的可以在编写代码的过程中优化内存的技巧,主要包括在类定义中使用 slots,使用生成器以及谨慎选择数据类型。学会这些技巧,可以极大提升大家的编码水平。
您学废了嘛?