何时需要使用Py_INCREF?

8

我正在开发一个C扩展,目前想追踪内存泄漏。通过阅读Python的文档,很难理解何时增加/减少Python对象的引用计数。此外,在尝试嵌入Python解释器(以便将扩展编译为独立程序)的几天中,我不得不放弃这个努力。因此,像Valgrind这样的工具在这里是无助的。

到目前为止,通过反复尝试,我学会了一些东西,例如Py_DECREF(Py_None)是一件坏事...但是对于任何常量都是这样吗?我不知道。

目前我的主要困惑可以列举如下:

  1. 如果对象在创建它的过程中没有超出该过程的生存期,那么我是否必须减少由PyWhatever_New()创建的任何东西的引用计数?
  2. 每个Py_INCREF是否需要与Py_DECREF匹配,还是应该多一个/另一个?
  3. 如果调用Python过程的结果是PyObject*,那么我是否需要增加它以确保我仍然可以使用它(永远),或者减少它以确保最终它将被垃圾回收,还是两者都不需要?
  4. 通过C API创建的Python对象在堆栈上分配还是在堆上分配?(例如,Py_INCREF可能会将它们重新分配到堆上)。
  5. 在将创建的Python对象传递给Python代码之前,我是否需要对C代码中创建的Python对象采取任何特殊措施?如果Python代码的生存期超出了创建Python对象的C代码的生存期怎么办?
  6. 最后,我知道Python既有引用计数又有垃圾回收器:在这种情况下,如果我弄乱了引用计数(即没有减少足够),那么这对对象有多重要?垃圾回收器最终会找到处理这些对象的方法吗?

1
每当您在文档中看到“borrowed”并且需要对象的生存时间超过几微秒时。 - Ignacio Vazquez-Abrams
你可能会发现使用像PyCXX这样的C++库比使用原始的C API更容易。 (http://cxx.sourceforge.net/PyCXX-Python3.html#h2_no_pointers) - abarnert
你有没有在文档中阅读过《引用计数详解》?因为你所问的一些问题在那里都有明确的答案。虽然我喜欢通过段错误来学习,但通常阅读文档更容易些。 - abarnert
@abarnert 我记得这些例子,但不确定是否在这篇文章中看到过。我明天会重新阅读它。主要问题是它使用的虚假术语。例如,“borrowed”对于不是Python维护者的任何人都没有意义。如果他们想让其他开发人员能够理解,他们必须用“必须增加/减少”的术语来表达。 - wvxvw
1
@abarnert 我非常讨厌C ++,因此不使用PyCXX。我宁愿用汇编语言或Fortran编写它,也不愿在C ++引起的痛苦中生活。 - wvxvw
1个回答

4
大部分内容都在Reference Count Details中涵盖,其余的则在针对你所提出的具体问题的文档中涵盖。但是,为了把所有内容放在一起:

Py_DECREF(Py_None) 是不好的事情……但是任何常量都是如此吗?

更普遍的规则是,如果你没有获得新的/被窃取的引用,并且没有调用 Py_INCREF,那么在任何东西上调用 Py_DECREF 都是不好的。由于你从不在任何可访问的常量上调用 Py_INCREF,这意味着你永远不会在它们上面调用 Py_DECREF

我是否需要在 PyWhatever_New() 创建的任何内容上递减引用计数?

是的。必须递减返回“新引用”的任何内容。按照惯例,以 _New 结尾的任何内容都应该返回一个新引用,但无论如何都应该进行记录(例如,请参见 PyList_New)。

每个 Py_INCREF 需要与 Py_DECREF 匹配,还是应该有一个多/少一个?

你自己代码中的数字可能不一定平衡。总数必须平衡,但在 Python 本身内部会进行增量和减量。例如,任何返回“新引用”的内容已经执行了 inc,而任何“窃取”引用的内容将对其进行 dec。

通过 C API 创建的 Python 对象是否在堆栈上分配在堆上还是堆栈上?(例如,Py_INCREF 是否将它们重新分配到堆上)。

无法通过 C API 在堆栈上创建对象。C API 只有返回对象指针的函数。

大多数这些对象都在堆上分配。有些实际上在静态内存中。

但是你的代码无论如何都不应该关心这个问题。你永远不会分配或删除它们;它们在 PySpam_New 等函数中被分配,并在你将它们 Py_DECREF 到 0 时自行释放,因此它们在哪里并不重要。

(除了你可以通过全局名称访问的常量,例如 Py_None。显然,这些常量在静态存储中。)

在将它们传递给 Python 代码之前,在 C 代码中创建的 Python 对象是否需要进行特殊处理?

不需要。

如果 Python 代码超过创建 Python 对象的 C 代码的生命周期怎么办?

我不确定你在这里说的“outlives”是什么意思。只要有任何对象依赖于您的扩展模块,它就不会被卸载。(实际上,在至少3.8之前,您的模块可能永远不会被卸载,直到关闭为止)。
如果你只是指新创建了一个对象的函数返回的问题,那不是问题。你必须非常努力地在堆栈上分配任何Python对象。而且没有办法将诸如C对象数组或C字符串之类的东西传递到Python代码中,而不将它们转换为Python对象元组或Python字节或str。有一些情况下,例如,你可以在PyCapsule中存储指向堆栈上某个东西的指针,并将其传递过去,但这与任何C程序都是相同的,并且......不要这样做。
最后,我知道Python既有引用计数又有垃圾回收器。
垃圾回收器只是一个循环打破器。如果您有保持彼此活着的对象引用循环,可以依赖GC。但是,如果您泄漏了对对象的引用,GC将永远不会清理它。

由于您从未在任何可访问为常量的东西上调用过 Py_INCREF - 那数字呢?例如 PyLong_FromLong()?它可能返回一个常量,也可能返回一个瞬时对象。如何知道它是其中之一? - wvxvw
我不确定你所说的“outlives”是什么意思。这种情况发生在C代码创建了一个Python对象,调用Python函数并将该对象的引用存储在其他地方(可能是类字段),然后Python函数退出,接着C函数退出(但仍存在具有对先前传递的值的引用的类实例)。 - wvxvw
1
@wvxvw,说实话,你似乎更喜欢争论这是不可能的,尽管世界上其他所有C扩展都可以做到。它就像看起来那么简单,没有隐藏的陷阱,只是需要大量手动引用计数,我们有时会因为同样愚蠢的原因而弄错malloc和free,并且调试它并没有显着不同。老实说,如果您无法使用Cython,那么使用带有助手的C ++或Rust或其他语言确实要容易得多,以便为您完成所有这些烦人的工作,但如果您讨厌这个想法,唯一的选择就是坚定地写下去。 - abarnert
1
@wvxvw 别再关注“Python代码被垃圾回收了”的问题了。GC(循环检测器)几乎从不涉及其中。每当一个值存储在命名空间或显式集合中时,Python都会执行一次incref操作;每当一个值从命名空间或集合中删除时,它会执行一次decref操作;只有当decref将对象的引用计数降至0时,该对象才会被删除。如果您调用了incref并额外持有指针,则在执行decref之前它不会降至0。是的,如果您在将其传递给Python之前将其decref为0,则在该decref调用中已经销毁了它,因此您正在传递指向垃圾的指针。 - abarnert
1
@wvxvw 或许这样会更清晰:唯一销毁对象的方式是某个人(你、处理 del 的解释器循环、持有引用的其他被销毁的对象、关闭逻辑等)调用 decref 并将引用计数降至 0。除了 decref,没有其他方法可以调用析构函数。这与其他引用计数系统相同。 - abarnert
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接