介绍大多数时候,我们不需要优化Python中的内存使用。要么我们的程序太小,无法占用大量内存,要么我们将数据存储在程序之外的数据库中。无论如何,在某些情况下,我们必须在内存中保留过大的结构或大量对象。因此,我想举例说明如何减少程序的内存使用。议程用__slots__生成器来约束类字段延迟加载用数组来约束元素类型用__slots__来约束类字段默认情况下,无论何时在Python中创建对象,即使在创建之后,也可以向对象添加新字段。例如,假设我有一个名为Dog的类:classDog:def__init__(self,name,age):self.name=nameself.age=agedefmain():dog=Dog("James",5)dog.breed="Pitbull“print(dog.breed)main()虽然名称和年龄是我传递给构造函数的唯一字段,但请注意在创建狗之后,如何初始化一个名为breed的新字段。本质上,dog的字段存储在一个内部字典中,可通过.__dict__访问,并且当dog.breed被初始化时,一个值为“Pitbull”的字段“breed”被添加到内部字典中。defmain():dog=Dog("James",5)print(dog.__dict__)'''输出:{'name':'James','age':5}'''dog.breed="Pitbull"print(dog.__dict__)'''output:{'name':'James','age':5,'breed':'Pitbull'}'''main()虽然这提供了灵活性,但大多数时候我们不'不需要在实例化之外添加新字段。为了节省内存使用,我们可以设置Dog的__slots__属性来预定义它的字段。classDog:__slots__=("name","age")def__init__(self,name,age):self.name=nameself.age=age使用__slots__防止创建内部字典,允许我们更紧凑地存储实例字段.但是,现在,我们无法再即时创建新字段。defmain():dog=Dog("James",5)dog.breed="Pitbull"'''output:AttributeError:'Dog'objecthasnoattribute'breed''''main()为了测试内存使用情况__slots__,我创建了100,000个Dog和SlotDog对象。classDog:def__init__(self,name,age):self.name=nameself.age=ageclassSlotDog:__slots__=("name","age")def__init__(self,name,age):self.name=nameself.age=age然后我使用memory_profiler分解创建100,000个对象后内存使用量的增加。创建Dog对象后,内存使用量增加了16.5MiB,而SlotDog对象增加了5.8MiB,显示使用__slots__有很大改进。您可以在GitHub(https://github.com/Ramko9999/Medium-Memory-Efficient-Python/blob/main/slots_perf.py)上查看创建代码。在必须实例化大量具有预定字段的对象的情况下,使用__slots__将是有益的。使用Genertor延迟加载在处理大文件或集合时,可能无法加载整个文件或在内存中维护集合。如果我们可以一次处理多个文件或集合中的一个元素,那就太好了。输入发电机!让我们考虑一个例子。假设我需要处理前n个奇数。自然地,我们可以创建一个列表并附加前n个奇数。defget_odds_list(n):odds=[]num=1foriinrange(n):odds.append(num)num+=2returnodds但是如果我们处理前几百万的赔率,那么在内存中维护这个列表就变得很昂贵。更好的方法是在我们计算赔率时使用生成器迭代赔率,而不是计算和存储所有百万赔率。这就是上面的函数作为生成器的样子:defget_odds_generator(n):num=1foriinrange(n):yieldnumnum+=2odds=get_odds_generator(1000000)当我们初始化赔率时,还没有计算奇数。Odds目前只是一个迭代器,一个值序列。为了访问迭代器中的元素,我们必须在迭代器上调用next。顾名思义,next返回序列中的下一个值。神奇之处在于yield关键字:它使函数成为生成器。本质上,当next被调用时,生成器get_odds_generator将评估其代码,直到它达到yield。然后生成器将返回该值,其状态将冻结。然后,当next再次被调用时,生成器会从它停止的地方重新开始计算它的代码。defget_odds_generator(n):num=1foriinrange(n):yieldnumnum+=2odds=get_odds_generator(1000000)first=next(odds)'''first=1解释:numis1.Weentertheforloopandimmediatelyyieldnum'''second=next(odds)'''second=3解释:numis1.Weadd2tonum,soitsnow3.Wegothenexiterationoftheloopandyieldnum'''third=next(odds)'''third=5解释:numis3.Weadd2tonum,soitsnow5.Wegothenexiterationoftheloopandyieldnum'''我们也可以浏览生成器生成的值如下。odds=get_odds_generator(1000000)foroddinodds:pass//processtheodd我们可以使用生成器来计算赔率。因此,我们不需要任何额外的内存来存储赔率。使用生成器的一个警告是我们将无法获取先前的元素或跳过一系列元素。如果需要访问前面的元素,最好直接使用列表。用数组约束元素类型尽管很多人认为列表是Python中的数组,但实际上存在一个单独的数组模块。列表和数组之间的核心区别在于数组仅限于一种类型的元素。我们可以在Python中创建具有多种类型值的列表。这不是lst=[1.0,1,{},"hi"]数组的情况。我们必须使用类型代码指定数组中元素的类型。类型代码是代表数组类型的字符:'i'代表整数,'b'代表字符,等等...fromarrayimportarrayarr=array('i',[])#createanarrayofintegersarr.append(4)#append4toarrarr。append('')#typeerror:integerisrequirednotstring数组有很多与列表相同的方法,例如append和pop(文档)。数组的主要优点是它们更紧凑。为了对此进行测试,我制作了一个包含100万个整数的列表和数组,发现列表的内存使用量增加了19.5MiB,而数组仅增加了4MiB。检查测量代码(代码)。如果您有大量相同类型的数据系列,请考虑使用数组。结论过早的优化是万恶之源。-DonaldEvanKnuth我展示了各种可以减少内存占用的做法,从使用__slots__到数组。只有在真正需要优化内存的最坏情况下才考虑这些做法。在大多数情况下,不需要__slots__和数组。另一方面,标准API很可能已经使用了生成器,因此您可以放心。
