Python内存机制简介

Posted 程序员的自我修养

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python内存机制简介相关的知识,希望对你有一定的参考价值。

1:

变量不是盒子,应该把变量视作便利贴。变量只不过是标注,所以无法阻止为对象贴上多个标注。标注就是别名:

>>> a = [1, 2, 3]
>>> b = a
>>> a.append(4)
>>> b
[1, 2, 3, 4]

 

下面的代码中,lewis 和 charles 是别名,即两个变量绑定同一个对象。而 alex 不是 charles 的别名,因为二者绑定的是不同的对象。alex 和 charles 绑定的对象具有相同的值(== 比较的就是值),但是它们的标识不同。

>>> charles = {name: Charles L. Dodgson, born: 1832}
>>> lewis = charles
>>> lewis is charles
True
>>> id(charles), id(lewis)
(140334566757552, 140334566757552)
>>> lewis[balance] = 950
>>> charles
{born: 1832, balance: 950, name: Charles L. Dodgson}
>>> alex = {name: Charles L. Dodgson, born: 1832, balance: 950}
>>> alex == charles
True
>>> alex is not charles
True

 

2:

每个变量都有标识、类型和值。对象一旦创建,它的标识绝不会变;可以把标识理解为对象在内存中的地址。

is 运算符比较两个对象的标识;id() 函数返回对象标识的整数表示。== 运算符比较两个对象的值(对象中保存的数据)。is 运算符比 == 速度快,因为它不能重载,所以 Python 不用寻找并调用特殊方法,而是直接比较两个整数

 

3:

元组与多数 Python 集合(列表、字典、集合等)一样,保存的是对象的引用。如果引用的元素是可变的,即便元组本身不可变,元素依然可变。也就是说,元组的不可变性其实是指 tuple 数据结构的物理内容(即保存的引用)不可变,与引用的对象无关。

而 str、bytes 和 array.array 等单一类型序列是扁平的,它们保存的不是引用,而是在连续的内存中保存数据本身(字符、字节和数字)。

 

4:

复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法。例如:

>>> l1 = [3, [55, 44], (7, 8, 9)]
>>> l2 = list(l1)
>>> l3 = l1
>>> l2
[3, [55, 44], (7, 8, 9)]
>>> l3
[3, [55, 44], (7, 8, 9)]
>>> l2 is l1
False
>>> l3 is l1
True

对列表和其他可变序列来说,还能使用简洁的 l2 = l1[:]语句创建副本。

构造方法或 [:] 做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)。如果所有元素都是不可变的,那么这样没有问题,还能节省内存。但是,如果有可变的元素,可能就会导致意想不到的问题:

>>> l1 = [3, [66, 55, 44], (7, 8, 9)]
>>> l2 = list(l1) 
>>> l1.append(100) 
>>> l1[1].remove(55) 
>>> print(l1:, l1)
(l1:, [3, [66, 44], (7, 8, 9), 100])
>>> print(l2:, l2)
(l2:, [3, [66, 44], (7, 8, 9)])
>>> l2[1] += [33, 22] 
>>> l2[2] += (10, 11) 
>>> print(l1:, l1)
(l1:, [3, [66, 44, 33, 22], (7, 8, 9), 100])
>>> print(l2:, l2)
(l2:, [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]) 

 

有时我们需要的是深复制(即副本不共享内部对象的引用)。copy 模块提供的 deepcopy 和 copy 函数能为任意对象做深复制和浅复制。

>>> class Bus:
...     def __init__(self, passengers=None):
...         if passengers is None:
...             self.passengers = []
...         else:
...             self.passengers = list(passengers)
...     def pick(self, name):
...         self.passengers.append(name)
...     def drop(self, name):
...         self.passengers.remove(name)
... 
>>> import copy
>>> bus1 = Bus([Alice, Bill, Claire, David])
>>> bus2 = copy.copy(bus1)
>>> bus3 = copy.deepcopy(bus1)
>>> bus4 = bus1
>>> 
>>> print id(bus1), id(bus2), id(bus3), id(bus4)
140334566771960 140334566290640 140334566290928 140334566771960

>>> bus1.drop(Bill)
>>> bus2.passengers
[Alice, Claire, David]

