Python 内存管理(引用计数)


python引用

1. 在python种通常我们不说把对象分配给变量,而是把变量分配给对象,也就是说,我们不说将对象赋值给变量,而认为我们将变量标识赋给对象。

>>> a = [1,2,3,4]
>>> b = a
>>> a is b
True
>>> id(a), id(b)
(140391868520256, 140391868520256)
>>> 

可以注意到,变量a和b指向同一个对象,它们所指向的对象在内存中的位置是相同的。当人们复制列表或字典时,就复制了对象列表的引用同,如果改变引用的值,则修改了原始的值。
2. 对引用式变量来说,说把变量分配给对象更合理,反过来说就有问题。因为,对象在赋值之前就创建了。

>>> class a:
...     def __init__(self):
...         pass
... 
>>> a
<class '__main__.a'>
>>> b = a()
>>> b
<__main__.a object at 0x7faf875f0880>
>>> 

Python中的赋值语句,应该始终先读右边。对象在右边创建或获取,在此之后左边的变量才会绑定到对象上,这就像为对象贴上标注。

3. 循环引用

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

向a中添加了b,又向b中添加了a,在打印的时,python会试图将其打印出来。由于引用循环,对象可能直接或间接的引用了本身,所以循环中的每个对象的引用计数都不是0。

4. 引用的问题

>>> t = (1,2,[3,4])
>>> t[2] += [5,6]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [3, 4, 5, 6])
>>> 

t[2]被改动了,但是也有异常抛出。让我们看看字节码

>>> dis.dis('t[2] += [5,6]')
  1        0 LOAD_NAME                0 (t)
              2 LOAD_CONST               0 (2)
              4 DUP_TOP_TWO
              6 BINARY_SUBSCR
              8 LOAD_CONST               1 (5)
             10 LOAD_CONST               2 (6)
             12 BUILD_LIST               2
             14 INPLACE_ADD
             16 ROT_THREE
             18 STORE_SUBSCR
             20 LOAD_CONST               3 (None)
             22 RETURN_VALUE
>>> 
  • LOAD_NAME(namei):将与 co_names[namei] 相关联的值推入栈顶。
  • LOAD_CONST(consti):将 co_consts[consti] 推入栈顶
  • DUP_TOP_TWO:复制堆栈顶部的两个引用,使它们保持相同的顺序
  • BINARY_SUBSCR:实现 TOS = TOS1[TOS],(TOS(Top Of Stack,栈的顶端))。
  • BUILD_LIST(count):创建一个使用了来自栈的 count 个项的列表,并将结果列表推入栈顶。
  • INPLACE_ADD:就地实现 TOS = TOS1 + TOS
  • ROT_THREE:将第二个和第三个堆栈项向上提升一个位置,顶项移动到位置三
  • STORE_SUBSCR:实现 TOS1[TOS] = TOS2
  • RETURN_VALUE:返回 TOS 到函数的调用者。

可以看到,代码的执行过程:

  1. 将t[2]的值存入TOS(Top Of Stack,栈的顶端)。
  2. 计算TOS+=[5,6]。这一步能够完成,是因为TOS指向的是一个可变对象(也就是列表)。
  3. t[2]=TOS赋值。这一步失败,是因为s是不可变的元组。

5. is 与 ==

  • ==运算符比较两个对象的值(对象中保存的数据),而is比较对象的标识。
  • is运算符比==速度快,因为它不能重载,所以Python不用寻找并调用特殊方法,而是直接比较两个整数ID。而a==b是语法糖,等同于a.__eq__(b)。继承自object的__eq__方法,用于比较两个对象的ID,结果与is一样。但是多数内置类型使用更有意义的方式覆盖了__eq__方法,会考虑对象属性的值。

6. 深复制与浅复制

  1. 浅复制:python默认做浅复制,例如:复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法。python的copy模块提供的copy函数能对任意对象做浅复制。。

    >>> a = [1,2,3,4]
    >>> b = list(a)
    >>> b == a
    True
    >>> b is a
    False
    >>> 
    >>> import copy
    >>> a = [1,2,[3,4]]
    >>> b = copy.copy(a)
    >>> a == b
    True
    >>> a is b
    False
    >>> a[2][0] = 5
    >>> a == b
    True
    >>> a
    [1, 2, [5, 4]]
    >>> b
    [1, 2, [5, 4]]
    >>> 
    

    可以看到list(a)创建a的副本。副本与源列表相等。但是二者指代不同的对象。对列表和其他可变序列来说,还能使用简洁的l2=l1[:]语句创建副本。然而,构造方法或[:]做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)。

  2. 深复制:深复制(即副本不共享内部对象的引用),python的copy模块提供的deepcopy函数能对任意对象做深复制。

    >>> import copy
    >>> a = [1,2,[3,4]]
    >>> b = copy.deepcopy(a)
    >>> a == b
    True
    >>> a is b
    False
    >>> a[2][0] = 5
    >>> a
    [1, 2, [5, 4]]
    >>> b
    [1, 2, [3, 4]]
    >>> 
    

7. 不要使用可变类型作为参数的默认值

class box:
        def __init__(self, goods=[]):
            self.goods = goods

        def get_good(self):
            self.goods.pop(0)
    
        def post_good(self, good):
            self.goods.append(good)


b = box()
c = box()
b.post_good('apple')
print(c.goods)

结果

None
 1 1.5 3 5 6 6.5 7 ['apple']
