-
Notifications
You must be signed in to change notification settings - Fork 128
-
This question is asked as a complete newcomer to pyo3 and related numpy handling.
If converting a rust vec structure into a numpy array, e.g.
let array = Python::with_gil(|py| vec_f32.into_pyarray(py).to_owned());
Are there any drawbacks to calling Python from rust (complexity, performance, portability, etc)?
In my case I am converting the vec to a numpy array purely for purposes of returning the data to Python. It is not subsequently used from rust.
In other words, would there be situations where it might be better just to return the vec structure to Python (as a list) and then take care of wrapping the list as a numpy array from within Python directly?
Thanks.
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 2 comments 4 replies
-
When you say "returning the data to Python", this probably implies you already have the GIL?
Beta Was this translation helpful? Give feedback.
All reactions
-
Exactly, you can just add py: Python as an additional argument to your #[pyfunction] which PyO3 will use to pass you a GIL token so that you do not need to call Python::with_gil (which will just check that you already have the GIL and be a no-op in this case).
((Alternatively, any GIL-bound reference like vector: &PyArray1<f64> gives you access to a GIL token by calling PyAny::py, e.g. let py = vector.py().))
If you still unsure how to avoid calling Python::with_gil, maybe you could post your complete #[pyfunction] here and we can try to figure it our together?
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
In my case, the function is a method inside a struct (pyclass) so I'm not sure how to pass the py: Python argument / token into the class, but it seems this is not really necessary as the Python::with_gil approach seems to be a valid approach and wouldn't incur sufficient additional overhead worth any concern.
Beta Was this translation helpful? Give feedback.
All reactions
-
so I'm not sure how to pass the py: Python argument / token into the class,
The GIL token is really just a type-level proof of having acquired the GIL. (It is also a zero-sized type, i.e. it does not require any space to be passed via stack or registers.)
Getting access is automatic for a method in a #[pymethods] block, i.e. you just add py: Python<'_> as an argument and PyO3 will create a token when calling your method. (The Python code does not "see" or have to pass this extra argument, so it does not become part of your Python-visible signature.)
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
In other words, would there be situations where it might be better just to return the vec structure to Python (as a list) and then take care of wrapping the list as a numpy array from within Python directly?
I think this really depends, but that said, it depends only in the same way as if you would implement that function in say pure Python. So if the return value should conceptually be a list and its only some callers which decide to treat it as an array for their purposes, so be it. If having a Vec is only an implementation detail that should not be visible to the caller, do not expose this and turn it into an array.
That said, turning a Vec into a Python list does have a cost, especially for something like Vec<f64> where the corresponding Python data structure might end up boxing the individual elements. The conversion from Vec<f64> to PyArray1<f64> is also not free, but it has a constant cost unrelated to the number of elements, so especially for large collections this should be more efficient.
Are there any drawbacks to calling Python from rust (complexity, performance, portability, etc)?
As an aside, note that your code snippet does not really call "into Python". Python::with_gil is implemented in PyO3 and manages how Rust code uses the GIL. into_pyarray is implemented in rust-numpy and interacts with NumPy's native code to set up a single Python object holding the Vec so that an ndarray pointing into the Vec can be created with that first object as its base. So there is really very little actual Python code involved.
As a general remark, I would say that calling Python functions does always have a significant cost compared to calling a Rust function just because the calling convention is much more flexible and involved. But I think portability is not a concern if you were called from Python in the first place and want to "call back".
As for complexity, there will also often be a cost due to marshalling arguments and return values as well as having a GIL token at hand, but PyO3 tries to make it as simple as possible. In my personal, experience calling back into Python code is not necessary very often, but when it was, that part of it was obvious. For example, because I wanted to make some part of a larger algorithm open to modification via the Python caller passing in a function which my code would call. But a lot of PyO3-written extensions end up converting their Python-passed arguments into "Rust-native" data structures, process them completely from within the Rust ecosystem and only convert the final results back into Python data structures.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
Thank you for this response.
In my case I want to explicitly return a numpy array to the caller of the function, though all of the logic leading up to the return happens on native rust arrays and vecs.
It seems leaving things as they are will work well for my purposes.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1