>>> bus3.passengers
[Alice, Bill, Claire, David]
>>> bus4.passengers
[Alice, Claire, David]
>>> 
>>> print id(bus1.passengers), id(bus2.passengers), id(bus3.passengers), id(bus4.passengers)
140334566290568 140334566290568 140334566768792 140334566290568 

 

5:

Python 中,函数内部的形参是实参的别名。这种方案的结果是,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象的标识(即不能把一个对象替换成另一个对象):

>>> def f(a, b):
...     a += b
...     return a
... 
>>> x = 1
>>> y = 2
>>> f(x, y)
3
>>> x, y
(1, 2)
>>> a = [1, 2]
>>> b = [3, 4]
>>> f(a, b)
[1, 2, 3, 4]
>>> a, b
([1, 2, 3, 4], [3, 4])
>>> t = (10, 20)
>>> u = (30, 40)
>>> f(t, u)
(10, 20, 30, 40)
>>> t, u
((10, 20), (30, 40))

对元组来说,+= 运算符创建一个新元组,

 

6:

应该避免使用可变的对象作为参数的默认值。比如下面的例子:

>>> class HauntedBus:
...     def __init__(self, passengers=[]):
...         self.passengers = passengers
...     def pick(self, name):
...         self.passengers.append(name)
...     def drop(self, name):
...         self.passengers.remove(name)
... 
>>> 
>>> bus1 = HauntedBus([Alice, Bill])
>>> bus1.pick(Charlie)
>>> bus1.drop(Alice)
>>> bus1.passengers
[Bill, Charlie]
>>> 
>>> bus2 = HauntedBus()
>>> bus2.pick(Carrie)
>>> bus2.passengers
[Carrie]
>>> 
>>> bus3 = HauntedBus()
>>> bus3.passengers
[Carrie]
>>> 
>>> bus3.pick(Dave)
>>> bus2.passengers
[Carrie, Dave]
>>> 
>>> bus2.passengers is bus3.passengers
True
>>> 
>>> bus1.passengers
[Bill, Charlie]
>>> HauntedBus.__init__.__defaults__
([Carrie, Dave],)

实例化 HauntedBus 时,如果传入乘客,会按预期运作。但是不为 HauntedBus 指定乘客的话,奇怪的事就发生了,没有指定初始乘客的 HauntedBus 实例会共享同一个乘客列表。self.passengers 变成了 passengers 参数默认值的别名。出现这个问题的根源是,默认值在定义函数时计算(通常在加载模块时),这样默认值变成了函数对象的属性。因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。

 

7:

del 语句删除名称,而不是对象。del 命令可能会导致对象被当作垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时。 重新绑定也可能会导致对象的引用数量归零,导致对象被销毁。

如果两个对象相互引用,当它们的引用只存在二者之间时,垃圾回收程序会判定它们都无法获取,进而把它们都销毁。

 

在 CPython 中,垃圾回收使用的主要算法是引用计数。实际上,每个对象都会统计有多少引用指向自己。当引用计数归零时,对象立即就被销毁:CPython 会在对象上调用__del__ 方法(如果定义了),然后释放分配给对象的内存。

 

为了演示对象生命结束时的情形,下面使用 weakref.finalize (python3)注册一个回调函数,在销毁对象时调用。

>>> import weakref
>>> s1 = {1, 2, 3}
>>> s2 = s1
>>> def bye():
...     print(Gone with the wind...)
... 
>>> ender = weakref.finalize(s1, bye)
>>> ender.alive
True
>>> 
>>> del s1
>>> ender.alive
True
>>> 
>>> s2 = spam
Gone with the wind...
>>> ender.alive
False

 

正是因为有引用,对象才会在内存中存在。当对象的引用数量归零后,垃圾回收程序会把对象销毁。但是,有时需要引用对象,而不让对象存在的时间超过所需时间。这经常用在缓存中。弱引用不会增加对象的引用数量。弱引用不会妨碍所指对象被当作垃圾回收。弱引用在缓存应用中很有用,

 

下例展示了如何使用 weakref.ref 实例获取所指对象。如果对象存在,调用弱引用可以获取对象;否则返回 None。

