|
|
|
|
公众号矩阵

编写高效内存Python代码的3个技巧

大多数时候,我们不需要优化Python中的内存使用情况。我们的程序太小而无法占用大量内存,或者我们正在将数据存储在程序外部的数据库中。无论如何,在某些情况下,我们必须在内存中保留过大的结构或大量的对象。因此,我希望举例说明可以减少程序内存使用量的做法。

作者:闻数起舞来源:今日头条|2021-02-23 10:48

介绍

大多数时候,我们不需要优化Python中的内存使用情况。我们的程序太小而无法占用大量内存,或者我们正在将数据存储在程序外部的数据库中。无论如何,在某些情况下,我们必须在内存中保留过大的结构或大量的对象。因此,我希望举例说明可以减少程序内存使用量的做法。

议程

  • 用__slots__限制类字段
  • Generator惰性加载
  • 用数组约束元素类型

用__slots__限制类字段

默认情况下,每当您在Python中创建对象时,即使在创建之后,您也能够将新字段添加到对象。

例如,假设我有一个名为Dog的类:

  1. class Dog:  
  2.     def __init__(self, name, age):  
  3.     self.name = name  
  4.         self.age = age 
  5.     def main():  
  6.     dog = Dog("James", 5)  
  7.         dog.breed = "Pitbull"  
  8.         print(dog.breed) 
  9.  
  10. main() 

