Skip to content

Commit

Permalink
pyo3 section. delete survey section
Browse files Browse the repository at this point in the history
  • Loading branch information
tom-shlomo committed Sep 30, 2024
1 parent 3f11c50 commit 8b1dc91
Show file tree
Hide file tree
Showing 10 changed files with 300 additions and 5 deletions.
3 changes: 1 addition & 2 deletions book/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ Most notably, we added a dedicated section and an exercise on using pyo3 to crea

To enhance the learning experience, we encouraged students to use VSCode as their development environment. We spent time setting up VSCode with essential tools like rust analyzer, clippy, and rustfmt to streamline their workflow.

At the conclusion of the workshop, we conducted a survey to gather feedback from the participants. The results were very positive, with 90% of respondents expressing a desire to use Rust in their projects and 80% feeling confident in their ability to do so with appropriate guidance.
You can find the full results [here](survey.md).
At the conclusion of the workshop, we conducted a survey to gather feedback from the participants. The results were very positive, with 90% of respondents expressing a desire to use Rust in their projects and 75% feeling confident in their ability to do so with appropriate guidance.

We hope that you will find the materials on this site useful and that they will inspire you to explore Rust further. Happy coding!
10 changes: 8 additions & 2 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@
[Welcome to Rust with Via](README.md)
[Schedule](schedule.md)
[Miscellaneous Tips](tips.md)
[Survey Results](survey.md)

# Creating Python Extension Modules
- [Welcome](pyo3/README.md)
- [Setup](pyo3/maturin.md)
- [Functions](pyo3/functions.md)
- [Classes](pyo3/classes.md)
- [Methods](pyo3/methods.md)
<!-- - [Even More Macros](pyo3/more_macros.md)] -->
<!-- - [Limitations](pyo3/class_limitations.md) -->
- [Error Handling](pyo3/errors.md)

# Exercises
- [Inventory](inventory/README.md)
Expand All @@ -22,4 +28,4 @@
- [Solution](mini-gtfs/solution.md)
- [Simple Solution](mini-gtfs/simple_solution.md)
- [Faster Solution](mini-gtfs/faster_solution.md)
- [Instructor Notes](mini-gtfs/instructor.md)
- [Instructor Notes](mini-gtfs/instructor.md)
5 changes: 5 additions & 0 deletions book/src/pyo3/README.md
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.
11 changes: 11 additions & 0 deletions book/src/pyo3/class_limitations.md
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.
57 changes: 57 additions & 0 deletions book/src/pyo3/classes.md
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.
76 changes: 76 additions & 0 deletions book/src/pyo3/errors.md
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")?)
}
```
32 changes: 32 additions & 0 deletions book/src/pyo3/functions.md
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.
43 changes: 43 additions & 0 deletions book/src/pyo3/maturin.md
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.
67 changes: 67 additions & 0 deletions book/src/pyo3/methods.md
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.
1 change: 0 additions & 1 deletion book/src/survey.md

This file was deleted.

0 comments on commit 8b1dc91

Please sign in to comment.