Merge pull request #2156 from mejrs/object
Add examples for object and numeric protocols
This commit is contained in:
commit
f379854c50
|
@ -8,6 +8,8 @@
|
|||
- [Python Functions](function.md)
|
||||
- [Python Classes](class.md)
|
||||
- [Class customizations](class/protocols.md)
|
||||
- [Basic object customization](class/object.md)
|
||||
- [Emulating numeric types](class/numeric.md)
|
||||
- [Emulating callable objects](class/call.md)
|
||||
- [Type Conversions](conversions.md)
|
||||
- [Mapping of Rust types to Python types](conversions/tables.md)]
|
||||
|
|
|
@ -53,31 +53,31 @@ After these steps you are ready to annotate your code!
|
|||
|
||||
The `#[cfg]` flags added by `pyo3-build-cfg` can be combined with all of Rust's logic in the `#[cfg]` attribute to create very precise conditional code generation. The following are some common patterns implemented using these flags:
|
||||
|
||||
```
|
||||
```rust,ignore
|
||||
#[cfg(Py_3_7)]
|
||||
```
|
||||
|
||||
This `#[cfg]` marks code that will only be present on Python 3.7 and upwards. There are similar options `Py_3_8`, `Py_3_9`, `Py_3_10` and so on for each minor version.
|
||||
|
||||
```
|
||||
```rust,ignore
|
||||
#[cfg(not(Py_3_7))]
|
||||
```
|
||||
|
||||
This `#[cfg]` marks code that will only be present on Python versions before (but not including) Python 3.7.
|
||||
|
||||
```
|
||||
```rust,ignore
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
```
|
||||
|
||||
This `#[cfg]` marks code that is only available when building for the unlimited Python API (i.e. PyO3's `abi3` feature is not enabled). This might be useful if you want to ship your extension module as an `abi3` wheel and also allow users to compile it from source to make use of optimizations only possible with the unlimited API.
|
||||
|
||||
```
|
||||
```rust,ignore
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
```
|
||||
|
||||
This `#[cfg]` marks code which is available when running Python 3.9 or newer, or when using the unlimited API with an older Python version. Patterns like this are commonly seen on Python APIs which were added to the limited Python API in a specific minor version.
|
||||
|
||||
```
|
||||
```rust,ignore
|
||||
#[cfg(PyPy)]
|
||||
```
|
||||
|
||||
|
|
|
@ -23,11 +23,24 @@ This chapter will discuss the functionality and configuration these attributes o
|
|||
To define a custom Python class, add the `#[pyclass]` attribute to a Rust struct or a fieldless enum.
|
||||
```rust
|
||||
# #![allow(dead_code)]
|
||||
# use pyo3::prelude::*;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[pyclass]
|
||||
struct MyClass {
|
||||
# #[pyo3(get)]
|
||||
num: i32,
|
||||
struct Integer{
|
||||
inner: i32
|
||||
}
|
||||
|
||||
// A "tuple" struct
|
||||
#[pyclass]
|
||||
struct Number(i32);
|
||||
|
||||
// PyO3 supports custom discriminants in enums
|
||||
#[pyclass]
|
||||
enum HttpResponse {
|
||||
Ok = 200,
|
||||
NotFound = 404,
|
||||
Teapot = 418,
|
||||
// ...
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
|
@ -41,20 +54,59 @@ Because Python objects are freely shared between threads by the Python interpret
|
|||
|
||||
The above example generates implementations for [`PyTypeInfo`], [`PyTypeObject`], and [`PyClass`] for `MyClass` and `MyEnum`. To see these generated implementations, refer to the [implementation details](#implementation-details) at the end of this chapter.
|
||||
|
||||
## Adding the class to a module
|
||||
## Constructor
|
||||
|
||||
Custom Python classes can then be added to a module using `add_class()`.
|
||||
By default it is not possible to create an instance of a custom class from Python code.
|
||||
To declare a constructor, you need to define a method and annotate it with the `#[new]`
|
||||
attribute. Only Python's `__new__` method can be specified, `__init__` is not available.
|
||||
|
||||
```rust
|
||||
# use pyo3::prelude::*;
|
||||
# #[pyclass]
|
||||
# struct MyClass {
|
||||
# #[allow(dead_code)]
|
||||
# num: i32,
|
||||
# }
|
||||
# struct Number(i32);
|
||||
#
|
||||
#[pymethods]
|
||||
impl Number {
|
||||
#[new]
|
||||
fn new(value: i32) -> Self {
|
||||
Number(value)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, if your `new` method may fail you can return `PyResult<Self>`.
|
||||
|
||||
```rust
|
||||
# use pyo3::prelude::*;
|
||||
# #[pyclass]
|
||||
# struct Number(i32);
|
||||
#
|
||||
#[pymethods]
|
||||
impl Number {
|
||||
#[new]
|
||||
fn new(value: i32) -> Self {
|
||||
Number(value)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If no method marked with `#[new]` is declared, object instances can only be
|
||||
created from Rust, but not from Python.
|
||||
|
||||
For arguments, see the `Method arguments` section below.
|
||||
|
||||
## Adding the class to a module
|
||||
|
||||
The next step is to create the module initializer and add our class to it
|
||||
|
||||
```rust
|
||||
# use pyo3::prelude::*;
|
||||
# #[pyclass]
|
||||
# struct Number(i32);
|
||||
#
|
||||
#[pymodule]
|
||||
fn mymodule(_py: Python, m: &PyModule) -> PyResult<()> {
|
||||
m.add_class::<MyClass>()?;
|
||||
fn my_module(_py: Python, m: &PyModule) -> PyResult<()> {
|
||||
m.add_class::<Number>()?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
@ -152,52 +204,6 @@ If a custom class contains references to other Python objects that can be collec
|
|||
* `module="XXX"` - Set the name of the module the class will be shown as defined in. If not given, the class
|
||||
will be a virtual member of the `builtins` module.
|
||||
|
||||
## Constructor
|
||||
|
||||
By default it is not possible to create an instance of a custom class from Python code.
|
||||
To declare a constructor, you need to define a method and annotate it with the `#[new]`
|
||||
attribute. Only Python's `__new__` method can be specified, `__init__` is not available.
|
||||
|
||||
```rust
|
||||
# use pyo3::prelude::*;
|
||||
#[pyclass]
|
||||
struct MyClass {
|
||||
# #[allow(dead_code)]
|
||||
num: i32,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl MyClass {
|
||||
#[new]
|
||||
fn new(num: i32) -> Self {
|
||||
MyClass { num }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, if your `new` method may fail you can return `PyResult<Self>`.
|
||||
```rust
|
||||
# use pyo3::prelude::*;
|
||||
#[pyclass]
|
||||
struct MyClass {
|
||||
# #[allow(dead_code)]
|
||||
num: i32,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl MyClass {
|
||||
#[new]
|
||||
fn new(num: i32) -> PyResult<Self> {
|
||||
Ok(MyClass { num })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If no method marked with `#[new]` is declared, object instances can only be
|
||||
created from Rust, but not from Python.
|
||||
|
||||
For arguments, see the `Method arguments` section below.
|
||||
|
||||
### Return type
|
||||
|
||||
Generally, `#[new]` method have to return `T: Into<PyClassInitializer<Self>>` or
|
||||
|
|
449
guide/src/class/numeric.md
Normal file
449
guide/src/class/numeric.md
Normal file
|
@ -0,0 +1,449 @@
|
|||
# Emulating numeric types
|
||||
|
||||
At this point we have a `Number` class that we can't actually do any math on!
|
||||
|
||||
Before proceeding, we should think about how we want to handle overflows. There are three obvious solutions:
|
||||
- We can have infinite precision just like Python's `int`. However that would be quite boring - we'd
|
||||
be reinventing the wheel.
|
||||
- We can raise exceptions whenever `Number` overflows, but that makes the API painful to use.
|
||||
- We can wrap around the boundary of `i32`. This is the approach we'll take here. To do that we'll just forward to `i32`'s
|
||||
`wrapping_*` methods.
|
||||
|
||||
### Fixing our constructor
|
||||
|
||||
Let's address the first overflow, in `Number`'s constructor:
|
||||
|
||||
```python
|
||||
from my_module import Number
|
||||
|
||||
n = Number(1 << 1337)
|
||||
```
|
||||
|
||||
```text
|
||||
Traceback (most recent call last):
|
||||
File "example.py", line 3, in <module>
|
||||
n = Number(1 << 1337)
|
||||
OverflowError: Python int too large to convert to C long
|
||||
```
|
||||
|
||||
Instead of relying on the default [`FromPyObject`] extraction to parse arguments, we can specify our
|
||||
own extraction function, using the `#[pyo3(from_py_with = "...")]` attribute. Unfortunately PyO3
|
||||
doesn't provide a way to wrap Python integers out of the box, but we can do a Python call to mask it
|
||||
and cast it to an `i32`.
|
||||
|
||||
```rust
|
||||
# #![allow(dead_code)]
|
||||
use pyo3::prelude::*;
|
||||
|
||||
fn wrap(obj: &PyAny) -> Result<i32, PyErr> {
|
||||
let val = obj.call_method1("__and__", (0xFFFFFFFF_u32,))?;
|
||||
let val: u32 = val.extract()?;
|
||||
// 👇 This intentionally overflows!
|
||||
Ok(val as i32)
|
||||
}
|
||||
```
|
||||
We also add documentation, via `///` comments and the `#[pyo3(text_signature = "...")]` attribute, both of which are visible to Python users.
|
||||
|
||||
```rust
|
||||
# #![allow(dead_code)]
|
||||
use pyo3::prelude::*;
|
||||
|
||||
fn wrap(obj: &PyAny) -> Result<i32, PyErr> {
|
||||
let val = obj.call_method1("__and__", (0xFFFFFFFF_u32,))?;
|
||||
let val: u32 = val.extract()?;
|
||||
Ok(val as i32)
|
||||
}
|
||||
|
||||
/// Did you ever hear the tragedy of Darth Signed The Overfloweth? I thought not.
|
||||
/// It's not a story C would tell you. It's a Rust legend.
|
||||
#[pyclass(module = "my_module")]
|
||||
#[pyo3(text_signature = "(int)")]
|
||||
struct Number(i32);
|
||||
|
||||
#[pymethods]
|
||||
impl Number {
|
||||
#[new]
|
||||
fn new(#[pyo3(from_py_with = "wrap")] value: i32) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
With that out of the way, let's implement some operators:
|
||||
```rust
|
||||
use std::convert::TryInto;
|
||||
use pyo3::exceptions::{PyZeroDivisionError, PyValueError};
|
||||
|
||||
# use pyo3::prelude::*;
|
||||
#
|
||||
# #[pyclass]
|
||||
# struct Number(i32);
|
||||
#
|
||||
#[pymethods]
|
||||
impl Number {
|
||||
fn __add__(&self, other: &Self) -> Self {
|
||||
Self(self.0.wrapping_add(other.0))
|
||||
}
|
||||
|
||||
fn __sub__(&self, other: &Self) -> Self {
|
||||
Self(self.0.wrapping_sub(other.0))
|
||||
}
|
||||
|
||||
fn __mul__(&self, other: &Self) -> Self {
|
||||
Self(self.0.wrapping_mul(other.0))
|
||||
}
|
||||
|
||||
fn __truediv__(&self, other: &Self) -> PyResult<Self> {
|
||||
match self.0.checked_div(other.0) {
|
||||
Some(i) => Ok(Self(i)),
|
||||
None => Err(PyZeroDivisionError::new_err("division by zero")),
|
||||
}
|
||||
}
|
||||
|
||||
fn __floordiv__(&self, other: &Self) -> PyResult<Self> {
|
||||
match self.0.checked_div(other.0) {
|
||||
Some(i) => Ok(Self(i)),
|
||||
None => Err(PyZeroDivisionError::new_err("division by zero")),
|
||||
}
|
||||
}
|
||||
|
||||
fn __rshift__(&self, other: &Self) -> PyResult<Self> {
|
||||
match other.0.try_into() {
|
||||
Ok(rhs) => Ok(Self(self.0.wrapping_shr(rhs))),
|
||||
Err(_) => Err(PyValueError::new_err("negative shift count")),
|
||||
}
|
||||
}
|
||||
|
||||
fn __lshift__(&self, other: &Self) -> PyResult<Self> {
|
||||
match other.0.try_into() {
|
||||
Ok(rhs) => Ok(Self(self.0.wrapping_shl(rhs))),
|
||||
Err(_) => Err(PyValueError::new_err("negative shift count")),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Unary arithmethic operations
|
||||
|
||||
```rust
|
||||
# use pyo3::prelude::*;
|
||||
#
|
||||
# #[pyclass]
|
||||
# struct Number(i32);
|
||||
#
|
||||
#[pymethods]
|
||||
impl Number {
|
||||
fn __pos__(slf: PyRef<Self>) -> PyRef<Self> {
|
||||
slf
|
||||
}
|
||||
|
||||
fn __neg__(&self) -> Self {
|
||||
Self(-self.0)
|
||||
}
|
||||
|
||||
fn __abs__(&self) -> Self {
|
||||
Self(self.0.abs())
|
||||
}
|
||||
|
||||
fn __invert__(&self) -> Self {
|
||||
Self(!self.0)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Support for the `complex()`, `int()` and `float()` built-in functions.
|
||||
|
||||
```rust
|
||||
# use pyo3::prelude::*;
|
||||
#
|
||||
# #[pyclass]
|
||||
# struct Number(i32);
|
||||
#
|
||||
use pyo3::types::PyComplex;
|
||||
|
||||
#[pymethods]
|
||||
impl Number {
|
||||
fn __int__(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn __float__(&self) -> f64 {
|
||||
self.0 as f64
|
||||
}
|
||||
|
||||
fn __complex__<'py>(&self, py: Python<'py>) -> &'py PyComplex {
|
||||
PyComplex::from_doubles(py, self.0 as f64, 0.0)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We do not implement the in-place operations like `__iadd__` because we do not wish to mutate `Number`.
|
||||
Similarly we're not interested in supporting operations with different types, so we do not implement
|
||||
the reflected operations like `__radd__` either.
|
||||
|
||||
Now Python can use our `Number` class:
|
||||
|
||||
```python
|
||||
from my_module import Number
|
||||
|
||||
def hash_djb2(s: str):
|
||||
'''
|
||||
A version of Daniel J. Bernstein's djb2 string hashing algorithm
|
||||
Like many hashing algorithms, it relies on integer wrapping.
|
||||
'''
|
||||
|
||||
n = Number(0)
|
||||
five = Number(5)
|
||||
|
||||
for x in s:
|
||||
n = Number(ord(x)) + ((n << five) - n)
|
||||
return n
|
||||
|
||||
assert hash_djb2('l50_50') == Number(-1152549421)
|
||||
```
|
||||
|
||||
### Final code
|
||||
|
||||
```rust
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::convert::TryInto;
|
||||
|
||||
use pyo3::exceptions::{PyValueError, PyZeroDivisionError};
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::class::basic::CompareOp;
|
||||
use pyo3::types::PyComplex;
|
||||
|
||||
fn wrap(obj: &PyAny) -> Result<i32, PyErr> {
|
||||
let val = obj.call_method1("__and__", (0xFFFFFFFF_u32,))?;
|
||||
let val: u32 = val.extract()?;
|
||||
Ok(val as i32)
|
||||
}
|
||||
/// Did you ever hear the tragedy of Darth Signed The Overfloweth? I thought not.
|
||||
/// It's not a story C would tell you. It's a Rust legend.
|
||||
#[pyclass(module = "my_module")]
|
||||
#[pyo3(text_signature = "(int)")]
|
||||
struct Number(i32);
|
||||
|
||||
#[pymethods]
|
||||
impl Number {
|
||||
#[new]
|
||||
fn new(#[pyo3(from_py_with = "wrap")] value: i32) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("Number({})", self.0)
|
||||
}
|
||||
|
||||
fn __str__(&self) -> String {
|
||||
self.0.to_string()
|
||||
}
|
||||
|
||||
fn __hash__(&self) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
self.0.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult<bool> {
|
||||
match op {
|
||||
CompareOp::Lt => Ok(self.0 < other.0),
|
||||
CompareOp::Le => Ok(self.0 <= other.0),
|
||||
CompareOp::Eq => Ok(self.0 == other.0),
|
||||
CompareOp::Ne => Ok(self.0 != other.0),
|
||||
CompareOp::Gt => Ok(self.0 > other.0),
|
||||
CompareOp::Ge => Ok(self.0 >= other.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn __bool__(&self) -> bool {
|
||||
self.0 != 0
|
||||
}
|
||||
|
||||
fn __add__(&self, other: &Self) -> Self {
|
||||
Self(self.0.wrapping_add(other.0))
|
||||
}
|
||||
|
||||
fn __sub__(&self, other: &Self) -> Self {
|
||||
Self(self.0.wrapping_sub(other.0))
|
||||
}
|
||||
|
||||
fn __mul__(&self, other: &Self) -> Self {
|
||||
Self(self.0.wrapping_mul(other.0))
|
||||
}
|
||||
|
||||
fn __truediv__(&self, other: &Self) -> PyResult<Self> {
|
||||
match self.0.checked_div(other.0) {
|
||||
Some(i) => Ok(Self(i)),
|
||||
None => Err(PyZeroDivisionError::new_err("division by zero")),
|
||||
}
|
||||
}
|
||||
|
||||
fn __floordiv__(&self, other: &Self) -> PyResult<Self> {
|
||||
match self.0.checked_div(other.0) {
|
||||
Some(i) => Ok(Self(i)),
|
||||
None => Err(PyZeroDivisionError::new_err("division by zero")),
|
||||
}
|
||||
}
|
||||
|
||||
fn __rshift__(&self, other: &Self) -> PyResult<Self> {
|
||||
match other.0.try_into() {
|
||||
Ok(rhs) => Ok(Self(self.0.wrapping_shr(rhs))),
|
||||
Err(_) => Err(PyValueError::new_err("negative shift count")),
|
||||
}
|
||||
}
|
||||
|
||||
fn __lshift__(&self, other: &Self) -> PyResult<Self> {
|
||||
match other.0.try_into() {
|
||||
Ok(rhs) => Ok(Self(self.0.wrapping_shl(rhs))),
|
||||
Err(_) => Err(PyValueError::new_err("negative shift count")),
|
||||
}
|
||||
}
|
||||
|
||||
fn __xor__(&self, other: &Self) -> Self {
|
||||
Self(self.0 ^ other.0)
|
||||
}
|
||||
|
||||
fn __or__(&self, other: &Self) -> Self {
|
||||
Self(self.0 | other.0)
|
||||
}
|
||||
|
||||
fn __and__(&self, other: &Self) -> Self {
|
||||
Self(self.0 & other.0)
|
||||
}
|
||||
|
||||
fn __int__(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn __float__(&self) -> f64 {
|
||||
self.0 as f64
|
||||
}
|
||||
|
||||
fn __complex__<'py>(&self, py: Python<'py>) -> &'py PyComplex {
|
||||
PyComplex::from_doubles(py, self.0 as f64, 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
fn my_module(_py: Python, m: &PyModule) -> PyResult<()> {
|
||||
m.add_class::<Number>()?;
|
||||
Ok(())
|
||||
}
|
||||
# const SCRIPT: &'static str = r#"
|
||||
# def hash_djb2(s: str):
|
||||
# n = Number(0)
|
||||
# five = Number(5)
|
||||
#
|
||||
# for x in s:
|
||||
# n = Number(ord(x)) + ((n << five) - n)
|
||||
# return n
|
||||
#
|
||||
# assert hash_djb2('l50_50') == Number(-1152549421)
|
||||
# assert hash_djb2('logo') == Number(3327403)
|
||||
# assert hash_djb2('horizon') == Number(1097468315)
|
||||
#
|
||||
#
|
||||
# assert Number(2) + Number(2) == Number(4)
|
||||
# assert Number(2) + Number(2) != Number(5)
|
||||
#
|
||||
# assert Number(13) - Number(7) == Number(6)
|
||||
# assert Number(13) - Number(-7) == Number(20)
|
||||
#
|
||||
# assert Number(13) / Number(7) == Number(1)
|
||||
# assert Number(13) // Number(7) == Number(1)
|
||||
#
|
||||
# assert Number(13) * Number(7) == Number(13*7)
|
||||
#
|
||||
# assert Number(13) > Number(7)
|
||||
# assert Number(13) < Number(20)
|
||||
# assert Number(13) == Number(13)
|
||||
# assert Number(13) >= Number(7)
|
||||
# assert Number(13) <= Number(20)
|
||||
# assert Number(13) == Number(13)
|
||||
#
|
||||
#
|
||||
# assert (True if Number(1) else False)
|
||||
# assert (False if Number(0) else True)
|
||||
#
|
||||
#
|
||||
# assert int(Number(13)) == 13
|
||||
# assert float(Number(13)) == 13
|
||||
# assert Number.__doc__ == "Did you ever hear the tragedy of Darth Signed The Overfloweth? I thought not.\nIt's not a story C would tell you. It's a Rust legend."
|
||||
# assert Number(12345234523452) == Number(1498514748)
|
||||
# try:
|
||||
# import inspect
|
||||
# assert inspect.signature(Number).__str__() == '(int)'
|
||||
# except ValueError:
|
||||
# # Not supported with `abi3` before Python 3.10
|
||||
# pass
|
||||
# assert Number(1337).__str__() == '1337'
|
||||
# assert Number(1337).__repr__() == 'Number(1337)'
|
||||
"#;
|
||||
|
||||
#
|
||||
# use pyo3::type_object::PyTypeObject;
|
||||
#
|
||||
# fn main() -> PyResult<()> {
|
||||
# Python::with_gil(|py| -> PyResult<()> {
|
||||
# let globals = PyModule::import(py, "__main__")?.dict();
|
||||
# globals.set_item("Number", Number::type_object(py))?;
|
||||
#
|
||||
# py.run(SCRIPT, Some(globals), None)?;
|
||||
# Ok(())
|
||||
# })
|
||||
# }
|
||||
|
||||
```
|
||||
|
||||
## Appendix: Writing some unsafe code
|
||||
|
||||
At the beginning of this chapter we said that PyO3 doesn't provide a way to wrap Python integers out
|
||||
of the box but that's a half truth. There's not a PyO3 API for it, but there's a Python C API
|
||||
function that does:
|
||||
|
||||
```c
|
||||
unsigned long PyLong_AsUnsignedLongMask(PyObject *obj)
|
||||
```
|
||||
|
||||
We can call this function from Rust by using [`pyo3::ffi::PyLong_AsUnsignedLongMask`]. This is an *unsafe*
|
||||
function, which means we have to use an unsafe block to call it and take responsibility for upholding
|
||||
the contracts of this function. Let's review those contracts:
|
||||
- The GIL must be held. If it's not, calling this function causes a data race.
|
||||
- The pointer must be valid, i.e. it must be properly aligned and point to a valid Python object.
|
||||
|
||||
Let's create that helper function. The signature has to be `fn(&PyAny) -> PyResult<T>`.
|
||||
- `&PyAny` represents a checked borrowed reference, so the pointer derived from it is valid (and not null).
|
||||
- Whenever we have borrowed references to Python objects in scope, it is guaranteed that the GIL is held. This reference is also where we can get a [`Python`] token to use in our call to [`PyErr::take`].
|
||||
|
||||
```rust
|
||||
# #![allow(dead_code)]
|
||||
use std::os::raw::c_ulong;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::ffi;
|
||||
use pyo3::conversion::AsPyPointer;
|
||||
|
||||
fn wrap(obj: &PyAny) -> Result<i32, PyErr> {
|
||||
let py: Python = obj.py();
|
||||
|
||||
unsafe {
|
||||
let ptr = obj.as_ptr();
|
||||
|
||||
let ret: c_ulong = ffi::PyLong_AsUnsignedLongMask(ptr);
|
||||
if ret == c_ulong::MAX {
|
||||
if let Some(err) = PyErr::take(py) {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ret as i32)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[`PyErr::take`]: https://docs.rs/pyo3/latest/pyo3/prelude/struct.PyErr.html#method.take
|
||||
[`Python`]: https://docs.rs/pyo3/latest/pyo3/struct.Python.html
|
||||
[`FromPyObject`]: https://docs.rs/pyo3/latest/pyo3/conversion/trait.FromPyObject.html
|
||||
[`pyo3::ffi::PyLong_AsUnsignedLongMask`]: https://docs.rs/pyo3/latest/pyo3/ffi/fn.PyLong_AsUnsignedLongMask.html
|
232
guide/src/class/object.md
Normal file
232
guide/src/class/object.md
Normal file
|
@ -0,0 +1,232 @@
|
|||
# Basic object customization
|
||||
|
||||
Recall the `Number` class from the previous chapter:
|
||||
|
||||
```rust
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[pyclass]
|
||||
struct Number(i32);
|
||||
|
||||
#[pymethods]
|
||||
impl Number {
|
||||
#[new]
|
||||
fn new(value: i32) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
fn my_module(_py: Python, m: &PyModule) -> PyResult<()> {
|
||||
m.add_class::<Number>()?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
At this point Python code can import the module, access the class and create class instances - but
|
||||
nothing else.
|
||||
|
||||
```python
|
||||
from my_module import Number
|
||||
|
||||
n = Number(5)
|
||||
print(n)
|
||||
```
|
||||
|
||||
```text
|
||||
<builtins.Number object at 0x000002B4D185D7D0>
|
||||
```
|
||||
|
||||
### String representations
|
||||
|
||||
It can't even print an user-readable representation of itself! We can fix that by defining the
|
||||
`__repr__` and `__str__` methods inside a `#[pymethods]` block. We do this by accessing the value
|
||||
contained inside `Number`.
|
||||
|
||||
```rust
|
||||
# use pyo3::prelude::*;
|
||||
#
|
||||
# #[pyclass]
|
||||
# struct Number(i32);
|
||||
#
|
||||
#[pymethods]
|
||||
impl Number {
|
||||
// For `__repr__` we want to return a string that Python code could use to recreate
|
||||
// the `Number`, like `Number(5)` for example.
|
||||
fn __repr__(&self) -> String {
|
||||
// We use the `format!` macro to create a string. Its first argument is a
|
||||
// format string, followed by any number of parameters which replace the
|
||||
// `{}`'s in the format string.
|
||||
//
|
||||
// 👇 Tuple field access in Rust uses a dot
|
||||
format!("Number({})", self.0)
|
||||
}
|
||||
|
||||
// `__str__` is generally used to create an "informal" representation, so we
|
||||
// just forward to `i32`'s `ToString` trait implementation to print a bare number.
|
||||
fn __str__(&self) -> String {
|
||||
self.0.to_string()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Hashing
|
||||
|
||||
|
||||
Let's also implement hashing. We'll just hash the `i32`. For that we need a [`Hasher`]. The one
|
||||
provided by `std` is [`DefaultHasher`], which uses the [SipHash] algorithm.
|
||||
|
||||
```rust
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
|
||||
// Required to call the `.hash` and `.finish` methods, which are defined on traits.
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
# use pyo3::prelude::*;
|
||||
#
|
||||
# #[pyclass]
|
||||
# struct Number(i32);
|
||||
#
|
||||
#[pymethods]
|
||||
impl Number {
|
||||
fn __hash__(&self) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
self.0.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: When implementing `__hash__` and comparisons, it is important that the following property holds:
|
||||
>
|
||||
> ```text
|
||||
> k1 == k2 -> hash(k1) == hash(k2)
|
||||
> ```
|
||||
>
|
||||
> In other words, if two keys are equal, their hashes must also be equal. In addition you must take
|
||||
> care that your classes' hash doesn't change during its lifetime. In this tutorial we do that by not
|
||||
> letting Python code change our `Number` class. In other words, it is immutable.
|
||||
>
|
||||
> By default, all `#[pyclass]` types have a default hash implementation from Python.
|
||||
> Types which should not be hashable can override this by setting `__hash__` to None.
|
||||
> This is the same mechanism as for a pure-Python class. This is done like so:
|
||||
>
|
||||
> ```rust
|
||||
> # use pyo3::prelude::*;
|
||||
> #[pyclass]
|
||||
> struct NotHashable { }
|
||||
>
|
||||
> #[pymethods]
|
||||
> impl NotHashable {
|
||||
> #[classattr]
|
||||
> const __hash__: Option<Py<PyAny>> = None;
|
||||
>}
|
||||
> ```
|
||||
|
||||
### Comparisons
|
||||
|
||||
Unlike in Python, PyO3 does not provide the magic comparison methods you might expect like `__eq__`,
|
||||
`__lt__` and so on. Instead you have to implement all six operations at once with `__richcmp__`.
|
||||
This method will be called with a value of `CompareOp` depending on the operation.
|
||||
|
||||
```rust
|
||||
use pyo3::class::basic::CompareOp;
|
||||
|
||||
# use pyo3::prelude::*;
|
||||
#
|
||||
# #[pyclass]
|
||||
# struct Number(i32);
|
||||
#
|
||||
#[pymethods]
|
||||
impl Number {
|
||||
fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult<bool> {
|
||||
match op {
|
||||
CompareOp::Lt => Ok(self.0 < other.0),
|
||||
CompareOp::Le => Ok(self.0 <= other.0),
|
||||
CompareOp::Eq => Ok(self.0 == other.0),
|
||||
CompareOp::Ne => Ok(self.0 != other.0),
|
||||
CompareOp::Gt => Ok(self.0 > other.0),
|
||||
CompareOp::Ge => Ok(self.0 >= other.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Truthyness
|
||||
|
||||
We'll consider `Number` to be `True` if it is nonzero:
|
||||
|
||||
```rust
|
||||
# use pyo3::prelude::*;
|
||||
#
|
||||
# #[pyclass]
|
||||
# struct Number(i32);
|
||||
#
|
||||
#[pymethods]
|
||||
impl Number {
|
||||
fn __bool__(&self) -> bool {
|
||||
self.0 != 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Final code
|
||||
|
||||
```rust
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::class::basic::CompareOp;
|
||||
|
||||
#[pyclass]
|
||||
struct Number(i32);
|
||||
|
||||
#[pymethods]
|
||||
impl Number {
|
||||
#[new]
|
||||
fn new(value: i32) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("Number({})", self.0)
|
||||
}
|
||||
|
||||
fn __str__(&self) -> String {
|
||||
self.0.to_string()
|
||||
}
|
||||
|
||||
fn __hash__(&self) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
self.0.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult<bool> {
|
||||
match op {
|
||||
CompareOp::Lt => Ok(self.0 < other.0),
|
||||
CompareOp::Le => Ok(self.0 <= other.0),
|
||||
CompareOp::Eq => Ok(self.0 == other.0),
|
||||
CompareOp::Ne => Ok(self.0 != other.0),
|
||||
CompareOp::Gt => Ok(self.0 > other.0),
|
||||
CompareOp::Ge => Ok(self.0 >= other.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn __bool__(&self) -> bool {
|
||||
self.0 != 0
|
||||
}
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
fn my_module(_py: Python, m: &PyModule) -> PyResult<()> {
|
||||
m.add_class::<Number>()?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
[`Hash`]: https://doc.rust-lang.org/std/hash/trait.Hash.html
|
||||
[`Hasher`]: https://doc.rust-lang.org/std/hash/trait.Hasher.html
|
||||
[`DefaultHasher`]: https://doc.rust-lang.org/std/collections/hash_map/struct.DefaultHasher.html
|
||||
[SipHash]: https://en.wikipedia.org/wiki/SipHash
|
|
@ -35,7 +35,7 @@ structs is not supported.
|
|||
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.
|
||||
|
||||
```
|
||||
```rust
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[derive(FromPyObject)]
|
||||
|
@ -65,7 +65,7 @@ struct RustyStruct {
|
|||
|
||||
By setting the `#[pyo3(item)]` attribute on the field, PyO3 will attempt to extract the value by calling the `get_item` method on the Python object.
|
||||
|
||||
```
|
||||
```rust
|
||||
use pyo3::prelude::*;
|
||||
|
||||
|
||||
|
@ -90,7 +90,7 @@ struct RustyStruct {
|
|||
|
||||
The argument passed to `getattr` and `get_item` can also be configured:
|
||||
|
||||
```
|
||||
```rust
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[derive(FromPyObject)]
|
||||
|
@ -135,7 +135,7 @@ Tuple structs are also supported but do not allow customizing the extraction. Th
|
|||
always assumed to be a Python tuple with the same length as the Rust type, the `n`th field
|
||||
is extracted from the `n`th item in the Python tuple.
|
||||
|
||||
```
|
||||
```rust
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[derive(FromPyObject)]
|
||||
|
@ -158,7 +158,7 @@ struct RustyTuple(String, String);
|
|||
Tuple structs with a single field are treated as wrapper types which are described in the
|
||||
following section. To override this behaviour and ensure that the input is in fact a tuple,
|
||||
specify the struct as
|
||||
```
|
||||
```rust
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[derive(FromPyObject)]
|
||||
|
@ -184,7 +184,7 @@ in extracting directly from the input object, i.e. `obj.extract()`, rather than
|
|||
an item or attribute. This behaviour is enabled per default for newtype structs and tuple-variants
|
||||
with a single field.
|
||||
|
||||
```
|
||||
```rust
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[derive(FromPyObject)]
|
||||
|
@ -223,7 +223,7 @@ i.e. a tuple variant assumes that the input is a Python tuple, and a struct vari
|
|||
extracting fields as attributes but can be configured in the same manner. The `transparent`
|
||||
attribute can be applied to single-field-variants.
|
||||
|
||||
```
|
||||
```rust
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[derive(FromPyObject)]
|
||||
|
@ -370,7 +370,7 @@ tested variants is returned. The names reported in the error message can be cust
|
|||
through the `#[pyo3(annotation = "name")]` attribute, e.g. to use conventional Python type
|
||||
names:
|
||||
|
||||
```
|
||||
```rust
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[derive(FromPyObject)]
|
||||
|
@ -454,7 +454,7 @@ All types in PyO3 implement this trait, as does a `#[pyclass]` which doesn't use
|
|||
Occasionally you may choose to implement this for custom types which are mapped to Python types
|
||||
_without_ having a unique python type.
|
||||
|
||||
```
|
||||
```rust
|
||||
use pyo3::prelude::*;
|
||||
|
||||
struct MyPyObjectWrapper(PyObject);
|
||||
|
|
|
@ -447,6 +447,8 @@ pub mod doc_test {
|
|||
"guide/src/python_typing_hints.md",
|
||||
guide_python_typing_hints
|
||||
);
|
||||
doctest!("guide/src/class/object.md", guide_class_object);
|
||||
doctest!("guide/src/class/numeric.md", guide_class_numeric);
|
||||
|
||||
// deliberate choice not to test guide/ecosystem because those pages depend on external crates
|
||||
// such as pyo3_asyncio.
|
||||
|
|
Loading…
Reference in a new issue