-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3f11c50
commit 8b1dc91
Showing
10 changed files
with
300 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,6 @@ | ||
# Welcome | ||
In this section, we will learn how to write Python extension modules in Rust using the [PyO3](https://pyo3.rs) library. | ||
|
||
We will cover how to define Python functions, classes, modules, and exceptions in Rust. | ||
|
||
PyO3 is a rich library that provides many more features (e.g., interaction with the GIL, async) that we will not cover. However, you are welcome to explore its excellent [documentation](https://pyo3.rs) for more information. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# Limitations | ||
|
||
## No Generics | ||
Due to mono-morphization, using `#[pyclass]` on generic types is not possible. | ||
|
||
## No Lifetime Parameters | ||
Python uses reference counting and garbage collection, so the Rust compiler cannot reason about lifetimes statically. | ||
Therefore, classes must either own their data, or use smart pointers like `Arc` for data sharing. Additionally, there is `pyo3::Py`, which is a smart pointer to data allocated on the Python heap. | ||
|
||
## Ownership | ||
Since Python does not have a concept of ownership, if a method accepts `self` (or any other argument) as owned, it will be cloned when crossing the Python-Rust boundary. This can be expensive for large objects, but can be avoided by using references or smart pointers. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# Python classes | ||
The attribute `#[pyclass]` is used to define a Python class from a Rust struct or enum: | ||
|
||
```rust,ignore | ||
use pyo3::prelude::*; | ||
#[pyclass] | ||
#[derive(Clone)] | ||
struct Location { | ||
lat: f64, | ||
lon: f64, | ||
} | ||
#[pyclass] | ||
struct RideRequest { | ||
rider_name: String, | ||
origin: Location, | ||
destination: Location, | ||
} | ||
#[pymodule] | ||
fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { | ||
m.add_class::<RideRequest>()?; | ||
m.add_class::<Location>()?; | ||
Ok(()) | ||
} | ||
``` | ||
|
||
By default, the class is not instantiable from Python. To define a constructor, add a method with the `#[new]` attribute: | ||
|
||
```rust,ignore | ||
#[pymethods] | ||
impl Location { | ||
#[new] | ||
fn new(lat: f64, lon: f64) -> Self { | ||
Location { lat, lon } | ||
} | ||
} | ||
#[pymethods] | ||
impl RideRequest { | ||
#[new] | ||
fn new(rider_name: String, origin: Location, destination: Location) -> Self { | ||
RideRequest { rider_name, origin, destination } | ||
} | ||
} | ||
``` | ||
|
||
```python | ||
from my_module import Location, RideRequest | ||
|
||
origin = Location(32.070575, 34.770354) | ||
destination = Location(32.077381, 34.793280) | ||
request = RideRequest("Alice", origin, destination) | ||
``` | ||
|
||
pycalsses implement `FromPyObject` and `IntoPy<PyObject>` so that they can be used as arguments and return values. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
# Error Handling | ||
|
||
Python and Rust have distinct mechanisms for handling errors. | ||
|
||
In Python, exceptions are raised when an error occurs. These exceptions propagate up the call stack until they are caught and handled. | ||
|
||
In Rust, errors are returned as values. The `Result<T, E>` enum represents the result of an operation that may fail. The `T` type is the value returned on success, and the `E` type is the error returned on failure. | ||
|
||
PyO3 bridges the gap between Python and Rust error handling by using the `PyResult<T>` type, which is an alias for `Result<T, PyErr>`. Here, `PyErr` represents a Python exception. If a `PyResult` returns from Rust to Python through PyO3 and it is an `Err` variant, the associated exception will be raised. | ||
|
||
```rust | ||
use pyo3::exceptions::PyValueError; | ||
use pyo3::prelude::*; | ||
|
||
#[pyfunction] | ||
fn divide(x: i32, y: i32) -> PyResult<i32> { | ||
if y == 0 { | ||
return Err(PyValueError::new_err("division by zero")); | ||
} | ||
Ok(x / y) | ||
} | ||
|
||
#[pymodule] | ||
fn my_module(m: &PyModule) -> PyResult<()> { | ||
m.add_function(wrap_pyfunction!(divide, m)?)?; | ||
Ok(()) | ||
} | ||
``` | ||
|
||
```python | ||
from my_module import divide | ||
|
||
try: | ||
divide(1, 0) | ||
except ValueError as e: | ||
print(e) # division by zero | ||
``` | ||
|
||
|
||
Many error types in the standard library implement `Into<PyErr>`, allowing the use of the `?` operator to easily propagate errors. | ||
|
||
```rust | ||
use pyo3::prelude::*; | ||
|
||
#[pyfunction] | ||
fn parse_int(s: &str) -> PyResult<i64> { | ||
Ok(s.parse()?) | ||
} | ||
|
||
#[pymodule] | ||
fn my_module(m: &PyModule) -> PyResult<()> { | ||
m.add_function(wrap_pyfunction!(parse_int, m)?)?; | ||
Ok(()) | ||
} | ||
``` | ||
|
||
```python | ||
from my_module import parse_int | ||
|
||
try: | ||
parse_int("abc") | ||
except ValueError as e: | ||
print(e) # invalid digit found in string | ||
``` | ||
|
||
Conveniently, `anyhow::Error` implements `Into<PyErr>`, so you can use `anyhow` for error handling in Rust and propagate errors to Python with the `?` operator. | ||
|
||
```rust | ||
use anyhow::Context; | ||
use pyo3::prelude::*; | ||
|
||
#[pyfunction] | ||
fn divide(x: i32, y: i32) -> PyResult<i32> { | ||
Ok(x.checked_div(y).context("division by zero")?) | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# Functions | ||
|
||
The PyO3 prelude provides the `pyfunction` attribute macro to define a Python function from a Rust function. | ||
To make it available to Python, we need also to define a module that exports the function. | ||
|
||
```rust,ignore | ||
use pyo3::prelude::*; | ||
#[pyfunction] | ||
fn largest_positive(x: Vec<i64>) -> Option<i64> { | ||
x.into_iter().filter(|&x| x > 0).max() | ||
} | ||
#[pymodule] | ||
fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { | ||
m.add_function(wrap_pyfunction!(largest_positive, m)?)?; | ||
Ok(()) | ||
} | ||
``` | ||
|
||
PyO3 automatically converts Rust types to Python types and vice versa: | ||
```python | ||
from my_module import largest_positive | ||
|
||
largest_positive([1, -2, 3, -4, 5]) # 5 | ||
largest_positive([-1, -2, -3]) # None | ||
``` | ||
|
||
Type conversions are defined through the `FromPyObject` and `IntoPy<PyObject>` traits, which are implemented for many standard Rust types. | ||
Checkout [the table in PyO3 documentation](https://pyo3.rs/v0.22.3/conversions/tables) for more information. | ||
|
||
There is also a derive macro for `FromPyObject`, which makes it easy to use your own types (structs and enums) as function arguments. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
# Setup | ||
|
||
The easiest way to create a Python extension in Rust by using [maturin](https://www.maturin.rs/), a tool that simplifies the process of configuring, building and publishing Rust-based Python packages. | ||
|
||
## Starting a new project | ||
In a fresh virtualenv, install maturin and create a new project: | ||
```shell | ||
pip install maturin | ||
maturin new hello-python | ||
cd hello-python | ||
``` | ||
|
||
A skeleton project will be created. It contains a small example of a Python module implemented in Rust, with a function that returns the sum of two numbers as a string: | ||
```rust,ignore | ||
use pyo3::prelude::*; | ||
/// Formats the sum of two numbers as string. | ||
#[pyfunction] | ||
fn sum_as_string(a: usize, b: usize) -> PyResult<String> { | ||
Ok((a + b).to_string()) | ||
} | ||
/// A Python module implemented in Rust. | ||
#[pymodule] | ||
fn hello_python(_py: Python, m: &PyModule) -> PyResult<()> { | ||
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; | ||
Ok(()) | ||
} | ||
``` | ||
|
||
## Building the project | ||
To build the project, run: | ||
```shell | ||
maturin develop | ||
``` | ||
This will compile the crate, build the python package and install it in the active virtualenv. | ||
|
||
Now you can use it from python: | ||
```python | ||
import hello_python | ||
hello_python.sum_as_string(5, 20) | ||
``` | ||
|
||
`maturing develop --release` will build the project in release mode. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# Methods | ||
To define methods on a Python class, add the `#[pymethods]` attribute to the `impl` block of a `pyclass`. | ||
|
||
```rust,ignore | ||
use pyo3::prelude::*; | ||
#[pyclass] | ||
struct Point { | ||
x: f64, | ||
y: f64, | ||
} | ||
#[pymethods] | ||
impl Point { | ||
#[new] | ||
fn new(x: f64, y: f64) -> Self { | ||
Point{x, y} | ||
} | ||
fn magnitude(&self) -> f64 { | ||
(self.x.powi(2) + self.y.powi(2)).sqrt() | ||
} | ||
fn scale(&mut self, factor: f64) { | ||
self.x *= factor; | ||
self.y *= factor; | ||
} | ||
#[getter] | ||
fn x(&self) -> f64 { | ||
self.x | ||
} | ||
#[setter] | ||
fn set_x(&mut self, x: f64) { | ||
self.x = x; | ||
} | ||
#[staticmethod] | ||
fn origin() -> Self { | ||
Point{x: 0.0, y: 0.0} | ||
} | ||
fn __repr__(&self) -> String { | ||
format!("Point({}, {})", self.x, self.y) | ||
} | ||
#[pymodule] | ||
fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { | ||
m.add_class::<Point>()?; | ||
Ok(()) | ||
} | ||
} | ||
``` | ||
|
||
```python | ||
from my_module import Point | ||
|
||
p = Point(3.0, 4.0) | ||
print(p.magnitude()) # 5.0 | ||
p.scale(2.0) | ||
p.x = 10.0 | ||
print(p.x) # 10.0 | ||
print(p) # Point(10.0, 8.0) | ||
print(Point.origin()) # Point(0.0, 0.0) | ||
``` | ||
PyO3 provides many more convenience macros, including automatic generation of getters and setters, `__eq__` and `__hash__` methods, and more. |
This file was deleted.
Oops, something went wrong.