当您的项目规划越来越大时,高效办理内存资源就成为必然要求。遗憾的是,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 还将互调技巧应用于小整数。咱们还能够利用它来优化内存。