如何在pybind11中创建对象与聚合返回值成员之间的保持活动关系?

0 投票
1 回答
27 浏览
提问于 2025-04-14 17:06

我正在使用pybind11来为一个C++库生成Python的接口,并且我用pybind11::keep_alive来管理被其他对象引用的C++对象的生命周期。当引用者是直接返回值时,这个方法工作得很好,但当引用者是一个聚合返回值的一部分时,我就遇到了问题。

用一个简化但完整的例子来说明可能更容易:

#include <iostream>

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

namespace py = pybind11;

class Container {
public:
  int some_value;

  Container() {
    std::cerr << "Container constructed\n";
    some_value = 42;
  }

  ~Container() {
    some_value = -1;
    std::cerr << "Container destructed\n";
  }

  // Container is not copyable.
  Container(const Container &) = delete;
  Container &operator=(const Container &) = delete;

  // Iterator references the contents of Container.
  struct Iterator {
    Container *owner;
    int value() const { return owner->some_value; }
  };

  Iterator iterator() { return Iterator{this}; }

  std::pair<Iterator, bool> iterator_pair() { return {Iterator{this}, true}; }
};

PYBIND11_MODULE(example, module) {
  py::class_<Container> owner(module, "Container");

  py::class_<Container::Iterator>(owner, "Iterator")
      .def_property_readonly("value", &Container::Iterator::value);

  owner
      .def(py::init())
      .def("iterator",      &Container::iterator,      py::keep_alive<0, 1>())
      .def("iterator_pair", &Container::iterator_pair, py::keep_alive<0, 1>());
}

在上面的例子中,我有一个容器对象,它返回一个迭代器,这个迭代器引用了容器的内容,所以这个迭代器必须保持容器的存活。对于iterator方法,这个逻辑完全没问题,因为py::keep_alive<0, 1>让迭代器对象保持对创建它的容器对象的引用。

但是,这个方法在iterator_pair方法中就不奏效了。例如,像这样运行:

import example

def Test1():
    it = example.Container().iterator()
    print(it.value)  # prints 42

def Test2():
    it, b = example.Container().iterator_pair()
    assert b == True
    print(it.value)

Test1()  # OK
Test2()  # Fails!

会失败,并输出以下内容:

Owner constructed
42
Owner destructed
Owner constructed
Owner destructed
Traceback (most recent call last):
  File "test.py", line 13, in <module>
    Test2()  # Fails!
    ^^^^^^^
  File "test.py", line 8, in Test2
    it, b = example.Container().iterator_pair()
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: cannot create weak reference to 'tuple' object

而且我不能去掉py::keep_alive(0, 1),因为那样会导致容器被过早销毁:

Owner constructed
42
Owner destructed
Owner constructed
Owner destructed
269054512

(注意,269054512只是内存中的随机垃圾,因为当迭代器引用它的值时,拥有者已经被释放了。)

在这种情况下,推荐的处理方式是什么呢?

我也遇到过类似的问题,比如返回必须保持其父对象存活的std::vector对象,所以我认为更一般的问题是,如何返回包含必须保持其父对象存活的子对象的聚合对象。

我能想到的一个解决方法是,把容器包装在一个std::shared_ptr<>中,并在迭代器中也使用它。这样,无论在Python中是否仍然被引用,C++容器对象都能保持存活。不过,我并不太想这样做,因为这需要在C++端进行大量的重构,而且感觉像是在重复Python已经具备的引用计数支持。

1 个回答

1

你可以自己写一个版本的 keep_alive

struct keep_alive_container {};

namespace pybind11::detail {

    template <>
    struct process_attribute<keep_alive_container>
        : public process_attribute_default<keep_alive_container> {
        static void precall(function_call&) {}
        static void postcall(function_call& call, handle ret) {
            keep_alive_impl(ret.attr("__getitem__")(0), call.args[0]);
        }
    };
} 

// later on
.def("iterator_pair", &Container::iterator_pair, keep_alive_container())

这需要调用一个叫 keep_alive_impl 的函数,它是一个 私有的 pybind11 函数,所以可能不太推荐这样做。

撰写回答