You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

pybind11中从C++返回的Python对象id()输出不稳定的原因咨询

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的绑定逻辑:

  1. 持久代理对象的情况:
    当你执行c = Child()时,Python会创建一个持久的Python代理对象,这个对象持有对应C++ Child对象的std::shared_ptr,Python的引用计数和C++的shared_ptr计数是绑定的,所以这个代理对象的id是固定的,后续调用get_child()会直接复用这个代理,id自然不变。

  2. 临时代理对象的情况:
    当你执行Holder(Child())时,Child()创建的是一个临时Python代理对象——这个对象在传给Holder构造函数后,因为没有被任何Python变量持有,它的Python引用计数会降到0,此时pybind11会销毁这个Python代理对象。但C侧的std::shared_ptr被Holder持有,所以C的Child对象并没有被销毁。

  3. 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就稳定了。
  4. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 07:18:05