当前位置: 首页 > 科技观察

Python对象与内存管理机制

时间:2023-03-15 20:50:16 科技观察

Python是一种面向对象的编程语言。Python中一切皆对象,内存空间是为每个对象分配的。Python的内存管理机制主要包括引用计数、垃圾回收和内存池机制。本文简要介绍Python对象和内存管理机制。参数传递常见的参数传递包括值传递和引用传递。值传递是将参数的值复制,然后传递给新的变量,使原变量和新变量相互独立,互不影响。引用传递是指将参数的引用传递给一个新变量,使原变量和新变量指向同一个内存地址。其中一个变量值的任何变化都会相应地改变另一个变量。Python参数传递Python的参数传递是赋值传递(passbyassignment),或者叫做对象引用传递(passbyobjectreference)。传递参数时,新变量指向与原变量相同的对象。让我们首先看一下Python中可变和不可变数据类型赋值的示例。1、不可变数据类型整型(int)赋值:a=1print(id(a))b=aprint(id(b))a=a+1print(id(a))c=1print(id(c))执行结果:140722100085136140722100085136140722100085168140722100085136其中id()函数用于返回对象的内存地址。可以看出b和c都指向了同一个对象,a=a+1并没有将a的值增加1,而是重新创建并指向了一个新的对象,值为2。最后的结果是a指向新对象2,b指向1,值不变。2、可变数据类型以列表(list)为例:l1=[1,2,3]print(id(l1))#l2=l1print(id(l2))l1.append(4)print(id(l1))print(l1)print(l2)执行结果:193320277229619332027722961933202772296[1,2,3,4][1,2,3,4]l1和l2指向同一个对象,因为list是可变数据类型,所以l1.append(4)不会创建新列表,仍然指向同一个对象。由于l1和l2指向同一个对象,改变列表也会导致l2的值改变。对可变对象(列表、字典、集合等)的更改将影响指向该对象的所有变量。对于不可变对象(字符串、整数、元组等),指向该对象的所有变量的值始终相同,不会改变。Python中的'=='和'is'==和is是Python对象比较中常用的两种方法,==比较对象的值是否相等,is比较对象的标识(ID)是否为相等,是否相同一个对象是否指向同一个内存地址。a=1b=aprint(id(a))print(id(b))print(a==b)print(aisb)执行结果:140722100085136140722100085136TrueTruea和b的值相等,指向同一个对象。在实际应用中,常用==来比较两个变量的值是否相等。is运算符常用于检查变量是否为None:ifaisNone:print("aisNone")ifaisnotNone:print("aisnotNone")Python浅拷贝和深拷贝Pythonwas前面介绍过Assignment(对象的引用传递),那么Python如何解决函数传递后不影响原始数据的问题呢?Python提供了两种方法:浅拷贝和深拷贝。浅拷贝(copy):复制父对象,而不是对象内部的子对象。深拷贝(deepcopy):父对象及其子对象的完整拷贝。浅拷贝1、不可变数据类型下的浅拷贝不可变对象整型变量和元组:importcopya=1b=copy.copy(a)print(id(a))print(id(b))print(a==b)print(aisb)t1=(1,2,3)t2=tuple(t1)print(id(t1))print(id(t2))print(t1==t2)print(t1ist2)执行结果:5062207250622072TrueTrue5514538455145384TrueTrue不可变对象的拷贝与对象的引用传递相同,a和b指向同一个对象,修改其中一个变量的值不会影响另一个变量,并且会开辟一个新的空间。2.可变数据类型对可变对象列表进行浅拷贝:importcopyl1=[1,2,3]l2=list(l1)l3=copy.copy(l1)l4=l1[:]print(id(l1))print(id(l2))print(l1==l2)print(l1isl2)print(id(l3))print(id(l4))l1.append(4)print(id(l1))print(l1==l2)print(l1isl2)执行结果:4852090448523784TrueFalse485238484852103248520904FalseFalse可以看出,变量对象的浅拷贝会重新分配一块内存,创建一个新的对象,里面的元素是对变量对象的引用原始对象中的子对象。改变l1的值不会影响l2、l3、l4的值,它们指向不同的对象。上面的例子比较简单,这里是一个比较复杂的数据结构:importcopyl1=[[1,2],(4,5)]l2=copy.copy(l1)print(id(l1))print(id(l2))print(id(l1[0]))print(id(l2[0]))l1.append(6)print(l1)print(l2)l1[0].append(3)print(l1)Print(l2)执行结果:1918057951816191805794944826803289914962680328991496[[1,2],(4,5),6][[1,2],(4,5)][[1,2,3],(4,5),6][[1,2,3],(4,5)]l2是l??1的浅拷贝,它们指向不同的对象,因为浅拷贝中的元素是对原对象元素的引用,所以元素inl2指向与l1相同的列表和元组对象(l1[0]和l2[0]指向相同的地址)。l1.append(6)不会对l2产生任何影响,因为l2和l1作为一个整体是两个不共享内存地址的不同对象。l1[0].append(3)将元素3添加到l1中的第一个列表中,因为l2是l1的浅拷贝,l2中的第一个元素和l1中的第一个元素指向同一个列表,所以第一个列表中l2将相应地添加元素3。这里有一个小问题:如果在l1(l1[1]+=(7,8))中的元组中添加一个元素,会影响l2吗?这里我们知道了使用浅拷贝可能带来的副作用,要避免它不得不使用深拷贝。深拷贝深拷贝会完整的复制一个对象,重新分配一块内存,创建一个新的对象,通过创建新的子对象,递归地将原对象中的元素复制到新对象中。因此,新对象与原对象没有任何关联,即父对象及其子对象的完整副本。导入copyl1=[[1,2],(4,5)]l2=copy.deepcopy(l1)print(id(l1))print(id(l2))l1.append(6)print(l1)print(l2)l1[0].append(3)print(l1)print(l2)执行结果:30260883422803026088342472[[1,2],(4,5),6][[1,2],(4,5)][[1,2,3],(4,5),6][[1,2],(4,5)]可以看出l1的变化不影响l2,l1和l2是完全独立,没有任何联系。在进行深拷贝时,深拷贝deepcopy中会维护一个字典,记录被拷贝的对象及其ID。如果要复制的对象已经存在字典中,则直接从字典中返回。Python垃圾收集Python垃圾收集包括引用计数、标记清除和分代收集。引用计数是一种垃圾收集机制。对象将被垃圾收集。fromsysimportgetrefcountl1=[1,2,3]print(getrefcount(l1))#查看引用计数l2=l1print(getrefcount(l2))执行结果:23使用getrefcount()时,变量作为参数传入,有将再被引用一次。del语句删除对对象的引用。请参阅以下示例fromsysimportgetrefcountclassTestObjectA():def__init__(self):print("hello!!!")def__del__(self):print("bye!!!")a=TestObjectA()b=ac=aprint(getrefcount(c))delaprint(getrefcount(c))delbprint(getrefcount(c))delcprint("666")执行结果:hello!!!432bye!!!666__del__方法的作用是当对象被销毁时调用。其中dela删除了变量a,但是对象TestObjectA仍然存在,同时它也被b和c引用,所以不会被回收,当引用计数为0时才会被回收。在上面的例子中,删除a、b、c后,引用的对象被回收(打印“666”之前)。此外,重新分配还会删除对该对象的引用。如果在标记清除中存在循环引用,则引用计数方法无法回收,导致内存泄漏。先看下面这个例子:classTestObjectA(dict):def__init__(self):print("A:hello!!!")def__del__(self):print("A:bye!!!")classTestObjectB(字典):def__init__(self):print("B:hello!!!")def__del__(self):print("B:bye!!!")a=TestObjectA()b=TestObjectB()a['1']=bb['1']=adeladelbprint("666")执行结果:A:hello!!!B:hello!!!666A:bye!!!B:bye!!!以上代码存在循环引用,删除a和b后,其引用计数仍为1,仍大于0,不会被回收(打印“666”后)。去除标记可以解决循环引用的问题。从根对象(寄存器和程序栈上的引用)开始,遍历对象,标记遍历的对象(垃圾检测),然后清除内存中未标记的对象(垃圾收集)。在上面的示例中,a和b相互引用。如果与其他对象没有引用关系,则不遍历,不做标记,所以清空。对于分代回收,频繁的mark-clearing会影响Python的性能。对象很多,多次清理后依然存在。可以认为这样的对象不需要经常回收。也就是说,对象存在的时间越长,它就越有可能不是垃圾。回收的对象分为几代(共三代),每一代的时间间隔不同。新创建的对象是第0代。如果一个对象能够在第0代的垃圾收集过程中存活下来,它将被放入第1代,如果第1代中的对象在第1代的垃圾收集过程中存活下来,它们将进入第2代。gc模块会在以下三种情况下启动垃圾收集:调用gc.collect():当gc模块的计数器达到阈值时强制收集所有代。当程序退出时,gc模块函数:gc.enable():启用自动垃圾收集gc.disable():禁用自动垃圾收集gc.isenabled():如果启用自动收集则返回True。gc.collect(generation=2):如果不设置参数,则对所有代进行一次收集gc.set_threshold(threshold0[,threshold1[,threshold2]]):设置垃圾收集阈值gc.get_count():currentcollectioncount垃圾收集启动importgcprint(gc.get_threshold())输出的默认阈值:(700,10,10)700是垃圾收集启动的阈值。当对象分配数减去释放数的值大于700时,垃圾回收就会开始。每十次第0代垃圾回收将导致第1代垃圾回收;每10次第1代收集将产生第2代收集。它可以使用set_threshold()方法重置。Python内存管理机制:PymallocPymallocPython实现了内存池机制,使用Pymalloc对小块内存(小于等于256kb)进行申请和释放管理。当Python频繁创建和销毁一些小对象时,底层会反复调用malloc、free等函数进行内存分配。这不仅会引入较大的系统开销,还可能产生大量的内存碎片。内存池的概念是预先在内存中申请一定的内存空间。当有满足条件的内存请求时,首先从内存池中为该需求分配内存。如果预申请的内存已经用完,Pymallocallocator会再申请新的内存(不能超过内存池预设的最大容量)。在垃圾回收期间,回收的内存返回到内存池。这样做最大的好处就是可以减少内存碎片,提高效率。如果应用程序的内存需求大于pymalloc设置的阈值,那么解释器就会将请求交给底层的C函数(malloc/realloc/free等)去执行。Python内存池金字塔级别-1和-2:由操作系统操纵。Layer0:大内存,如果申请的内存大于256kb,使用malloc、free等函数分配和释放内存。Layer1和Layer2:由python接口函数Pymem_Malloc实现,如果申请的内存小于等于256kb,则使用这一层进行分配。第三层(顶层):用户直接操作python对象图片来源:https://www.c-sharpcorner.com/article/memory-management-in-python/总结本文主要介绍参数传递Python的浅拷贝、深拷贝、垃圾回收和内存池机制。Python中参数的传递既不是值传递也不是引用传递,而是赋值传递,或者说对象引用传递。注意可变对象和不可变对象之间的区别。比较运算符==比较对象之间的值是否相等,`is比较对象是否指向同一个内存地址。浅拷贝中的元素是对原始对象中子对象的引用。如果父对象中的元素是可变的,改变它的值也会影响复制的对象。深拷贝会递归复制原对象中的每一个子对象,即原对象的完整拷贝。Python垃圾回收包括三种类型:引用计数、标记清除和分代回收。您可以使用gc模块来配置垃圾收集。Python为了减少内存碎片,提高效率,使用Pymalloc来管理小于等于256kb的小内存。