Skip to content

[BUG]: def_property doesn't seems to keep parent alive when the return object is a py::array #4236

@zhqrbitee

Description

@zhqrbitee

Required prerequisites

Problem description

I posted this as a discussion before: #4200, but got no answer for a while, so repost it here.

I have a use case that have some internal C++ vectors and want to return to python side as numpy array. To avoid copy, we only return a view on a C++ vector. It works OK in most of times, but I'm now hitting below corner case:

#include <vector>
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>

struct Wrapper1 {
  Wrapper1(const std::vector<double> &values): values{values} {};
  std::vector<double> values;
};

struct Wrapper2 {
  Wrapper2(const std::vector<double> &values): values{values} {};

  std::shared_ptr<Wrapper1> wrapper1() const { return std::make_shared<Wrapper1>(values); };

  std::vector<double> values;
};

namespace py = pybind11;
using namespace pybind11::literals;

PYBIND11_MODULE(test_return_numpy_array_no_copy_helper, m) {
  py::class_<Wrapper1, std::shared_ptr<Wrapper1>>(m, "Wrapper1")
      .def(py::init<const std::vector<double> &>(), "values"_a)
      .def_property_readonly("values", [](const Wrapper1& self) {
        // https://github.com/pybind/pybind11/issues/1042#issuecomment-325941022
        auto array = pybind11::array(self.values.size(), self.values.data(),
                                     // the base of the numpy array, as long as it is set to a python object, it won't do copy
                                     pybind11::handle{Py_None});
        // https://github.com/pybind/pybind11/issues/481, make numpy array read-only
        reinterpret_cast<pybind11::detail::PyArray_Proxy*>(array.ptr())->flags &=
            ~pybind11::detail::npy_api::NPY_ARRAY_WRITEABLE_;
        return array;
      });

  py::class_<Wrapper2, std::shared_ptr<Wrapper2>>(m, "Wrapper2")
      .def(py::init<const std::vector<double> &>(), "values"_a)
      .def_property_readonly("wrapper1", &Wrapper2::wrapper1);
}

Wrapper2 create a new instance of std::shared_ptr<Wrapper1> which contains a C++ std::vector values. The values is then return to python as a numpy array view.
From the docs: https://pybind11.readthedocs.io/en/stable/advanced/functions.html, def_property_readonly will use return_value_policy::reference_internal and thus keep the parent object alive. So I expect the values returned will keep its parent, i.e. the new instance of std::shared_ptr<Wrapper1> alive and the underlying memory will be valid as long as values is alive in Python.

However, below test case shows it is not the case

def test_return_np_array_with_no_copy():
    vec = [0.0, 0.5, 1.3, 2.2]
    wrapper2 = Wrapper2(vec)
    values = wrapper2.wrapper1.values
    # in some cases, you will see values be something like
    # array([1.08885983e-309, 5.00000000e-001, 1.30000000e+000, 2.20000000e+000])).
    # It means the underlying memory is destroyed
    assert np.all(values == vec), f"{values}"

I also try to manually to use keep_alive<0, 1> in the values binding of Wrapper1, but that doesn't seem to help.
So is the keep_alive system not supposed to work with an existing python object, e.g. py::array? If so, what should I do to make above test case work?

Any thoughts would be appreciated. Thanks.

Reproducible example code

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    triageNew bug, unverified

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions