From 2813d2e6c9d31ab016e29d0e2627a13eaa4ac960 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Sun, 17 May 2020 23:13:56 +0100 Subject: [PATCH] Add conversion table to guide --- guide/src/conversions.md | 106 +++++++++++++++++++++++++++++++++++++-- guide/src/types.md | 6 ++- tests/test_bytes.rs | 32 +++++++++--- 3 files changed, 132 insertions(+), 12 deletions(-) diff --git a/guide/src/conversions.md b/guide/src/conversions.md index 2d49f780..ce8f7c34 100644 --- a/guide/src/conversions.md +++ b/guide/src/conversions.md @@ -1,8 +1,104 @@ # Type Conversions +In this portion of the guide we'll talk about the mapping of Python types to Rust types offered by PyO3, as well as the traits available to perform conversions between them. + +## Mapping of Rust types to Python types + +When writing functions callable from Python (such as a `#[pyfunction]` or in a `#[pymethods]` block), the trait `FromPyObject` is required for function arguments, and `IntoPy` is required for function return values. + +Consult the tables in the following section to find the Rust types provided by PyO3 which implement these traits. + +### Argument Types + +When accepting a function argument, it is possible to either use Rust library types or PyO3's Python-native types. (See the next section for discussion on when to use each.) + +The table below contains the Python type and the corresponding function argument types that will accept them: + +| Python | Rust | Rust (Python-native) | +| ------------- |:-------------------------------:|:--------------------:| +| `object` | - | `&PyAny` | +| `str` | `String`, `Cow`, `&str` | `&PyUnicode` | +| `bytes` | `Vec`, `&[u8]` | `&PyBytes` | +| `bool` | `bool` | `&PyBool` | +| `int` | Any integer type (`i32`, `u32`, `usize`, etc) | `&PyLong` | +| `float` | `f32`, `f64` | `&PyFloat` | +| `complex` | `num_complex::Complex`[^1] | `&PyComplex` | +| `list[T]` | `Vec` | `&PyList` | +| `dict[K, V]` | `HashMap`, `BTreeMap` | `&PyDict` | +| `tuple[T, U]` | `(T, U)`, `Vec` | `&PyTuple` | +| `set[T]` | `HashSet`, `BTreeSet` | `&PySet` | +| `frozenset[T]` | `HashSet`, `BTreeSet` | `&PyFrozenSet` | +| `bytearray` | `Vec` | `&PyByteArray` | +| `slice` | - | `&PySlice` | +| `type` | - | `&PyType` | +| `module` | - | `&PyModule` | +| `datetime.datetime` | - | `&PyDateTime` | +| `datetime.date` | - | `&PyDate` | +| `datetime.time` | - | `&PyTime` | +| `datetime.tzinfo` | - | `&PyTzInfo` | +| `datetime.timedelta` | - | `&PyDelta` | +| `typing.Optional[T]` | `Option` | - | +| `typing.Sequence[T]` | `Vec` | `&PySequence` | +| `typing.Iterator[Any]` | - | `&PyIterator` | + +There are also a few special types related to the GIL and Rust-defined `#[pyclass]`es which may come in useful: + +| What | Description | +| ------------- | ------------------------------- | +| `Python` | A GIL token, used to pass to PyO3 constructors to prove ownership of the GIL | +| `PyObject` | A Python object isolated from the GIL lifetime. This can be sent to other threads. To call Python APIs using this object, it must be used with `AsPyRef::as_ref` to get a `&PyAny` reference. | +| `Py` | Same as above, for a specific Python type or `#[pyclass]` T. | +| `&PyCell` | A `#[pyclass]` value owned by Python. | +| `PyRef` | A `#[pyclass]` borrowed immutably. | +| `PyRefMut` | A `#[pyclass]` borrowed mutably. | + +For more detail on accepting `#[pyclass]` values as function arguments, see [the section of this guide on Python Classes](class.md). + +#### Using Rust library types vs Python-native types + +Using Rust library types as function arguments will incur a conversion cost compared to using the Python-native types. Using the Python-native types is almost zero-cost (they just require a type check similar to the Python builtin function `isinstance()`). + +However, once that conversion cost has been paid, the Rust standard library types offer a number of benefits: +- You can write functionality in native-speed Rust code (free of Python's runtime costs). +- You get better interoperability with the rest of the Rust ecosystem. +- You can use `Python::allow_threads` to release the Python GIL and let other Python threads make progress while your Rust code is executing. +- You also benefit from stricter type checking. For example you can specify `Vec`, which will only accept a Python `list` containing integers. The Python-native equivalent, `&PyList`, would accept a Python `list` containing Python objects of any type. + +For most PyO3 usage the conversion cost is worth paying to get these benefits. As always, if you're not sure it's worth it in your case, benchmark it! + +### Returning Rust values to Python + +When returning values from functions callable from Python, Python-native types (`&PyAny`, `&PyDict` etc.) can be used with zero cost. + +Because these types are references, in some situations the Rust compiler may ask for lifetime annotations. If this is the case, you should use `Py`, `Py` etc. instead - which are also zero-cost and can be created from the native types with an `.into()` conversion. + +If your function is fallible, it should return `PyResult`, which will raise a `Python` exception if the `Err` variant is returned. + +Finally, the following Rust types are also able to convert to Python as return values: + +| Rust type | Resulting Python Type | +| ------------- |:-------------------------------:| +| `String` | `str` | +| `&str` | `str` | +| `bool` | `bool` | +| Any integer type (`i32`, `u32`, `usize`, etc) | `int` | +| `f32`, `f64` | `float` | +| `Option` | `Optional[T]` | +| `(T, U)` | `Tuple[T, U]` | +| `Vec` | `List[T]` | +| `HashMap` | `Dict[K, V]` | +| `BTreeMap` | `Dict[K, V]` | +| `HashSet` | `Set[T]` | +| `BTreeSet` | `Set[T]` | +| `&PyCell` | `T` | +| `PyRef` | `T` | +| `PyRefMut` | `T` | + +## Traits + PyO3 provides some handy traits to convert between Python types and Rust types. -## `.extract()` and the `FromPyObject` trait +### `.extract()` and the `FromPyObject` trait The easiest way to convert a Python object to a Rust value is using `.extract()`. It returns a `PyResult` with a type error if the conversion @@ -24,14 +120,14 @@ and [`PyRefMut`]. They work like the reference wrappers of `std::cell::RefCell` and ensure (at runtime) that Rust borrows are allowed. -## The `ToPyObject` trait +### The `ToPyObject` trait [`ToPyObject`] is a conversion trait that allows various objects to be converted into [`PyObject`]. `IntoPy` serves the same purpose, except that it consumes `self`. -## `*args` and `**kwargs` for Python object calls +### `*args` and `**kwargs` for Python object calls There are several ways how to pass positional and keyword arguments to a Python object call. [`PyAny`] provides two methods: @@ -120,7 +216,7 @@ fn main() { } ``` -## `FromPy` and `IntoPy` +### `FromPy` and `IntoPy` Many conversions in PyO3 can't use `std::convert::From` because they need a GIL token. The [`FromPy`] trait offers an `from_py` method that works just like `from`, except for taking a `Python<'_>` argument. @@ -142,3 +238,5 @@ Eventually, traits such as [`ToPyObject`] will be replaced by this trait and a [ [`PyRef`]: https://pyo3.rs/master/doc/pyo3/pycell/struct.PyRef.html [`PyRefMut`]: https://pyo3.rs/master/doc/pyo3/pycell/struct.PyRefMut.html + +[^1]: Requires the `num-complex` optional feature. diff --git a/guide/src/types.md b/guide/src/types.md index d510a3bf..cb36c31a 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -94,6 +94,9 @@ arguments and intermediate values. These types all implement `Deref`, so they all expose the same methods which can be found on `PyAny`. +To see all Python types exposed by `PyO3` you should consult the +[`pyo3::types`] module. + **Conversions:** ```rust @@ -232,7 +235,6 @@ borrows, analog to `Ref` and `RefMut` used by `RefCell`. on types like `Py` and `PyAny` to get a reference quickly. - ## Related traits and types ### `PyClass` @@ -245,9 +247,9 @@ usually defined using the `#[pyclass]` macro. This trait marks structs that mirror native Python types, such as `PyList`. - [eval]: https://docs.rs/pyo3/latest/pyo3/struct.Python.html#method.eval [clone_ref]: https://docs.rs/pyo3/latest/pyo3/struct.PyObject.html#method.clone_ref +[pyo3::types]: https://pyo3.rs/master/doc/pyo3/types/index.html [PyAny]: https://docs.rs/pyo3/latest/pyo3/types/struct.PyAny.html [PyList_append]: https://docs.rs/pyo3/latest/pyo3/types/struct.PyList.html#method.append [RefCell]: https://doc.rust-lang.org/std/cell/struct.RefCell.html diff --git a/tests/test_bytes.rs b/tests/test_bytes.rs index cffd6053..a48c05fc 100644 --- a/tests/test_bytes.rs +++ b/tests/test_bytes.rs @@ -1,4 +1,5 @@ use pyo3::prelude::*; +use pyo3::types::PyBytes; use pyo3::wrap_pyfunction; mod common; @@ -13,10 +14,29 @@ fn test_pybytes_bytes_conversion() { let gil = Python::acquire_gil(); let py = gil.python(); - let bytes_pybytes_conversion = wrap_pyfunction!(bytes_pybytes_conversion)(py); - py_assert!( - py, - bytes_pybytes_conversion, - "bytes_pybytes_conversion(b'Hello World') == b'Hello World'" - ); + let f = wrap_pyfunction!(bytes_pybytes_conversion)(py); + py_assert!(py, f, "f(b'Hello World') == b'Hello World'"); +} + +#[pyfunction] +fn bytes_vec_conversion(py: Python, bytes: Vec) -> &PyBytes { + PyBytes::new(py, bytes.as_slice()) +} + +#[test] +fn test_pybytes_vec_conversion() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let f = wrap_pyfunction!(bytes_vec_conversion)(py); + py_assert!(py, f, "f(b'Hello World') == b'Hello World'"); +} + +#[test] +fn test_bytearray_vec_conversion() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let f = wrap_pyfunction!(bytes_vec_conversion)(py); + py_assert!(py, f, "f(bytearray(b'Hello World')) == b'Hello World'"); }