python中引用计数增加_扩展Python模块系列(四)----引用计数问题的处理_weixin_39931923的博客-程序员宅基地

技术标签: python中引用计数增加  

承接上文,发现在使用Python C/C++ API扩展Python模块时,总要在各种各样的地方考虑到引用计数问题,稍不留神可能会导致扩展的模块存在内存泄漏。引用计数问题是C语言扩展Python模块最头疼的地方,需要由程序员对使用的每个C API都要充分了解,甚至要熟悉源码才能精确掌握什么时候引用计数加一,什么时候减一。

本文为翻译文章,我觉得对于源码中的引用计数讲解得比较清楚,所以就翻译为中文。http://edcjones.tripod.com/refcount.html#

Summary:

Python Object的结构体定义包含一个引用计数和对象类型:

#define PyObject_HEAD \

intob_refcnt; \struct _typeobject *ob_type;

typedefstruct_object {

PyObject_HEAD

} PyObject;

Python提供了两组与引用计数相关的宏定义【object.h】:

#define Py_INCREF(op) ( \_Py_CHECK_THREAD_SAVE \

_Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA \

((PyObject*)(op))->ob_refcnt++)

/*当引用计数为0时,会释放对象所占的内存*/

#define Py_DECREF(op) \

do{ \if(_Py_CHECK_THREAD_SAVE \

_Py_DEC_REFTOTAL _Py_REF_DEBUG_COMMA \--((PyObject*)(op))->ob_refcnt != 0) \

_Py_CHECK_REFCNT(op) \else\

_Py_Dealloc((PyObject*)(op)); \

}while (0)

另外一组考虑是对象为NULl的情况:

#define Py_XINCREF(op) do { if ((op) == NULL) ; else Py_INCREF(op); } while (0)

#define Py_XDECREF(op) do { if ((op) == NULL) ; else Py_DECREF(op); } while (0)

在Python中,没有谁能真正拥有一个对象,只拥有对象的引用。一个对象的reference count定义为该对象的引用者数量,对象的引用者当不再使用该对象时有责任主动调用Py_DECREF(),当reference count为0时,Python可能会delete这个对象。

每次调用Py_INCREF(),最终都应该对应调用Py_DECREF()。C语言中,每个malloc,必须最终调用free()。而现实很容易忘记free掉在堆上分配的内存,而且不使用工具的话也难以察觉内存泄漏问题,因为现代机器内存、虚拟内存都很充足,一般会在长时间运行的服务器程序上出现内存泄漏问题。

当一个指向Python Object的指针的引用计数加1,就是说这个对象被protected.

什么时候调用Py_INCREF()和Py_DECREF()

从 函数中返回一个Python Object

大部分Python 对象是通过Python C API提供的函数来创建的,一般形如 PyObject* Py_Something(arguments)创建一个Python 对象,然后返回给调用者。一般在Py_Something函数中对该Python对象调用了Py_INCREF(并不是所有的函数都会调用),而调用Py_Something的函数在使用其返回的Python对象时要牢记该对象引用计数已被加1,当不再需要该对象时需要调用Py_DECREF()。