>>> import weakref
>>> a_set = {0, 1}
>>> wref = weakref.ref(a_set)
>>> wref
<weakref at 0x7fdb8cc1c368; to set at 0x7fdb8cc2bd00>
>>> 
>>> wref()
set([0, 1])
>>> 
>>> a_set = {2, 3, 4}
>>> wref()
set([0, 1])
>>> 
>>> wref() is None
False
>>> 
>>> wref() is None
True

上例是一个控制台会话,而Python 控制台会自动把 _ 变量绑定到结果不为 None 的表达式结果上。首次调用 wref() 返回的是被引用的对象{0, 1}, {0, 1} 会绑定给 _ 变量。接下来,a_set 不再指代 {0, 1} 集合,因此集合的引用数量减少了。但是 _ 变量仍然指代它。所以再次调用 wref() 依旧返回 {0, 1}。计算这个表达式时,{0, 1} 存在,因此 wref() 不是 None。但是,随后 _ 绑定到结果值 False。现在 {0, 1} 没有强引用了。因为 {0, 1} 对象不存在了,所以 wref() 返回 None

 

weakref 模块的文档(http://docs.python.org/3/library/weakref.html)指出,weakref.ref类其实是低层接口,供高级用途使用,多数程序最好使用 weakref 集合和 finalize。也就是说,应该使用 WeakKeyDictionary、WeakValueDictionary、WeakSet 和finalize(在内部使用弱引用),不要自己动手创建并处理 weakref.ref 实例。

WeakValueDictionary 类实现的是一种可变映射,里面的值是对象的弱引用。被引用的对象在程序中的其他地方被当作垃圾回收后,对应的键会自动从 WeakValueDictionary中删除。因此,WeakValueDictionary 经常用于缓存。如下例:

>>> class Cheese:
...     def __init__(self, kind):
...         self.kind = kind
...     def __repr__(self):
...         return Cheese(%r) % self.kind
... 
>>> import weakref
>>> stock = weakref.WeakValueDictionary()
>>> catalog = [Cheese(Red Leicester), Cheese(Tilsit), Cheese(Brie), Cheese(Parmesan)]
>>> 
>>> for cheese in catalog:
...     stock[cheese.kind] = cheese
... 
>>> sorted(stock.keys())
[Brie, Parmesan, Red Leicester, Tilsit]
>>> 
>>> del catalog
>>> sorted(stock.keys())
[Parmesan]
>>> 
>>> del cheese
>>> sorted(stock.keys())
[]

删除 catalog 之后,stock 中的大多数奶酪都不见了,这是 WeakValueDictionary的预期行为。为什么不是全部呢?这是因为for循环中的变量 cheese 是全局变量,除非显式删除,否则不会消失。

 

不是每个 Python 对象都可以作为弱引用的目标(或称所指对象)。基本的 list 和 dict实例不能作为所指对象,但是它们的子类可以轻松地解决这个问题:

>>> wref_to_a_list = weakref.ref([])    
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: cannot create weak reference to list object

class MyList(list):
    pass
a_list = MyList(range(10))
# a_list可以作为弱引用的目标
wref_to_a_list = weakref.ref(a_list) 

 

8:

如果所有 Python 对象都是不可变的,那么本章就没有存在的必要了。处理不可变的对象时,变量保存的是真正的对象还是共享对象的引用无关紧要。仅当对象可变时,对象标识才重要。

 

CPython 中的垃圾回收主要依靠引用计数,这容易实现,但是遇到引用循环容易泄露内存,因此 CPython 2.0实现了分代垃圾回收程序,它能把引用循环中不可获取的对象销毁。

以上是关于Python内存机制简介的主要内容,如果未能解决你的问题,请参考以下文章

14.VisualVM使用详解15.VisualVM堆查看器使用的内存不足19.class文件--文件结构--魔数20.文件结构--常量池21.文件结构访问标志(2个字节)22.类加载机制概(代码片段

Android 逆向类加载器 ClassLoader ( 类加载器源码简介 | BaseDexClassLoader | DexClassLoader | PathClassLoader )(代码片段

Python-内存管理

[Python3] 043 多线程 简介

python如何进行内存管理

Python基础之python代码程序内存回收机制