Numpy arrays¶
Most Python functions in aare take NumPy arrays as input. The bindings in
python/src/ receive them as py::array_t<T> and wrap the buffer in an
NDView before calling the C++ implementation. No data is copied, so the
C++ code reads and writes the memory that the caller’s array owns. This page
explains the choices we make when declaring such an argument and what they
mean for the user.
The preferred pattern¶
Declare the argument with only the element type, take it by value, mark it
noconvert in the py::arg list and let make_view check the rest:
m.def(
"push",
[](Pedestal &self, py::array_t<uint16_t> frame) {
auto view = make_view_2d(frame);
self.push(view);
},
py::arg("frame").noconvert());
The responsibilities are split between pybind11 and make_view:
pybind11 checks that the argument is a NumPy array with exactly the requested dtype. Anything else fails with a
TypeError, and since the argument isnoconvertpybind11 never tries to build a matching array from it.make_view(python/src/np_helper.hpp) checks that the array has the expected number of dimensions and is C-contiguous. It raises aValueErrorwith a message that tells the user what to fix, for exampleArray is not C-contiguous. Use np.ascontiguousarray(arr). Writing functions usemake_viewwhich also rejects read-only arrays. Read-only input goes throughmake_const_view.
The result is a binding that either operates on the caller’s memory or refuses the call. It never silently works on a temporary copy.
Take the array by value¶
py::array_t is a handle to the Python object, not the data. Passing it by
value copies the handle, which increments the reference count of the NumPy
array, and nothing else. The pixel data stays where it is. We therefore
declare the parameter as py::array_t<T> frame rather than
py::array_t<T> &frame:
A reference saves one reference-count increment per call, which is not measurable next to the work done on a frame.
A by-value parameter is a plain lvalue inside the lambda, so
make_view, which callsmutable_data(), works without further qualification. Aconst py::array_t<T> &does not compile with it.It matches the usual pybind11 style for
py::objectand its subclasses, so the binding reads the same as examples in the pybind11 documentation.
Why not let pybind11 convert¶
Without noconvert, pybind11 tries every overload once with conversions
disabled and then once more with conversions enabled. In the second pass a
py::array_t<T> argument accepts anything NumPy can turn into an array of
type T: an array with another dtype, a non-contiguous array, or even a
Python list. pybind11 creates a new array, hands the converted copy to the
binding and drops it when the call returns. For detector data this has
several consequences:
The copy costs time and memory, and the user gets no indication that it happened. For a stack of frames this can be gigabytes.
Writes are lost. A function that fills or updates its input, for example a pedestal subtraction in place, writes into the temporary copy.
Values are cast without warning.
forcecastallows unsafe casts, so afloat64frame is truncated touint16instead of being rejected.Overloads stop working as intended.
apply_calibrationis registered for bothdoubleandfloat. With conversions allowed the first overload would accept everything by casting and thefloatoverload would never be chosen.
Argument conversion is only worth it for small inputs where convenience beats control, for example the parameter vector passed to a fit. Even then the function must not write into the argument.
Note
When a binding intentionally lets pybind11 convert, say so with a comment
next to the argument. Otherwise a missing noconvert looks like an
oversight and the next person to touch the binding will add it.
// Conversion allowed: small read-only vector, accept lists and any dtype
py::array_t<double, py::array::c_style | py::array::forcecast> par
What the template flags mean¶
py::array_t<T, Flags> takes an optional second template argument. The
flags do two things: they add requirements that the array must fulfil for the
non-converting check to pass, and they tell NumPy what to produce when a
conversion is allowed.
py::array::c_styleThe array must be C-contiguous. With
noconverta non-contiguous array is rejected by pybind11 with aTypeErrorlisting the accepted signatures. Withoutnoconvertpybind11 makes a contiguous copy. Sincemake_viewalready enforces contiguity and gives a more helpfulValueError, the flag is redundant in the preferred pattern. It is still present in some older bindings and is harmless there.py::array::f_styleSame as above for Fortran order.
NDViewassumes C order, so we do not use it.py::array::forcecastAllows NumPy to perform unsafe casts, such as
float64touint16, when converting. It is the default value of the template argument, sopy::array_t<uint16_t>andpy::array_t<uint16_t, py::array::forcecast>are the same type. The flag has no effect together withnoconvertbecause no conversion takes place. It only matters when conversions are allowed, where it decides whether a lossy cast is accepted or the call is rejected.
The common spelling py::array_t<T, py::array::c_style | py::array::forcecast>
therefore means: when converting, produce a contiguous array of type T and
cast whatever you were given. Combined with noconvert it collapses to
“C-contiguous array of exactly type T”, which is what the plain
py::array_t<T> plus make_view already gives us.
Summary¶
Declare the argument as
py::array_t<T>by value and add.noconvert()to itspy::arg. The handle is reference counted, so passing it by value does not copy the data.Create the view with
make_view_1d/2d/3dormake_const_view_*and let it validate dimensions and contiguity. Add an explicit check only when the binding accepts several shapes, asapply_calibrationdoes for 2D and 3D pedestals.Return data to Python with
return_image_dataorreturn_vectorso that the NumPy array owns the C++ allocation.Only allow conversion for small, read-only inputs where accepting lists or other dtypes is a real convenience for the user, and mark it with a comment in the code.