voidMyCode(arguments) {

PyObject*pyo;

...

pyo= Py_Something(args);

MyCode函数调用了Py_Something,有责任处理pyo的引用计数,当MyCode使用完了pyo之后,必须要调用Py_DECREF(pyo)。

不过,如果MyCode需要返回pyo对象,比如:

PyObject*MyCode(arguments) {

PyObject*pyo;

...

pyo=Py_Something(args);

...returnpyo;

}

此时,MyCode不应该调用PY_DECREF(),在这种情况下,MyCode将pyo对象的引用计数责任传递了出去。

Note:如果一个函数返回的是None对象,C代码应该是这样:必须要增加None对象的引用计数。

Py_INCREF(Py_None);return Py_None;

到目前为止讨论了最常见的情况,即当调用Py_Something创建了一个引用,并将引用计数的责任传递给其调用者。在Python文档中,这被称为new reference。比如文档中有说明:

PyObject* PyList_New(intlen)

Return value: New reference.

Returns anew list of length len on success, or NULL on failure.

当一个引用被INCREF,通常称为这个引用被protected。

有时候,Python源码中不会调用Py_DECREF()。

PyObject *PyTuple_GetItem(register PyObject*op, register inti)

{if (!PyTuple_Check(op)) {

PyErr_BadInternalCall();returnNULL;

}if (i < 0 || i >= ((PyTupleObject *)op) ->ob_size) {

PyErr_SetString(PyExc_IndexError,"tuple index out of range");returnNULL;

}return ((PyTupleObject *)op) ->ob_item[i];

}

这种情况被称为borrowing a reference。

PyObject* PyTuple_GetItem(PyObject *p, intpos)

Return value: Borrowed reference.

Returns theobject at position pos inthe tuple pointed to by

p. If posis outof bounds, returns NULL and sets an

IndexError exception.

本文也称之为这个对象的引用是unprotected。

Python源码中,返回unprotected preferencess(borrowing a reference)的函数有:

PyTuple_GetItem(),

PyList_GetItem(),

PyList_GET_ITEM(),

PyList_SET_ITEM(),

PyDict_GetItem(),

PyDict_GetItemString(),

PyErr_Occurred(),

PyFile_Name(),

PyImport_GetModuleDict(),

PyModule_GetDict(),

PyImport_AddModule(),

PyObject_Init(),

Py_InitModule(),

Py_InitModule3(),

Py_InitModule4(), and

PySequence_Fast_GET_ITEM().

对于PyArg_ParseTuple()来说,这个函数有时候会返回PyObject,存在类型为PyObject*的参数中。比如sysmodule.c中的例子:

static PyObject *sys_getrefcount(PyObject*self, PyObject *args)

{

PyObject*arg;if (!PyArg_ParseTuple(args, "O:getrefcount", &arg))returnNULL;return PyInt_FromLong(arg->ob_refcnt);

}

PyArg_ParseTuple源码的实现中,没有对arg的引用计数INCREF,所以arg是一个unprotected object,当sys_getrefcount返回时,arg不应当被DECREF。

这里提供一个比较完整的功能函数,计算一个列表中的整数之和.

示例1:

long sum_list(PyObject *list)

{inti, n;long total = 0;

PyObject*item;

n=PyList_Size(list);if (n < 0)return -1; /*Not a list*/

/*Caller should use PyErr_Occurred() if a -1 is returned.*/

for (i = 0; i < n; i++) {/*PyList_GetItem does not INCREF "item".

"item" is unprotected.*/item= PyList_GetItem(list, i); /*Can't fail*/

if(PyInt_Check(item))

total+=PyInt_AsLong(item);

}returntotal;

}

PyList_GetItem()返回的item是PyObject类型,引用计数没有被INCREF,所以函数done之后没有对item进行DECREF。

示例2:

long sum_sequence(PyObject *sequence)

{inti, n;long total = 0;

PyObject*item;

n=PySequence_Length(sequence);if (n < 0)return -1; /*Has no length.*/

/*Caller should use PyErr_Occurred() if a -1 is returned.*/

for (i = 0; i < n; i++) {/*PySequence_GetItem INCREFs item.*/item=PySequence_GetItem(sequence, i);if (item ==NULL)return -1; /*Not a sequence, or other failure*/

if(PyInt_Check(item))

total+=PyInt_AsLong(item);

Py_DECREF(item);

}returntotal;

}

与示例1不同,PySequnce_GetItem()的源码实现中,对返回的item的引用计数进行了INCREF,所以在函数done时需要调用Py_DECREF(item)。

什么时候不需要调用INCREF

1.对于函数中的局部变量,这些局部变量如果是PyObject对象的指针,没有必要增加这些局部对象的引用计数。理论上,当有一个变量指向对象的时候,对象的引用计数会被+1,同时在变量离开作用域时,对象的引用计数会被-1,而这两个操作是相互抵消的,最终对象的引用数没有改变。使用引用计数真正的原因是防止对象在有变量指向它的时候被提前销毁。

什么时候需要调用INCREF

如果有任何的可能在某个对象上调用DECREF,那么就需要保证该对象不能处于unprotected状态。

1) 如果一个引用处于unprotected,可能会引起微妙的bug。一个常见的情况是,从list中取出元素对象,继续操作它,但是不增加它的引用计数。PyList_GetItem 会返回一个 borrowed reference ,所以 item 处于未保护状态。一些其他的操作可能会从 list 中将这个对象删除(递减它的引用计数,或者释放它)。导致 item 成为一个悬垂指针。

