pybind11中从C++返回的Python对象id()输出不稳定的原因咨询
我现在用一个Holder类存储指向Python中创建的C++ Child类对象的std::shared_ptr,相关代码如下:
C++ 代码(pointers.cpp)
#include <memory> #include <iostream> #include <pybind11/pybind11.h> class Child { }; class Holder { public: Holder(std::shared_ptr<Child> child) : m_child {child} { print_child_address(); } std::shared_ptr<Child> get_child() { return m_child; } void print_child_address() { std::cout << m_child << std::endl; } private: std::shared_ptr<Child> m_child; }; namespace py = pybind11; PYBIND11_MODULE(pointers, m) { py::class_<Child, std::shared_ptr<Child>>(m, "Child") .def(py::init<>()); py::class_<Holder>(m, "Holder") .def(py::init<const std::shared_ptr<Child>&>(), py::arg("child")) .def("get_child", &Holder::get_child) .def("print_child_address", &Holder::print_child_address); }
CMakeLists.txt
cmake_minimum_required(VERSION 3.21) project(pointers VERSION 1.0 LANGUAGES CXX) list(APPEND CMAKE_PREFIX_PATH "${CMAKE_BINARY_DIR}") find_package(pybind11 REQUIRED) pybind11_add_module(pointers pointers.cpp)
正常使用场景(Python交互)
>>> from pointers import * >>> c = Child() >>> h = Holder(c) 0x607d93adc800 >>> hex(id(c)) '0x7c9f96460a70' >>> hex(id(h.get_child())) '0x7c9f96460a70'
这个场景下一切正常,但当我用下面的方式调用时,出现了奇怪的现象:
异常现象场景(Python交互)
>>> h = Holder(Child()) 0x6304fb8a8800 >>> hex(id(h.get_child())) '0x7dd87bcc4eb0' >>> hex(id(h.get_child())) '0x7dd87bcc4c70' >>> a = h.get_child() >>> hex(id(h.get_child())) '0x7dd87bcc4eb0' >>> hex(id(h.get_child())) '0x7dd87bcc4eb0'
可以看到,h.get_child()返回的对象id每次调用都变化,直到把它赋值给变量a后才稳定;在iPython里更特殊,只要单独调用一次h.get_child(),id就会稳定下来。
C++侧的std::shared_ptr是完全稳定的,从print_child_address()的输出就能看出来,但Python侧对象id的这种不稳定让我很困惑,有人能解释下这背后的原因吗?
原因解释
这其实是pybind11的临时Python代理对象机制导致的现象,核心是Python代理对象与C++ std::shared_ptr的绑定逻辑:
持久代理对象的情况:
当你执行c = Child()时,Python会创建一个持久的Python代理对象,这个对象持有对应C++ Child对象的std::shared_ptr,Python的引用计数和C++的shared_ptr计数是绑定的,所以这个代理对象的id是固定的,后续调用get_child()会直接复用这个代理,id自然不变。临时代理对象的情况:
当你执行Holder(Child())时,Child()创建的是一个临时Python代理对象——这个对象在传给Holder构造函数后,因为没有被任何Python变量持有,它的Python引用计数会降到0,此时pybind11会销毁这个Python代理对象。但C侧的std::shared_ptr被Holder持有,所以C的Child对象并没有被销毁。get_child()的行为逻辑:
当你调用h.get_child()时,pybind11会检查当前是否存在对应C++std::shared_ptr的Python代理对象:- 如果之前的临时代理已经被销毁,pybind11会重新创建一个新的Python代理对象来包裹这个
std::shared_ptr,每次调用如果没有持久的Python代理存在,就会新建一个,所以每次的id都不一样(因为是不同的Python对象)。 - 当你把
h.get_child()赋值给变量a时,这个新的Python代理对象被Python持有,引用计数保持大于0,之后再调用get_child()时,pybind11会找到这个已存在的代理对象,所以id就稳定了。
- 如果之前的临时代理已经被销毁,pybind11会重新创建一个新的Python代理对象来包裹这个
iPython的特殊行为:
iPython会自动将最后一次表达式的结果保存到内置变量_中,所以当你单独调用h.get_child()时,返回的代理对象会被_持有,引用计数不为0,之后再调用get_child()就会复用这个代理,id也就稳定了。
补充说明
- 完全不用担心C侧的Child对象安全:因为
std::shared_ptr的计数是独立且正确的,只要Holder还持有它,不管Python代理对象怎么创建销毁,C的Child对象都不会被销毁。 - 这种id变化只是Python侧代理对象的创建销毁导致的,本质上所有这些代理对象都对应同一个C++ Child对象,你可以通过调用
print_child_address()验证,C++侧的内存地址始终是同一个。
内容来源于stack exchange

