轉換特徵
PyO3 提供一些方便的特徵,用於在 Python 與 Rust 型別之間進行轉換。
.extract() 與 FromPyObject 特徵
將 Python 物件轉為 Rust 值最簡單的方法是使用 .extract()。若轉換失敗會回傳帶有型別錯誤的 PyResult,因此通常會像這樣使用:
use pyo3::prelude::*;
use pyo3::types::PyList;
fn main() -> PyResult<()> {
Python::attach(|py| {
let list = PyList::new(py, b"foo")?;
let v: Vec<i32> = list.extract()?;
assert_eq!(&v, &[102, 111, 111]);
Ok(())
})
}
這個方法可用於多種 Python 物件型別,並能產生各式各樣的 Rust 型別;你可以在 FromPyObject 的實作清單中查看。
FromPyObject 也可用於你自己的 Rust 型別(包裝成 Python 物件時,見類別章節)。在那裡,為了既能操作可變參照,_又_滿足 Rust 不允許可變參照別名的規則,你必須提取 PyO3 的參照包裝器 PyRef 與 PyRefMut。它們的行為類似 std::cell::RefCell 的參照包裝器,並在執行期確保 Rust 借用是允許的。
衍生 FromPyObject
若成員型別本身實作 FromPyObject,則可為多種結構與列舉自動衍生 FromPyObject。這甚至包含泛型成員 T: FromPyObject。不支援對空列舉、列舉變體與結構進行衍生。
為結構衍生 FromPyObject
The derivation generates code that will attempt to access the attribute my_string on the Python object, i.e. obj.getattr("my_string"), and call extract() on the attribute.
use pyo3::prelude::*;
#[derive(FromPyObject)]
struct RustyStruct {
my_string: String,
}
fn main() -> PyResult<()> {
Python::attach(|py| -> PyResult<()> {
let module = PyModule::from_code(
py,
c"class Foo:
def __init__(self):
self.my_string = 'test'",
c"<string>",
c"",
)?;
let class = module.getattr("Foo")?;
let instance = class.call0()?;
let rustystruct: RustyStruct = instance.extract()?;
assert_eq!(rustystruct.my_string, "test");
Ok(())
})
}
在欄位上設定 #[pyo3(item)] 屬性時,PyO3 會嘗試透過呼叫 Python 物件的 get_item 方法來提取值。
use pyo3::prelude::*;
#[derive(FromPyObject)]
struct RustyStruct {
#[pyo3(item)]
my_string: String,
}
use pyo3::types::PyDict;
fn main() -> PyResult<()> {
Python::attach(|py| -> PyResult<()> {
let dict = PyDict::new(py);
dict.set_item("my_string", "test")?;
let rustystruct: RustyStruct = dict.extract()?;
assert_eq!(rustystruct.my_string, "test");
Ok(())
})
}
傳給 getattr 與 get_item 的引數也可進行設定:
use pyo3::prelude::*;
#[derive(FromPyObject)]
struct RustyStruct {
#[pyo3(item("key"))]
string_in_mapping: String,
#[pyo3(attribute("name"))]
string_attr: String,
}
fn main() -> PyResult<()> {
Python::attach(|py| -> PyResult<()> {
let module = PyModule::from_code(
py,
c"class Foo(dict):
def __init__(self):
self.name = 'test'
self['key'] = 'test2'",
c"<string>",
c"",
)?;
let class = module.getattr("Foo")?;
let instance = class.call0()?;
let rustystruct: RustyStruct = instance.extract()?;
assert_eq!(rustystruct.string_attr, "test");
assert_eq!(rustystruct.string_in_mapping, "test2");
Ok(())
})
}
這會嘗試從 name 屬性提取 string_attr,並從鍵為 "key" 的對映中提取 string_in_mapping。attribute 的引數限制為非空字串常值,而 item 可接受任何實作 ToBorrowedObject 的有效常值。
你可以在結構上使用 #[pyo3(from_item_all)],以 get_item 方法提取每個欄位。此時不能使用 #[pyo3(attribute)],且幾乎不能在任何欄位上使用 #[pyo3(item)]。不過,仍可使用 #[pyo3(item("key"))] 指定欄位的鍵。
use pyo3::prelude::*;
#[derive(FromPyObject)]
#[pyo3(from_item_all)]
struct RustyStruct {
foo: String,
bar: String,
#[pyo3(item("foobar"))]
baz: String,
}
fn main() -> PyResult<()> {
Python::attach(|py| -> PyResult<()> {
let py_dict = py.eval(c"{'foo': 'foo', 'bar': 'bar', 'foobar': 'foobar'}", None, None)?;
let rustystruct: RustyStruct = py_dict.extract()?;
assert_eq!(rustystruct.foo, "foo");
assert_eq!(rustystruct.bar, "bar");
assert_eq!(rustystruct.baz, "foobar");
Ok(())
})
}
為元組結構衍生 FromPyObject
元組結構也受支援,但不允許自訂提取方式。輸入一律視為與 Rust 型別長度相同的 Python tuple,第 n 個欄位會從 Python tuple 的第 n 個項目提取。
use pyo3::prelude::*;
#[derive(FromPyObject)]
struct RustyTuple(String, String);
use pyo3::types::PyTuple;
fn main() -> PyResult<()> {
Python::attach(|py| -> PyResult<()> {
let tuple = PyTuple::new(py, vec!["test", "test2"])?;
let rustytuple: RustyTuple = tuple.extract()?;
assert_eq!(rustytuple.0, "test");
assert_eq!(rustytuple.1, "test2");
Ok(())
})
}
只有單一欄位的元組結構會被視為包裝型別,並在下一節說明。若要覆寫此行為並確保輸入確實為 tuple,請將結構指定為
use pyo3::prelude::*;
#[derive(FromPyObject)]
struct RustyTuple((String,));
use pyo3::types::PyTuple;
fn main() -> PyResult<()> {
Python::attach(|py| -> PyResult<()> {
let tuple = PyTuple::new(py, vec!["test"])?;
let rustytuple: RustyTuple = tuple.extract()?;
assert_eq!((rustytuple.0).0, "test");
Ok(())
})
}
為包裝型別衍生 FromPyObject
pyo3(transparent) 屬性可用於只有一個欄位的結構。這會直接從輸入物件提取(即 obj.extract()),而不是嘗試存取項目或屬性。此行為預設啟用於 newtype 結構與單一欄位的元組變體。
use pyo3::prelude::*;
#[derive(FromPyObject)]
struct RustyTransparentTupleStruct(String);
#[derive(FromPyObject)]
#[pyo3(transparent)]
struct RustyTransparentStruct {
inner: String,
}
use pyo3::types::PyString;
fn main() -> PyResult<()> {
Python::attach(|py| -> PyResult<()> {
let s = PyString::new(py, "test");
let tup: RustyTransparentTupleStruct = s.extract()?;
assert_eq!(tup.0, "test");
let stru: RustyTransparentStruct = s.extract()?;
assert_eq!(stru.inner, "test");
Ok(())
})
}
為列舉衍生 FromPyObject
針對列舉的 FromPyObject 衍生會產生程式碼,依欄位順序嘗試提取各變體。一旦某個變體成功提取,就會回傳該變體。這使得提取 str | int 等 Python union 型別成為可能。
結構衍生的相同自訂與限制也適用於列舉變體:元組變體假設輸入為 Python tuple,結構變體預設以屬性提取欄位,但可用相同方式設定。transparent 屬性可套用於單一欄位的變體。
use pyo3::prelude::*;
#[derive(FromPyObject)]
#[derive(Debug)]
enum RustyEnum<'py> {
Int(usize), // input is a positive int
String(String), // input is a string
IntTuple(usize, usize), // input is a 2-tuple with positive ints
StringIntTuple(String, usize), // input is a 2-tuple with String and int
Coordinates3d {
// needs to be in front of 2d
x: usize,
y: usize,
z: usize,
},
Coordinates2d {
// only gets checked if the input did not have `z`
#[pyo3(attribute("x"))]
a: usize,
#[pyo3(attribute("y"))]
b: usize,
},
#[pyo3(transparent)]
CatchAll(Bound<'py, PyAny>), // This extraction never fails
}
use pyo3::types::{PyBytes, PyString};
fn main() -> PyResult<()> {
Python::attach(|py| -> PyResult<()> {
{
let thing = 42_u8.into_pyobject(py)?;
let rust_thing: RustyEnum<'_> = thing.extract()?;
assert_eq!(
42,
match rust_thing {
RustyEnum::Int(i) => i,
other => unreachable!("Error extracting: {:?}", other),
}
);
}
{
let thing = PyString::new(py, "text");
let rust_thing: RustyEnum<'_> = thing.extract()?;
assert_eq!(
"text",
match rust_thing {
RustyEnum::String(i) => i,
other => unreachable!("Error extracting: {:?}", other),
}
);
}
{
let thing = (32_u8, 73_u8).into_pyobject(py)?;
let rust_thing: RustyEnum<'_> = thing.extract()?;
assert_eq!(
(32, 73),
match rust_thing {
RustyEnum::IntTuple(i, j) => (i, j),
other => unreachable!("Error extracting: {:?}", other),
}
);
}
{
let thing = ("foo", 73_u8).into_pyobject(py)?;
let rust_thing: RustyEnum<'_> = thing.extract()?;
assert_eq!(
(String::from("foo"), 73),
match rust_thing {
RustyEnum::StringIntTuple(i, j) => (i, j),
other => unreachable!("Error extracting: {:?}", other),
}
);
}
{
let module = PyModule::from_code(
py,
c"class Foo(dict):
def __init__(self):
self.x = 0
self.y = 1
self.z = 2",
c"<string>",
c"",
)?;
let class = module.getattr("Foo")?;
let instance = class.call0()?;
let rust_thing: RustyEnum<'_> = instance.extract()?;
assert_eq!(
(0, 1, 2),
match rust_thing {
RustyEnum::Coordinates3d { x, y, z } => (x, y, z),
other => unreachable!("Error extracting: {:?}", other),
}
);
}
{
let module = PyModule::from_code(
py,
c"class Foo(dict):
def __init__(self):
self.x = 3
self.y = 4",
c"<string>",
c"",
)?;
let class = module.getattr("Foo")?;
let instance = class.call0()?;
let rust_thing: RustyEnum<'_> = instance.extract()?;
assert_eq!(
(3, 4),
match rust_thing {
RustyEnum::Coordinates2d { a, b } => (a, b),
other => unreachable!("Error extracting: {:?}", other),
}
);
}
{
let thing = PyBytes::new(py, b"text");
let rust_thing: RustyEnum<'_> = thing.extract()?;
assert_eq!(
b"text",
match rust_thing {
RustyEnum::CatchAll(ref i) => i.cast::<PyBytes>()?.as_bytes(),
other => unreachable!("Error extracting: {:?}", other),
}
);
}
Ok(())
})
}
If none of the enum variants match, a PyTypeError containing the names of the tested variants is returned. The names reported in the error message can be customized through the #[pyo3(annotation = "name")] attribute, e.g. to use conventional Python type names:
use pyo3::prelude::*;
#[derive(FromPyObject)]
#[derive(Debug)]
enum RustyEnum {
#[pyo3(transparent, annotation = "str")]
String(String),
#[pyo3(transparent, annotation = "int")]
Int(isize),
}
fn main() -> PyResult<()> {
Python::attach(|py| -> PyResult<()> {
{
let thing = 42_u8.into_pyobject(py)?;
let rust_thing: RustyEnum = thing.extract()?;
assert_eq!(
42,
match rust_thing {
RustyEnum::Int(i) => i,
other => unreachable!("Error extracting: {:?}", other),
}
);
}
{
let thing = "foo".into_pyobject(py)?;
let rust_thing: RustyEnum = thing.extract()?;
assert_eq!(
"foo",
match rust_thing {
RustyEnum::String(i) => i,
other => unreachable!("Error extracting: {:?}", other),
}
);
}
{
let thing = b"foo".into_pyobject(py)?;
let error = thing.extract::<RustyEnum>().unwrap_err();
assert!(error.is_instance_of::<pyo3::exceptions::PyTypeError>(py));
}
Ok(())
})
}
If the input is neither a string nor an integer, the error message will be: "'<INPUT_TYPE>' is not an instance of 'str | int'".
#[derive(FromPyObject)] Container Attributes
pyo3(transparent)- extract the field directly from the object as
obj.extract()instead ofget_item()orgetattr() - Newtype structs and tuple-variants are treated as transparent per default.
- only supported for single-field structs and enum variants
- extract the field directly from the object as
pyo3(annotation = "name")- changes the name of the failed variant in the generated error message in case of failure.
- e.g.
pyo3("int")reports the variant’s type asint. - only supported for enum variants
pyo3(rename_all = "...")- renames all attributes/item keys according to the specified renaming rule
- Possible values are: “camelCase”, “kebab-case”, “lowercase”, “PascalCase”, “SCREAMING-KEBAB-CASE”, “SCREAMING_SNAKE_CASE”, “snake_case”, “UPPERCASE”.
- fields with an explicit renaming via
attribute(...)/item(...)are not affected
#[derive(FromPyObject)] Field Attributes
pyo3(attribute),pyo3(attribute("name"))- retrieve the field from an attribute, possibly with a custom name specified as an argument
- argument must be a string-literal.
pyo3(item),pyo3(item("key"))- retrieve the field from a mapping, possibly with the custom key specified as an argument.
- can be any literal that implements
ToBorrowedObject
pyo3(from_py_with = ...)- apply a custom function to convert the field from Python the desired Rust type.
- the argument must be the path to the function.
- the function signature must be
fn(&Bound<PyAny>) -> PyResult<T>whereTis the Rust type of the argument.
pyo3(default),pyo3(default = ...)- if the argument is set, uses the given default value.
- in this case, the argument must be a Rust expression returning a value of the desired Rust type.
- if the argument is not set,
Default::defaultis used. - note that the default value is only used if the field is not set. If the field is set and the conversion function from Python to Rust fails, an exception is raised and the default value is not used.
- this attribute is only supported on named fields.
For example, the code below applies the given conversion function on the "value" dict item to compute its length or fall back to the type default value (0):
use pyo3::prelude::*;
#[derive(FromPyObject)]
struct RustyStruct {
#[pyo3(item("value"), default, from_py_with = Bound::<'_, PyAny>::len)]
len: usize,
#[pyo3(item)]
other: usize,
}
use pyo3::types::PyDict;
fn main() -> PyResult<()> {
Python::attach(|py| -> PyResult<()> {
// Filled case
let dict = PyDict::new(py);
dict.set_item("value", (1,)).unwrap();
dict.set_item("other", 1).unwrap();
let result = dict.extract::<RustyStruct>()?;
assert_eq!(result.len, 1);
assert_eq!(result.other, 1);
// Empty case
let dict = PyDict::new(py);
dict.set_item("other", 1).unwrap();
let result = dict.extract::<RustyStruct>()?;
assert_eq!(result.len, 0);
assert_eq!(result.other, 1);
Ok(())
})
}
⚠ Phase-Out of FromPyObject blanket implementation for cloneable PyClasses ⚠
Historically PyO3 has provided a blanket implementation for #[pyclass] types that also implement Clone, to allow extraction of such types by value. Over time this has turned out problematic for a few reasons, the major one being the prevention of custom conversions by downstream crates if their type is Clone. Over the next few releases the blanket implementation is gradually phased out, and eventually replaced by an opt-in option. As a first step of this migration a new skip_from_py_object option for #[pyclass] was introduced, to opt-out of the blanket implementation and allow downstream users to provide their own implementation:
#![allow(dead_code)]
use pyo3::prelude::*;
#[pyclass(skip_from_py_object)] // opt-out of the PyO3 FromPyObject blanket
#[derive(Clone)]
struct Number(i32);
impl<'py> FromPyObject<'_, 'py> for Number {
type Error = PyErr;
fn extract(obj: pyo3::Borrowed<'_, 'py, pyo3::PyAny>) -> Result<Self, Self::Error> {
if let Ok(obj) = obj.cast::<Self>() { // first try extraction via class object
Ok(obj.borrow().clone())
} else {
obj.extract::<i32>().map(Self) // otherwise try integer directly
}
}
}
As a second step the from_py_object option was introduced. This option also opts-out of the blanket implementation and instead generates a custom FromPyObject implementation for the pyclass which is functionally equivalent to the blanket.
IntoPyObject
The IntoPyObject trait defines the to-python conversion for a Rust type. All types in PyO3 implement this trait, as does a #[pyclass] which doesn’t use extends.
This trait defines a single method, into_pyobject(), which returns a Result with Ok and Err types depending on the input value. For convenience, there is a companion IntoPyObjectExt trait which adds methods such as into_py_any() which converts the Ok and Err types to commonly used types (in the case of into_py_any(), Py<PyAny> and PyErr respectively).
Occasionally you may choose to implement this for custom types which are mapped to Python types without having a unique python type.
derive macro
IntoPyObject can be implemented using our derive macro. Both structs and enums are supported.
structs will turn into a PyDict using the field names as keys, tuple structs will turn convert into PyTuple with the fields in declaration order.
#![allow(dead_code)]
use pyo3::prelude::*;
use std::collections::HashMap;
use std::hash::Hash;
// structs convert into `PyDict` with field names as keys
#[derive(IntoPyObject)]
struct Struct {
count: usize,
obj: Py<PyAny>,
}
// tuple structs convert into `PyTuple`
// lifetimes and generics are supported, the impl will be bounded by
// `K: IntoPyObject, V: IntoPyObject`
#[derive(IntoPyObject)]
struct Tuple<'a, K: Hash + Eq, V>(&'a str, HashMap<K, V>);
For structs with a single field (newtype pattern) the #[pyo3(transparent)] option can be used to forward the implementation to the inner type.
#![allow(dead_code)]
use pyo3::prelude::*;
// newtype tuple structs are implicitly `transparent`
#[derive(IntoPyObject)]
struct TransparentTuple(Py<PyAny>);
#[derive(IntoPyObject)]
#[pyo3(transparent)]
struct TransparentStruct<'py> {
inner: Bound<'py, PyAny>, // `'py` lifetime will be used as the Python lifetime
}
For enums each variant is converted according to the rules for structs above.
#![allow(dead_code)]
use pyo3::prelude::*;
use std::collections::HashMap;
use std::hash::Hash;
#[derive(IntoPyObject)]
enum Enum<'a, 'py, K: Hash + Eq, V> { // enums are supported and convert using the same
TransparentTuple(Py<PyAny>), // rules on the variants as the structs above
#[pyo3(transparent)]
TransparentStruct { inner: Bound<'py, PyAny> },
Tuple(&'a str, HashMap<K, V>),
Struct { count: usize, obj: Py<PyAny> }
}
Additionally IntoPyObject can be derived for a reference to a struct or enum using the IntoPyObjectRef derive macro. All the same rules from above apply as well.
#[derive(IntoPyObject)]/#[derive(IntoPyObjectRef)] Field Attributes
pyo3(into_py_with = ...)-
apply a custom function to convert the field from Rust into Python.
-
the argument must be the function identifier
-
the function signature must be
fn(Cow<'_, T>, Python<'py>) -> PyResult<Bound<'py, PyAny>>whereTis the Rust type of the argument.#[derive(IntoPyObject)]will invoke the function withCow::Owned#[derive(IntoPyObjectRef)]will invoke the function withCow::Borrowed
use pyo3::prelude::*; use pyo3::IntoPyObjectExt; use std::borrow::Cow; #[derive(Clone)] struct NotIntoPy(usize); #[derive(IntoPyObject, IntoPyObjectRef)] struct MyStruct { #[pyo3(into_py_with = convert)] not_into_py: NotIntoPy, } /// Convert `NotIntoPy` into Python fn convert<'py>(not_into_py: Cow<'_, NotIntoPy>, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> { not_into_py.0.into_bound_py_any(py) }
-
manual implementation
If the derive macro is not suitable for your use case, IntoPyObject can be implemented manually as demonstrated below.
use pyo3::prelude::*;
#[allow(dead_code)]
struct MyPyObjectWrapper(Py<PyAny>);
impl<'py> IntoPyObject<'py> for MyPyObjectWrapper {
type Target = PyAny; // the Python type
type Output = Bound<'py, Self::Target>; // in most cases this will be `Bound`
type Error = std::convert::Infallible; // the conversion error type, has to be convertible to `PyErr`
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
Ok(self.0.into_bound(py))
}
}
// equivalent to former `ToPyObject` implementations
impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper {
type Target = PyAny;
type Output = Borrowed<'a, 'py, Self::Target>; // `Borrowed` can be used to optimized reference counting
type Error = std::convert::Infallible;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
Ok(self.0.bind_borrowed(py))
}
}
BoundObject for conversions that may be Bound or Borrowed
IntoPyObject::into_py_object returns either Bound or Borrowed depending on the implementation for a concrete type. For example, the IntoPyObject implementation for u32 produces a Bound<'py, PyInt> and the bool implementation produces a Borrowed<'py, 'py, PyBool>:
use pyo3::prelude::*;
use pyo3::IntoPyObject;
use pyo3::types::{PyBool, PyInt};
let ints: Vec<u32> = vec![1, 2, 3, 4];
let bools = vec![true, false, false, true];
Python::attach(|py| {
let ints_as_pyint: Vec<Bound<'_, PyInt>> = ints
.iter()
.map(|x| Ok(x.into_pyobject(py)?))
.collect::<PyResult<_>>()
.unwrap();
let bools_as_pybool: Vec<Borrowed<'_, '_, PyBool>> = bools
.iter()
.map(|x| Ok(x.into_pyobject(py)?))
.collect::<PyResult<_>>()
.unwrap();
});
In this example if we wanted to combine ints_as_pyints and bools_as_pybool into a single Vec<Py<PyAny>> to return from the Python::attach closure, we would have to manually convert the concrete types for the smart pointers and the python types.
Instead, we can write a function that generically converts vectors of either integers or bools into a vector of Py<PyAny> using the BoundObject trait:
use pyo3::prelude::*;
use pyo3::BoundObject;
use pyo3::IntoPyObject;
let bools = vec![true, false, false, true];
let ints = vec![1, 2, 3, 4];
fn convert_to_vec_of_pyobj<'py, T>(py: Python<'py>, the_vec: Vec<T>) -> PyResult<Vec<Py<PyAny>>>
where
T: IntoPyObject<'py> + Copy
{
the_vec.iter()
.map(|x| {
Ok(
// Note: the below is equivalent to `x.into_py_any()`
// from the `IntoPyObjectExt` trait
x.into_pyobject(py)
.map_err(Into::into)?
.into_any()
.unbind()
)
})
.collect()
}
let vec_of_pyobjs: Vec<Py<PyAny>> = Python::attach(|py| {
let mut bools_as_pyany = convert_to_vec_of_pyobj(py, bools).unwrap();
let mut ints_as_pyany = convert_to_vec_of_pyobj(py, ints).unwrap();
let mut result: Vec<Py<PyAny>> = vec![];
result.append(&mut bools_as_pyany);
result.append(&mut ints_as_pyany);
result
});
In the example above we used BoundObject::into_any and BoundObject::unbind to manipulate the python types and smart pointers into the result type we wanted to produce from the function.