bug(PyObject *list) {

PyObject*item = PyList_GetItem(list, 0);

PyList_SetItem(list,1, PyInt_FromLong(0L));

PyObject_Print(item, stdout,0); /*BUG!*/}

这个函数的功能:从list中取出第0个元素item(此时没有递增它的引用计数),然后替换list[1]为整数0,最后打印item.看起来很正常,没有什么问题,其实不然。

我们跟着PyList_SetItem函数的流程走一遍。list中所有元素的引用计数都是protected的,所以当把list[1]的元素替换时,必须将原来的元素的引用计数减少。假设原来的元素list[1]是一个用户自定义的一个类,并且实现了__del__方法。如果这个类的instance的引用计数为1,当减少它的引用计数时,此instance会被释放,会调用__del__方法。而__del__方法是python用户自己写的代码,所以__del__可以是任意的python代码,那么是不是有可能做了某些操作导致list[0]的引用计数无效,比如在__del__方法中del list[0],假如list[0]的引用计数也是1,那么list[0]会被释放,而被释放的item再次被作为参数传递给了PyObject_print()函数,此时会出现意想不到的行为。

解决的办法也很多简单:

no_bug(PyObject *list) {

PyObject*item = PyList_GetItem(list, 0);

Py_INCREF(item);/*Protect item.*/PyList_SetItem(list,1, PyInt_FromLong(0L));

PyObject_Print(item, stdout,0);

Py_DECREF(item);

}

This is a true story. An older version of Python contained variants of this bug and someone spent a considerable amount of time in a C debugger to figure out why his __del__() methods would fail...

2) 传递PyObject对象给函数,一般都是假设传递过来的对象的引用计数已经是protected,因此在函数内部不需要调用Py_INCREF。不过,如果想要参数存活到函数退出,可以调用Py_INCREF。

When you pass an objectreference into another

function,in general, the function borrows the reference fromyou-- ifit needs to store it, it will use Py_INCREF() to become an

independent owner.

PyDict_SetItem()就是这样的例子,将某些东西存放在字典中,会将key和value的引用计数都加1.

而PyTuple_SetItem()和PyList_SetItem()与PyDict_SetItem()不同,他们接管传递给他们的对象(偷取一个引用)。

PyTuple_SetItem的原型是PyTuple_SetItem(atuple, i, item): 如果atuple[i]当前包含了一个PyObject,则将此PyObject DECREF,然后atuple[i]设置为item。item并不会被INCREFed

如果PyTuple_SetItem插入元素失败,会减少item的引用计数。同样,PyTuple_GetItem不会增加返回的item的引用计数。

PyObject *t;

PyObject*x;

x= PyInt_FromLong(1L);

PyTuple_SetItem(t,0, x);

当x作为参数传递给PyTuple_SetItem函数时,那么必须不能调用Py_DECREF,因为PyTuple_SetItem()函数实现中没有增加x的引用计数,如果你此时人为减少x的引用计数,那么tuple t中的元素item已经被释放了。

当tuple t 被DECREFed,其里面的元素都会被DECREFed。

PyTuple_SetItem这样的设计主要是考虑到一个很常见的场景:创建一个新的对象来填充tuple或list。例如创建这样一个tuple, (1, 2, "there")。 使用Python C API可以这样做:

PyObject *t;

t= PyTuple_New(3);

PyTuple_SetItem(t,0, PyInt_FromLong(1L));

PyTuple_SetItem(t,1, PyInt_FromLong(2L));

PyTuple_SetItem(t,2, PyString_FromString("three"));

Note: PyTuple_SetItem是设置tuple元素的唯一的方法。 PySequence_SetItem和PyObject_SetItem都会拒绝这样做,因为tuple是一个不可变的数据类型。

创建list与创建tuple的接口类似,PyList_New()和PyList_SetItem(),有个区别是填充list的元素可以使用PySequence_SetItem(),但是PySequence_SetItem会增加传入的item的引用计数。

PyObject *l, *x;

l= PyList_New(3);

x= PyInt_FromLong(1L);

PySequence_SetItem(l,0, x); Py_DECREF(x);

