Python 缓冲池以及垃圾回收机制

Python 缓冲池以及垃圾回收机制

Python作为一种动态类型的语言,其对象和引用分离。这与曾经的面向过程语言有很大的区别。为了有效的释放内存,Python内置了垃圾回收的支持。该文说明Python语言的对象的内存使用、缓冲池机制以及垃圾回收机制。


1. Python对象的内存使用

要理解Python缓冲池机制,首先要理解Python对象在内存中的存储方式。赋值语句是语言最常见的功能了。但即使是最简单的赋值语句,也可以很有内涵。Python的赋值语句就很值得研究。

`a = 1`

整数1为一个对象。而a是一个引用。利用赋值语句,引用a指向对象1。Python是动态类型的语言(参考动态类型),对象与引用分离。Python像使用“筷子”那样,通过引用来接触和翻动真正的食物——对象。

为了探索对象在内存的存储,我们可以求助于Python的内置函数id()。它用于返回该对象的内存地址。

a = 1
print(id(a))
print(hex(id(a)))

在我的计算机上,它们返回的是:

11246696
'0xab9c68'

分别为内存地址的十进制和十六进制表示。 在Python中,整数和短小的字符,Python都会缓存这些对象,以便重复使用。当我们创建多个等于1的引用时,实际上是让所有这些引用指向同一个对象。

a = 1
b = 1

print(id(a))
print(id(b))

上面程序返回:

11246696
11246696

可见a和b实际上是指向同一个对象的两个引用。

为了检验两个引用指向同一个对象,我们可以用is关键字。is用于判断两个引用所指的对象是否相同。

# True
a = 1
b = 1
print(a is b)

# True
a = "good"
b = "good"
print(a is b)

# False
a = "very good morning"
b = "very good morning"
print(a is b)

# False
a = []
b = []
print(a is b)

上面的注释为相应的运行结果。可以看到,由于Python缓存了整数和短字符串,因此每个对象只存有一份。比如,所有整数1的引用都指向同一对象。即使使用赋值语句,也只是创造了新的引用,而不是对象本身。长的字符串和其它对象可以有多个相同的对象,可以使用赋值语句创建出新的对象。

在Python中,每个对象都有存有指向该对象的引用总数,即引用计数(reference count)。我们可以使用sys包中的getrefcount(),来查看某个对象的引用计数。需要注意的是,当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用。因此,getrefcount()所得到的结果,会比期望的多1。

from sys import getrefcount

a = [1, 2, 3]
print(getrefcount(a))

b = a
print(getrefcount(b))

由于上述原因,两个getrefcount将返回2和3,而不是期望的1和2。

2. Python缓冲池初探

2.1 首先说下自己对编程语言中为什么要设置缓冲池(i.e.缓冲池的作用)

1.降低’常用’对象频繁创建撤销频率

2.减少内存使用(i.e.降低内存占用)

2.2 再说下哪些类型能使用缓冲池

1.可变对象(对象是python对数据的抽象)肯定没戏,因为随时可能被修改,因而对其缓存是没有意义的。
2.不可变对象
缓冲池大小受内存制约可定是有一定范围的(缓存范围)
可以动态的进出池来实现对有限缓冲内存的有效合理利用

2.3 不可变对象使用缓冲池

在Python中,字符串和整型对象都是不可变的(immutable)类型,因此Python会很高效地缓存它们。这样的处理机制能提升Python的性能。因此,我们看到下面示例中str1和str2也都是指向同一块内存地址:

>>> str1, str2 = 'a', 'a'  
>>> print str1 is str2  
True  
>>> id(str1), id(str2)  
(22171104, 22171104)

那么,是不是所有的整型和字符串Python都会帮我们缓存呢?答案是No。正如我们学习英语的时候并不需要记忆所有的单词,而是只选取了一些高频词汇。Python也采取这样的策略。我们会在下面的示例中看到并不是所有的整型和字符串Python都会帮我们缓存:

>>> num5, num6 = 300, 300  
>>> print num5 is num6  
False  
>>> id(num5), id(num6)  
(24972028, 24971416)  
>>> str3, str4 = 'abcd', 'abcd'  
>>> print str3 is str4  
False  
>>> id(str3), id(str4)  
(29674688, 29674656)

Python能够帮我们缓存多少呢?Python2.3简单整数缓存范围是(-1,100),Python2.5.4以后简单整数缓存范围至少是[-5,256]。所有的短字符也都在缓存区。

>>> tuple1, tuple2 = (1, ), (1, )  
>>> print tuple1 is tuple2  
False  
>>> id(tuple1), id(tuple2)  
(24899856, 24756080)  
>>> tuple3, tuple4 = (1, 2), (1, 2)  
>>> print tuple3 is tuple4  
False  
>>> id(tuple3), id(tuple4)  
(29705880, 24881752)

(PS:tuple1和tuple2在定义时声明为(1, ),这是因为在Python中符号()不仅仅是元组,它被重载为分组操作符,由圆括号()包裹的单一元素首先被当做分组操作,而不是元组)

我们没有看到元组表现出缓存机制。这是为什么?元组不适用于实现缓冲池机制(因为tuple是复合对象),元组虽然是不可变对象,但是其元素可以是可变对象,即元组是不可变对象其元素不可变但其元素的元素可能变化。例如:

l = [1, 2, 3]  
t = (1, 2, l)  
print(t, id(t), id(t[2]))  
t[2].append('变')  
print(t, id(t), id(t[2]))  
''''' 
输出: 
    (1, 2, [1, 2, 3])       1997111491608 1997111532552 
    (1, 2, [1, 2, 3, '变']) 1997111491608 1997111532552 
'''  

可以看到,虽然元组是不可变类型,即其元不可变(赋值,修改),但是其元素的元素可能改变,因而缓存也是没有意义的。

3. Python对象引用对象

Python的一个容器对象(container),比如表、词典等,可以包含多个对象。实际上,容器对象中包含的并不是元素对象本身,是指向各个元素对象的引用。

我们也可以自定义一个对象,并引用其它对象:

class from_obj(object):
  def __init__(self, to_obj):
    self.to_obj = to_obj

b = [1,2,3]
a = from_obj(b)
print(id(a.to_obj))
print(id(b))

可以看到,a引用了对象b。对象引用对象,是Python最基本的构成方式。即使是a = 1这一赋值方式,实际上是让词典的一个键值”a”的元素引用整数对象1。该词典对象用于记录所有的全局引用。该词典引用了整数对象1。我们可以通过内置函数globals()来查看该词典。


你可能感兴趣的:(Python 缓冲池以及垃圾回收机制)