尽管名称和年龄是我传递给构造函数的唯一字段,但是请注意,在创建dog之后,如何初始化一个名为繁殖的新字段。本质上,dog的字段存储在内部字典中,可通过.__ dict__访问,并且在初始化dog.breed时,将其值为“ Pitbull”的字段“ breed”添加到内部字典中。

  1. def main(): 
  2.     dog = Dog("James", 5) 
  3.     print(dog.__dict__)  
  4.     ''
  5.     output: {'name''James''age': 5} 
  6.     ''
  7.     dog.breed = "Pitbull" 
  8.     print(dog.__dict__) 
  9.     ''
  10.     output: {'name''James''age': 5, 'breed''Pitbull'
  11.     ''
  12. main() 

尽管这提供了灵活性,但大多数时候我们不需要在实例化之外添加新字段。为了节省内存占用量,我们可以设置Dog的__slots__属性来预定义其字段。

  1. class Dog: 
  2.     __slots__ = ("name""age"
  3.     def __init__(self, name, age): 
  4.         self.name = name 
  5.         self.age = age 

使用__slots__可以防止创建内部字典,从而使我们可以更紧凑地存储实例字段。但是,现在,我们不再能够即时创建新字段。

  1. def main(): 
  2.     dog = Dog("James", 5) 
  3.     dog.breed = "Pitbull"  
  4.     ''
  5.     output: AttributeError:'Dog' object has no attribute 'breed' 
  6.     ''
  7.    
  8. main() 

为了测试__slots__的内存使用情况,我创建了100,000个Dog和SlotDog对象。

  1. class Dog: 
  2.    
  3.   def __init__(self, name, age): 
  4.     self.name = name 
  5.     self.age = age 
  6. class SlotDog: 
  7.      
  8.    __slots__=("name""age"
  9.    def __init__(self, name, age): 
  10.      self.name = name 
  11.      self.age = age 

然后,我使用memory_profiler分解了创建100,000个对象后内存使用量的增加情况。创建Dog对象后,内存使用量增加了16.5 MiB,而SlotDog对象则增加了5.8 MiB,这表明使用__slots__有了很大的改进。您可以在GitHub上查看创建代码(https://github.com/Ramko9999/Medium-Memory-Efficient-Python/blob/main/slots_perf.py)。

在必须实例化具有预定字段的大量对象的情况下,使用__slots__将是有益的。

用Genertor惰性加载

当使用大文件或集合时,可能无法加载整个文件或将集合维护在内存中。如果我们可以一次处理多个文件或集合中的一个元素,那就太好了。

进入生成器!

让我们考虑一个例子。说我需要获取前n个奇数进行处理。自然地,我们可以创建一个列表并附加前n个奇数。

  1. def get_odds_list(n): 
  2.     odds = [] 
  3.     num = 1 
  4.     for i in range(n): 
  5.         odds.append(num) 
  6.         num += 2 
  7.     return odds 

但是,如果我们要处理前几百万的赔率,那么在内存中维护此列表将变得昂贵。更好的方法是在我们计算赔率时利用生成器迭代赔率,而不是计算和存储所有百万赔率。

这是上面的函数作为生成器的样子:

  1. def get_odds_generator(n): 
  2.     num = 1 
  3.     for i in range(n): 
  4.         yield num 
  5.         num += 2 
  6. odds = get_odds_generator(1000000) 

当我们初始化赔率时,尚未计算任何奇数。此刻的赔率只是一个迭代器,一个值序列。为了访问迭代器中的元素,我们必须在迭代器上调用next。顾名思义,next返回序列中的下一个值。

神奇之处在于yield关键字:它使函数成为生成器。本质上,当按赔率调用next时,生成器get_odds_generator将评估其代码,直到达到yield为止。然后,生成器将返回该值,并且其状态将冻结。然后,再次调用next时,生成器将从中断状态重新开始评估其代码。

  1. def get_odds_generator(n): 
  2.     num = 1 
  3.     for i in range(n): 
  4.         yield num 
  5.         num += 2 
  6.  
  7. odds = get_odds_generator(1000000) 
  8.  
  9. first = next(odds) 
  10. ''
  11. first = 1 
  12. Explanation: num is 1. We enter the for loop and immediately yield num 
  13. ''
  14.  
  15. second = next(odds) 
  16. ''
  17. second = 3  
  18. Explanation: num is 1. We add 2 to num, so its now 3.  
  19. We go the next iteration of the loop and yield num 
  20. ''
  21.  
  22. third = next(odds) 
  23. ''
  24. third = 5 
  25. Explanation: num is 3. We add 2 to num, so its now 5.  
  26. We go to the next iteration of the loop and yield num 
  27. ''

我们还可以按照以下方式浏览生成器生成的值。

  1. odds = get_odds_generator(1000000) 
  2. for odd in odds: 
  3.     pass //process the odd 

我们可以使用生成器来计算赔率。因此,我们不需要任何额外的内存来存储赔率。

使用生成器的一个警告是,我们将无法获取先前的元素或跳过元素的序列。如果您需要访问以前的元素,则最好直接使用列表。

用数组约束元素类型

尽管许多人认为列表在Python中是数组,但实际上存在一个单独的数组模块。列表和数组之间的核心区别在于,数组仅限于一种类型的元素。

我们可以使用多种类型的值在Python中创建列表。

  1. lst = [1.0, 1, {}, "hi"

数组不是这种情况。我们必须使用类型代码指定数组中元素的类型。类型代码是代表数组类型的字符:“ i”代表整数,“ b”代表字符,依此类推…

  1. from array import array 
  2. arr = array('i', []) # create an array of integers 
  3. arr.append(4) # append 4 to arr 
  4. arr.append('') # type error: integer is required not string 

数组与列表有很多共同的方法,例如append和pop(文档)。数组的主要优点是它们更加紧凑。为了测试这一点,我制作了一个包含一百万个整数的列表和数组,发现该列表的内存使用量增加了19.5 MiB,而数组仅增加了4 MiB。签出测量代码(代码)。

如果您有大量相同类型的数据序列,请考虑使用数组。

结论

过早的优化是万恶之源。

-唐纳德·埃文·克努斯

我已经展示了可以减少内存占用的多种实践,从使用__slots__到数组不等。仅在真正需要优化内存的最坏情况下考虑使用这些做法。在大多数情况下,不需要__slots__和数组。另一方面,标准API很可能已经使用了生成器,因此您可以放轻松。

【编辑推荐】

  1. 让我们一起揭秘代码效率真相
  2. Gartner:2021年全球低码开发技术市场将大幅增长23%
  3. 用Python和JS实现的Web SSH工具,真香!
  4. 天下武功,唯快不破!让你的软件开发提速就看这5点
  5. 运维和开发都掉入的Redis使用误区,真不是开玩笑……
【责任编辑:华轩 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

数据湖与数据仓库的分析实践攻略

数据湖与数据仓库的分析实践攻略

助力现代化数据管理:数据湖与数据仓库的分析实践攻略
共3章 | 创世达人

6人订阅学习

云原生架构实践

云原生架构实践

新技术引领移动互联网进入急速赛道
共3章 | KaliArch

33人订阅学习

数据中心和VPDN网络建设案例

数据中心和VPDN网络建设案例

漫画+案例
共20章 | 捷哥CCIE

220人订阅学习

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微