【问题标题】:pybind11 keeping objects alivepybind11 保持对象活着
【发布时间】:2020-01-30 19:39:12
【问题描述】:

我正在研究 pybind11 中的一个测试文件,并遇到了 keep_alive 的不同用法。

py::keep_alive<1, 2>
py::keep_alive<1, 0>
py::keep_alive<0, 1>

有人能解释一下这个测试文件中的这些用法吗?我知道索引0 指的是返回,1 指的是this 指针。我只能理解py::keep_alive&lt;1, 2&gt;(使用文档),但不能理解它在这个测试文件中的用法。

class Child {
public:
    Child() { py::print("Allocating child."); }
    Child(const Child &) = default;
    Child(Child &&) = default;
    ~Child() { py::print("Releasing child."); }
};
py::class_<Child>(m, "Child")
    .def(py::init<>());

class Parent {
public:
    Parent() { py::print("Allocating parent."); }
    ~Parent() { py::print("Releasing parent."); }
    void addChild(Child *) { }
    Child *returnChild() { return new Child(); }
    Child *returnNullChild() { return nullptr; }
};
py::class_<Parent>(m, "Parent")
    .def(py::init<>())
    .def(py::init([](Child *) { return new Parent(); }), py::keep_alive<1, 2>())
    .def("addChild", &Parent::addChild)
    .def("addChildKeepAlive", &Parent::addChild, py::keep_alive<1, 2>())
    .def("returnChild", &Parent::returnChild)
    .def("returnChildKeepAlive", &Parent::returnChild, py::keep_alive<1, 0>())
    .def("returnNullChildKeepAliveChild", &Parent::returnNullChild, py::keep_alive<1, 0>())
    .def("returnNullChildKeepAliveParent", &Parent::returnNullChild, py::keep_alive<0, 1>());

【问题讨论】:

    标签: c++ pybind11


    【解决方案1】:

    在实际代码中,addChild 函数将通过将 Parent 对象存储到指针来实现,而不获取所有权(即它不会稍后在 C++ 端删除它)。 py::keep_alive&lt;1, 2&gt; 所做的就是将来自Parent 对象的引用放到传递给addChildChild 对象上,从而将Child 的生命周期与Parent 的生命周期联系起来。

    所以,如果写作:

    p = Parent()
    p.addChild(Child())
    

    如果没有keep_alive,那么临时的Child 对象将在下一行超出范围(引用计数减至零)。相反,使用keep_alive&lt;1, 2&gt;,会发生以下情况(伪代码):

    p = Parent()
    c = Child()
    p.__keep_alive = c
    p.addChild(c)
    del c
    

    所以现在当p 超出范围时,它的数据会被清理,包括。 __keep_alive 引用,此时 c 也会被清理。意思是,p 和“临时”子 c 同时超出范围,而不是更早。

    编辑:对于keep_alive&lt;0, 1&gt;,隐式this 的生命周期与返回值相关联。在测试中,它仅用于验证该策略是否可以使用 None 返回,但在访问临时的内部数据项时很常见,通常在长语句中处理中间临时,如下所示:

    c = getCopyOfData().at(0).getField('f')
    

    问题在于,在 C++ 中,临时对象的生命周期一直到语句结束,因此上述情况在音译代码中很常见。但在 Python 中,它以 ref-count 为 0 结束。Iow.,getCopyOfData() 的结果将在调用 at(0) 完成后消失,将 getField() 点留在已删除的内存中。相反,使用keep_alive&lt;0, 1&gt;,它将是(伪代码):

    d = getCopyOfData()
    at0 = d.at(0)
    at0.__keep_alive = d
    del d
    c = at0.getField('f')
    c.__keep_alive = at0
    del at0
    

    所以现在复制的数据容器d 不会超出范围,直到对访问字段的引用超出范围。

    对于keep_alive&lt;1, 0&gt;,返回值的生命周期与隐含的this 相关联。如果所有权被传递给调用者,这很有用,而隐式 this 保留一个指针,实际上将内存管理从 C++ 推迟到 Python。请记住,在 pybind11 中,对象身份被保留,因此任何对 returnChildKeepAlive 的调用返回相同的指针都会导致相同的 Python 对象,而不是新对象。所以在这种情况下(伪代码):

    c = p.returnChildKeepAlive()    # with c now owning the C++ object
    p.__keep_alive = c
    

    如果引用 c 首先超出范围,p 仍将使其保持活动状态,以免被悬空指针卡住。如果p首先超出范围,c不会受到影响,因为它接管了所有权(即C++端不会被删除)。而如果returnChildKeepAlive()被第二次调用,它会返回一个对未完成的c的引用,而不是一个新的代理,因此不会影响整个生命周期管理。

    【讨论】:

    • 谢谢。您能否也评论一下keep_alive&lt;1,0&gt;keep_alive&lt;0,1&gt;
    • 完成;见编辑。我的印象是这个问题主要是关于keep_alive&lt;1, 2&gt;
    • 感谢 Wim 的宝贵时间和完整的回答。作为一个附带问题,如果我使用import py_compilepy_compile.compile('test.py') 编译我的python 脚本,那么我可以将test.cpython-38.pyc 导入pybind 吗?这会在编译后增强 python 集成吗?
    • pybind11 将导入转发到 Python,因此您不会将 test.pyc 交给它,并且始终只导入 test。如果test.pyc 存在于test.py 旁边并且是最新的(或者如果您首先只有test.pyc),它将被加载以支持.py 文件。预编译只影响启动性能,不影响集成。如果您预编译,则仅在安装期间执行此操作:.pyc.pyo 文件将包含原始源文件的完整路径,如果其中任何一个被移动,这会导致令人困惑的回溯。
    • 非常感谢您的帮助。知识渊博。感谢分享!
    猜你喜欢
    • 2011-06-19
    • 2011-05-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多