Process finished with exit code 0

注意到c中列表值也同b一样改变了。这种问题很难发现。如示例所示,实例化box时,如果传入物品,会按预期运作。但是不为box指定goods的话,奇怪的事就发生了,这是因为self.goods变成了goods参数默认值的别名。出现这个问题的根源是,默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。上例改为此可避免以上问题的发生。

class box:
    def __init__(self, goods=None):
        if goods is None:
            goods = []
        self.goods = goods

    def get_good(self):
        self.goods.pop(0)

    def post_good(self, good):
        self.goods.append(good)

python引用计数

原理

  1. 在C/C++语言中,程序员负责动态分配和回收堆heap当中的内存。在C里,通过函数malloc()和free() 来完成。在C++里是操作 new 和 delete 来实现相同的功能。每个由malloc()分配的内存块,最终都要由free()退回到可用内存池里面去。而调用 free() 的时机非常重要,如果一个内存块忘了free() 则会导致内存泄漏,这块内存在程序结束前将无法重新使用。这叫做 内存泄漏 。而如果对同一内存块 free() 了以后,另外一个指针再次访问,则再次使用malloc()复用这块内存会导致冲突。这叫做野指针 。
  2. 内存泄露往往发生在一些并不常见的代码流程上面。比如一个函数申请了内存以后,做了些计算,然后释放内存块。现在一些对函数的修改可能增加对计算的测试并检测错误条件,然后过早的从函数返回了。这很容易忘记在退出前释放内存,特别是后期修改的代码。这种内存泄漏,一旦引入,通常很长时间都难以检测到,错误退出被调用的频度较低,而现代电脑又有非常巨大的虚拟内存,所以泄漏仅在长期运行或频繁调用泄漏函数时才会变得明显。因此,有必要避免内存泄漏,通过代码规范会策略来最小化此类错误。
  3. Python通过malloc()和 free()包含大量的内存分配和释放,同样需要避免内存泄漏和野指针。他选择的方法就是引用计数。其原理比较简单:每个对象都包含一个计数器,计数器的增减与对象引用的增减直接相关,当引用计数为0时,表示对象已经没有存在的意义了,对象就可以删除了。
  4. 另一个叫法是 自动垃圾回收 。(有时引用计数也被看作是垃圾回收策略,于是这里的"自动"用以区分两者)。自动垃圾回收的优点是用户不需要明确的调用free()。(另一个优点是改善速度或内存使用,然而这并不难)。缺点是对C,没有可移植的自动垃圾回收器,而引用计数则可以可移植的实现(只要malloc()和free()函数是可用的,这也是C标准担保的)。也许以后有一天会出现可移植的自动垃圾回收器,但在此前我们必须与引用计数一起工作。
  5. Python使用传统的引用计数实现,也提供了循环监测器,用以检测引用循环。这使得应用无需担心直接或间接的创建了循环引用,这是引用计数垃圾收集的一个弱点。引用循环是对象(可能直接)的引用了本身,所以循环中的每个对象的引用计数都不是0。典型的引用计数实现无法回收处于引用循环中的对象,或者被循环所引用的对象,哪怕没有循环以外的引用了。循环检测器能够检测垃圾回收循环并能回收它们。 gc 模块提供了一种运行该检测器的方式(collect()函数),以及多个配置接口和在运行时禁用该检测器的功能。

代码分析

typedef struct _typeobject PyTypeObject;

/* Define pointers to support a doubly-linked list of all live heap objects. */
#define _PyObject_HEAD_EXTRA            \
    struct _object *_ob_next;           \
    struct _object *_ob_prev;



/* Nothing is actually declared to be a PyObject, but every pointer to
 * a Python object can be cast to a PyObject*.  This is inheritance built
 * by hand.  Similarly every pointer to a variable-size Python object can,
 * in addition, be cast to PyVarObject*.
 */

typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt; 
    PyTypeObject *ob_type;
} PyObject;



typedef struct {
    PyObject ob_base;
    Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;

PyObject 有两个成员的结构体,引用计数、对象类型:ob_refcnt,ob_type;还有一个指针域,用于生成双向循环链表。Py_ssize_t 是一个所占字节数与 size_t 相同的有符号的整数类型(C99中没有定义ssize_t这种类型,某些编译器比如gcc扩展有该类型)。

由以上代码可知:

  1. 在引用计数中,最重要的是ob_refcnt,它记录了该对象被引用的次数。若此对象的引用计数器变为0,则代表此对象已经被废弃,python内存管理将自动回收该内存。
    每个对象维护一个ob_ref减1,用来记录该对象当前被引用的次数
  2. 每当新的引用指向该对象时,它的引用计数ob_refcnt加1
  3. 每当该对象的引用失效时ob_refcnt减1
  4. 一旦对象的引用计数为0,该对象可以被回收,对象占用的内存空间将被释放
  5. 它的缺点是需要额外的空间维护计数,这个问题是其次的
  6. 最主要的问题是它不能解决对象的循环引用

在python中可使用sys模块获取ob_refcnt的值

>>> import sys
>>> a = [1,2,3]
>>> b = a
>>> c = a
>>> sys.getrefcount(a)
4
>>> 

当使用某个引用计数作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用,因此,getrefcount()所得到的的结果,会比期望多1.

声明:Hello World|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - Python 内存管理(引用计数)


我的朋友,理论是灰色的,而生活之树是常青的!