x= PyInt_FromLong(2L);

PySequence_SetItem(l,1, x); Py_DECREF(x);

x= PyString_FromString("three");

PySequence_SetItem(l,2, x); Py_DECREF(x);

Python信奉极简主义,上述创建tuple(list)和填充tuple(list)的代码可以简化为:

PyObject *t, *l;

t= Py_BuildValue("(iis)", 1, 2, "three");

l= Py_BuildValue("[iis]", 1, 2, "three");

Two Examples:

Example 1:

PyObject*MyFunction(void)

{

PyObject* temporary_list=NULL;

PyObject* return_this=NULL;

temporary_list= PyList_New(1); /*Note 1*/

if (temporary_list ==NULL)returnNULL;

return_this= PyList_New(1); /*Note 1*/

if (return_this ==NULL)

Py_DECREF(temporary_list);/*Note 2*/

returnNULL;

}

Py_DECREF(temporary_list);/*Note 2*/

returnreturn_this;

}

Note1: PyList_New返回的object的引用计数为1

Note2: 因为temporary_list 在函数退出时不应该存在,所以在函数返回前必须DECREFed。

Example 2:

PyObject*MyFunction(void)

{

PyObject* temporary=NULL;

PyObject* return_this=NULL;

PyObject*tup;

PyObject*num;interr;

tup= PyTuple_New(2);if (tup ==NULL)returnNULL;

err= PyTuple_SetItem(tup, 0, PyInt_FromLong(222L)); /*Note 1*/

if(err) {

Py_DECREF(tup);returnNULL;

}

err= PyTuple_SetItem(tup, 1, PyInt_FromLong(333L)); /*Note 1*/

if(err) {

Py_DECREF(tup);returnNULL;

}

temporary= PyTuple_Getitem(tup, 0); /*Note 2*/

if (temporary ==NULL) {

Py_DECREF(tup);returnNULL;

}

return_this= PyTuple_Getitem(tup, 1); /*Note 3*/

if (return_this ==NULL) {

Py_DECREF(tup);/*Note 3*/

returnNULL;

}/*Note 3*/Py_DECREF(tup);returnreturn_this;

}

Note1:如果PyTuple_SetItem失败或者这个tuple引用计数变为0,那么PyInt_FromLong创建的对象引用计数也被减少

Note2:PyTuple_GetItem不会增加返回的对象的引用计数

Note3:MyFunction没有责任处理temporary的引用计数,不需要DECREF temporary

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_39931923/article/details/111005687

智能推荐

C# 雷达图的算法_oxyplot c# 雷达图-程序员宅基地

本人原创,无私奉献,雕虫小技让大家见笑。下面是C# 源码,看不明白的可以留言。工具:VS2017using System;using System.Collections.Generic;using Luman.Practices.NHBaseCore.Enum;using Luman.Project.ClientShare.Sys.Base;using Luman.Proje..._oxyplot c# 雷达图

2017 寒假作业 (一) 随笔-程序员宅基地

前言在写这篇随笔之前,掌握了一下markdown的基本语法,以及探索了一下GitHub的用法,所以拖了一点时间,这也算是写这篇随笔的一大收获吧,在这个过程中提高了自己的自学能力。有关技能和经验的调查你有什么技能比大多人(超过70%以上)更好?针对这个技能的获取你有什么成功的经验?沉思了许久,如果在四个月前也许我会说,数学。在当时我还是能够自信地说我的数学成绩能比70%的人更好,但这70%...

剑指Offer精选编程面试题4-二维数组中的查找-程序员宅基地

文章目录1)题目二维数组中的查找要求:样例2)思路3)小结4)代码1)题目二维数组中的查找要求:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。样例2)思路注意题目说的数组,无论是列还是行,都是递增的,所有这个矩阵,是个有序矩阵,则可以根据这个矩阵的特点进行查找。矩...

linux图形环境下拷贝文件,cpptest针对linux环境使用(含图像界面操作)_不学无术嘤嘤怪的博客-程序员宅基地

Cpptest在linux环境下的使用先决条件:? Linux 内核2.4 或2.6 或更高版本,带有glibc 2.3 或更高版本、以及x86兼容处理器。? Linux 内核2.6 或更高版本,带有glibc 2.3 或更高版本、以及x86_64 兼容处理器。? Solaris 7, 8, 9, 10 和包含gtk 图形化库的UltraSPARC 处理器。? Solaris 7, 8, 9, 1..._cpptest_9.2.0.37

