扩展Python模块系列(四)----引用计数问题的处理
承接上文,发现在使用Python C/C++ API扩展Python模块时,总要在各种各样的地方考虑到引用计数问题,稍不留神可能会导致扩展的模块存在内存泄漏。引用计数问题是C语言扩展Python模块最头疼的地方,需要由程序员对使用的每个C API都要充分了解,甚至要熟悉源码才能精确掌握什么时候引用计数加一,什么时候减一。
本文为翻译文章,我觉得对于源码中的引用计数讲解得比较清楚,所以就翻译为中文。http://edcjones.tripod.com/refcount.html#
Summary:
Python Object的结构体定义包含一个引用计数和对象类型:
#define PyObject_HEAD \
int ob_refcnt; \
struct _typeobject *ob_type; typedef struct _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 != ) \
_Py_CHECK_REFCNT(op) \
else \
_Py_Dealloc((PyObject *)(op)); \
} while ()
另外一组考虑是对象为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()。
void MyCode(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);
...
return pyo;
}
此时,MyCode不应该调用PY_DECREF(),在这种情况下,MyCode将pyo对象的引用计数责任传递了出去。
Note:如果一个函数返回的是None对象,C代码应该是这样:必须要增加None对象的引用计数。
Py_INCREF(Py_None);
return Py_None;
到目前为止讨论了最常见的情况,即当调用Py_Something创建了一个引用,并将引用计数的责任传递给其调用者。在Python文档中,这被称为new reference。比如文档中有说明:
PyObject* PyList_New(int len)
Return value: New reference.
Returns a new list of length len on success, or NULL on failure.
当一个引用被INCREF,通常称为这个引用被protected。
有时候,Python源码中不会调用Py_DECREF()。
PyObject *
PyTuple_GetItem(register PyObject *op, register int i)
{
if (!PyTuple_Check(op)) {
PyErr_BadInternalCall();
return NULL;
}
if (i < || i >= ((PyTupleObject *)op) -> ob_size) {
PyErr_SetString(PyExc_IndexError,
"tuple index out of range");
return NULL;
}
return ((PyTupleObject *)op) -> ob_item[i];
}
这种情况被称为borrowing a reference。
PyObject* PyTuple_GetItem(PyObject *p, int pos)
Return value: Borrowed reference.
Returns the object at position pos in the tuple pointed to by
p. If pos is out of 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))
return NULL;
return PyInt_FromLong(arg->ob_refcnt);
}
PyArg_ParseTuple源码的实现中,没有对arg的引用计数INCREF,所以arg是一个unprotected object,当sys_getrefcount返回时,arg不应当被DECREF。
这里提供一个比较完整的功能函数,计算一个列表中的整数之和.
示例1:
long sum_list(PyObject *list)
{
int i, n;
long total = ;
PyObject *item; n = PyList_Size(list);
if (n < )
return -; /* Not a list */
/* Caller should use PyErr_Occurred() if a -1 is returned. */
for (i = ; 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);
}
return total;
}
PyList_GetItem()返回的item是PyObject类型,引用计数没有被INCREF,所以函数done之后没有对item进行DECREF。
示例2:
long sum_sequence(PyObject *sequence)
{
int i, n;
long total = ;
PyObject *item;
n = PySequence_Length(sequence);
if (n < )
return -; /* Has no length. */
/* Caller should use PyErr_Occurred() if a -1 is returned. */
for (i = ; i < n; i++) {
/* PySequence_GetItem INCREFs item. */
item = PySequence_GetItem(sequence, i);
if (item == NULL)
return -; /* Not a sequence, or other failure */
if (PyInt_Check(item))
total += PyInt_AsLong(item);
Py_DECREF(item);
}
return total;
}
与示例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, ); PyList_SetItem(list, , PyInt_FromLong(0L));
PyObject_Print(item, stdout, ); /* 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, );
Py_INCREF(item); /* Protect item. */ PyList_SetItem(list, , PyInt_FromLong(0L));
PyObject_Print(item, stdout, );
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 object reference into another
function, in general, the function borrows the reference from you
-- if it 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, , 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();
PyTuple_SetItem(t, , PyInt_FromLong(1L));
PyTuple_SetItem(t, , PyInt_FromLong(2L));
PyTuple_SetItem(t, , 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();
x = PyInt_FromLong(1L);
PySequence_SetItem(l, , x); Py_DECREF(x);
x = PyInt_FromLong(2L);
PySequence_SetItem(l, , x); Py_DECREF(x);
x = PyString_FromString("three");
PySequence_SetItem(l, , x); Py_DECREF(x);
Python信奉极简主义,上述创建tuple(list)和填充tuple(list)的代码可以简化为:
PyObject *t, *l; t = Py_BuildValue("(iis)", , , "three");
l = Py_BuildValue("[iis]", , , "three");
Two Examples:
Example 1:
PyObject*
MyFunction(void)
{
PyObject* temporary_list=NULL;
PyObject* return_this=NULL; temporary_list = PyList_New(); /* Note 1 */
if (temporary_list == NULL)
return NULL; return_this = PyList_New(); /* Note 1 */
if (return_this == NULL)
Py_DECREF(temporary_list); /* Note 2 */
return NULL;
} Py_DECREF(temporary_list); /* Note 2 */
return return_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;
int err; tup = PyTuple_New();
if (tup == NULL)
return NULL; err = PyTuple_SetItem(tup, , PyInt_FromLong(222L)); /* Note 1 */
if (err) {
Py_DECREF(tup);
return NULL;
}
err = PyTuple_SetItem(tup, , PyInt_FromLong(333L)); /* Note 1 */
if (err) {
Py_DECREF(tup);
return NULL;
} temporary = PyTuple_Getitem(tup, ); /* Note 2 */
if (temporary == NULL) {
Py_DECREF(tup);
return NULL;
} return_this = PyTuple_Getitem(tup, ); /* Note 3 */
if (return_this == NULL) {
Py_DECREF(tup);
/* Note 3 */
return NULL;
} /* Note 3 */
Py_DECREF(tup);
return return_this;
}
Note1:如果PyTuple_SetItem失败或者这个tuple引用计数变为0,那么PyInt_FromLong创建的对象引用计数也被减少
Note2:PyTuple_GetItem不会增加返回的对象的引用计数
Note3:MyFunction没有责任处理temporary的引用计数,不需要DECREF temporary
扩展Python模块系列(四)----引用计数问题的处理的更多相关文章
- 扩展Python模块系列(一)----开发环境配置
本系列将介绍如何用C/C++扩展Python模块,使用C语言编写Python模块,添加到Python中作为一个built-in模块.Python与C之间的交互目前有几种方案: 1. 原生的Python ...
- 扩展Python模块系列(五)----异常和错误处理
在上一节中,讨论了在用C语言扩展Python模块时,应该如何处理无处不在的引用计数问题.重点关注的是在实现一个C Python的函数时,对于一个PyObject对象,何时调用Py_INCREF和Py_ ...
- 扩展Python模块系列(二)----一个简单的例子
本节使用一个简单的例子引出Python C/C++ API的详细使用方法.针对的是CPython的解释器. 目标:创建一个Python内建模块test,提供一个功能函数distance, 计算空间中两 ...
- 扩展Python模块系列(三)----参数解析与结果封装
在上一节中,通过一个简单的例子介绍了C语言扩展Python内建模块的整体流程,从本节开始讲开始深入讨论一些细节问题,在细节讨论中从始至终都会涉及[引用计数]的问题.首先讨论C语言封装的Python函数 ...
- C语言扩展Python模块
1. 先创建一个PythonDemo.cpp文件: //c/c++中调用python脚本,配置步骤参见上一篇:C/C++与python交互 \ C/C++中调用python文件. #include ...
- python模块学习(四)
re模块 就其本质而言,正则表达式(或 RE)是一种小型的.高度专业化的编程语言,(在Python中)它内嵌在Python中,并通过 re 模块实现.正则表达式模式被编译成一系列的字节码,然后由用 C ...
- 【听如子说】-python模块系列-AIS编解码Pyais
Pyais Module Introduce pyais一个简单实用的ais编解码模块 工作中需要和ais打交道,在摸鱼的过程中发现了一个牛逼的模块,对ais编解码感兴趣的可以拿项目学习一下,或者运用 ...
- python模块知识四 包和logging日志
11.包 包:文件夹下具有__init__.py文件就是一个包,包用来管理多个模块 包的结构如下: bake ├── __init__.py ├── api ├── __init__.py ├── p ...
- Python 模块之间的引用
项目结构: Dog.Cat模块引用Animal模块 Animal模块代码: # -*- coding:UTF-8 -*- # 定义一个动物类 class Animal(object): def run ...
随机推荐
- 苹果APP发布
1 发布方式 苹果发布上架有两种方式,一种是上传到苹果商店,一种是挂在web服务器上扫描下载,下面分别介绍这两种发布方式 1.1 上传AppStore 1. 用公司账号或者个人开发账号生成上架.p12 ...
- 【canvas学习笔记一】基本认识
<canvas>标签定义了一块画布,画布可以在网页中绘制2D和3D图象,现在先学习如何绘制2D图象,绘制3D图象属于WebGL的内容(也就是网页版的OpenGL,3D图形接口). 属性 & ...
- easyui复选框树动态加载后台数据,实现自动选中数据库中数据。后台语言是.NET
最近公司做项目用到了easyui复选框树来实现加载不同类型产品.因为我刚刚毕业,现在也算是实习吧,所以一脸懵逼啊.在公司里的一个哥的帮助下 ,我写出来这个EasyUi复选框树了,虽然东西不难,但也是自 ...
- SQL Server系列之SQL Server 2016 中文企业版详细安装步骤(超多图)
1. 下载地址 下载地址 :https://www.microsoft.com/en-us/server-cloud/products/sql-server-2016/ 官方技术文档:https:// ...
- SpringMVC源码情操陶冶-AbstractHandlerExceptionResolver
springmvc支持服务端在处理业务逻辑过程中出现异常的时候可以配置相应的ModelAndView对象返回给客户端,本文介绍springmvc默认的几种HandlerExceptionResolve ...
- (转)使用BigDecimal进行精确运算
场景:在进行支付业务的金额计算时,通常采用BigDecimal类型的数据,并没有看到常见的int double类型,所以有必要好好学习下BigDecimal的常用用法. 1 误区 首先我们先来看如下代 ...
- java多线程系列(四)---Lock的使用
Lock的使用 前言:本系列将从零开始讲解java多线程相关的技术,内容参考于<java多线程核心技术>与<java并发编程实战>等相关资料,希望站在巨人的肩膀上,再通过我的理 ...
- iptables中DNAT的配置方法
1.一对一流量完全DNAT 首先说一下网络环境,普通主机一台做防火墙用,网卡两块 eth0 192.168.0.1 内网 eth1 202.202.202.1 外网 内网中一台主机 192.168. ...
- Mybatis-Generator生成Mapper文件中<if test="criteria.valid">的问题解答
写在前面 <Docker+SpringBoot+Mybatis+thymeleaf的Java博客系统开源啦> 由于开源了项目的缘故,很多使用了My Blog项目的朋友遇到问题也都会联系我去 ...
- centos下编译安装Openssl
yum install -y zlib*mkdir /datacd /data下载好tar包tar zxf openssl-1.0.2g.tar.gzcd openssl-1.0.2g./config ...