MultCloud – 支持数据互传的网盘管理-程序员宅基地

MultCloudhttps://www.multcloud.com/是一款在线服务,可以在一个地方管理众多网盘,支持国产百度盘,最具有特色的地方是你可以直接在MultCloud里操作将Dropbox的文件传输到百度盘中。MultCloud支持的国外网盘较多,Dropbox、Google Drive、Skydrive、Amazon S3、SugarSync、Box、..._multicloud跨网盘传输0%

android中九宫格的布局,Android布局--九宫格示例.doc_科技瑜晓川的博客-程序员宅基地

Android布局--九宫格示例.doc下载提示(请认真阅读)1.请仔细阅读文档,确保文档完整性,对于不预览、不比对内容而直接下载带来的问题本站不予受理。2.下载的文档,不会出现我们的网址水印。3、该文档所得收入(下载+内容+预览)归上传者、原创作者;如果您是本文档原作者,请点此认领!既往收益都归您。文档包含非法信息?点此举报后获取现金奖励!下载文档到电脑,查找使用更方便9.9积分还剩页未读,继续..._在activity_1中,用线性布局设计一个九宫格

随便推点

Vue 2.6 中部分引入 TypeScript 的方法-程序员宅基地

在 Vue 与 Cesium 联合开发的过程中,我发现很多 Cesium 代码不宜直接写在 .vue 文件中。同时由于 Cesium 库较为复杂,不借助 TypeScript 的静态类型会导致代码难维护困难等问题。而我本身又不太愿意改变 Vue 现有的开发方式,因此决定通过在 Vue 中增加 TS 解析器的方式来解决以上问题。步骤如下:安装 typescript 和 ts-loadern..._如何在老vue2.6上添加typesript

华硕电脑连接不上wifi_笔记本无法连接无线网络WiFi的解决办法_slily0124的博客-程序员宅基地

笔记本连不上无线WiFi网络怎么办呢?路由器无线网络Wifi工作正常,手机都可以成功连接上,但是笔记本电脑却无法连接此无线网络。小编最近也遇到了这个“奇怪”的问题。后来经过多方面探索和尝试,终于解决了问题。现与大家分享一下,希望能帮助同样遇到此问题的网友们。当笔记本电脑连接不上无线网络时,我们需要将无线网卡驱动进行更新或升级。建议将无线网卡驱动升级到“最新版”,而不要升级或使用“兼容版本”或“通用..._华硕主板无线网连不上

406错误码_xmpp 代码406-程序员宅基地

更改之前@RequestMapping(value = "Login", produces = "text/plain;charset=utf-8")更改之后@RequestMapping(value = "Login", produces = "application/json;charset=utf-8")_xmpp 代码406

beast php,windows php-beast 安装-程序员宅基地

https://github.com/imaben/php-beast-binarieswindows下 可以直接在这里下载dll根据自己的php版本 还有是不是线程安全的 来选择下载对应的放到对应的扩展目录 中在配置文件 php.ini中加入扩展配置放到对应的扩展目录 中在配置文件 php.ini中加入扩展配置extension=php_beast_x86_nts.dll加密方案:使用beas..._window php-beast

Linux procfs详解-程序员宅基地

Linux procfs详解1.0 proc文件系统总览在类Unix系统中体现了一种良好的抽象哲学,就是几乎所有的数据实体都被抽象成一个统一的接口--文件来看待,这样我们就可以用一些简单的基本工具完成大量复杂的操作。在Linux中存在着一类特殊的伪文件系统,用于使用与文件接口统一的操作来完成各种功能,例如ptyfs、devfs、sysfs和procfs。而procfs就是其中应用最广泛_procfs

Spotlight 监控工具使用-程序员宅基地

监控MySQL数据库性能的工具:Spotlight on MySQL 《转载》我们的服务器数据库:是在windows2003上。这款工具非常的花哨,界面很漂亮,自带报警。1、创建连接2、监控界面3、查看MySQL启动错误日志4、参数图形界面5、操作系统参数查看这款用着也不错。自己很喜欢,有些参数不...