Book Image

Speed Up Your Python with Rust

By : Maxwell Flitton
5 (2)
Book Image

Speed Up Your Python with Rust

5 (2)
By: Maxwell Flitton

Overview of this book

Python has made software development easier, but it falls short in several areas including memory management that lead to poor performance and security. Rust, on the other hand, provides memory safety without using a garbage collector, which means that with its low memory footprint, you can build high-performant and secure apps relatively easily. However, rewriting everything in Rust can be expensive and risky as there might not be package support in Rust for the problem being solved. This is where Python bindings and pip come in. This book will help you, as a Python developer, to start using Rust in your Python projects without having to manage a separate Rust server or application. Seeing as you'll already understand concepts like functions and loops, this book covers the quirks of Rust such as memory management to code Rust in a productive and structured manner. You'll explore the PyO3 crate to fuse Rust code with Python, learn how to package your fused Rust code in a pip package, and then deploy a Python Flask application in Docker that uses a private Rust pip module. Finally, you'll get to grips with advanced Rust binding topics such as inspecting Python objects and modules in Rust. By the end of this Rust book, you'll be able to develop safe and high-performant applications with better concurrency support.
Table of Contents (16 chapters)
1
Section 1: Getting to Understand Rust
5
Section 2: Fusing Rust with Python
11
Section 3: Infusing Rust into a Web Application

Metaprogramming with macros instead of decorators

Metaprogramming can generally be described as a way in which the program can manipulate itself based on certain instructions. Considering the strong typing Rust has, one of the simplest ways that we can metaprogram is by using generics. A classic example of demonstrating generics is through coordinates:

struct Coordinate <T> {
        x: T,
        y: T
    }
fn main() {
    let one = Coordinate{x: 50, y: 50};
    let two = Coordinate{x: 500, y: 500};
    let three = Coordinate{x: 5.6, y: 5.6};
}

What is happening here is that the compiler is looking through all the uses of our struct throughout the whole program. It then creates structs that have those types. Generics are a good way of saving time and getting the compiler to write repetitive code. While this is the simplest form of metaprogramming, another form of metaprogramming in Rust is macros.

You may have noticed throughout the chapter that some of the functions that we use, such as the println! function, have an ! at the end. This is because it is not technically a function, it is a macro. The ! denotes that the macro is being called. Defining our own macros is a blend of defining our own function and using lifetime notation within a match statement within the function. To demonstrate this, we can define our own macro that capitalizes the first character in a string passed through it with the following code:

macro_rules! capitalize {
        ($a: expr) => {
            let mut v: Vec<char> = $a.chars().collect();
            v[0] = v[0].to_uppercase().nth(0).unwrap();
            $a = v.into_iter().collect();
        }
    }
fn main() {
    let mut x = String::from("test");
    capitalize!(x);
    println!("{}", x);
}

Instead of using the fn term that is used for defining functions, we define our macro using macro_rules!. We then say that the $a is the expression passed into the macro. We then get the expression, convert it into a vector of chars, uppercase the first character, and then convert it back to a string. It must be noted that the macro that we defined does not return anything, and we do not assign any variable when calling our macro in the main function. However, when we print the x variable at the end of the main function, it is capitalized. Therefore, we can deduce that our macro is altering the state of the variable.

However, we must remember that macros are a last resort. Our example shows that our macro alters the state even though it is not directly demonstrated in the main function. As the complexity of the program grows, we could end up with a lot of brittle, highly coupled processes that we are not aware of. If we change one thing, it could break five other things. For capitalizing the first letter, it is better to just build a function that does this and returns a string value.

Macros do not just stop at what we have covered, they also have the same effect as our decorators in Python. To demonstrate this, let's look at our coordinate again. We can generate our coordinate and then pass it through a function so it can be moved. We then try to print the coordinate outside of the function with the following code:

struct Coordinate {
    x: i8,
    y: i8
}
fn print(point: Coordinate) {
    println!("{} {}", point.x, point.y);
}
fn main() {
    let test = Coordinate{x: 1, y:2};
    print(test);
    println!("{}", test.x)
}

It will be expected that Rust will refuse to compile the code because the coordinate has been moved into the scope of the print function that we created and therefore we cannot use it in the final println!. We could borrow the coordinate and pass that through to the function. However, there is another way we can do this. Remember that integers passed through functions without any trouble because they had a Copy trait. Now, we could try and code a Copy trait ourselves, but this would be convoluted and would require advanced knowledge. Luckily for us, we can implement the Copy and Clone traits by utilizing a derive macro with the following code:

#[derive(Clone, Copy)]
struct Coordinate {
        x: i8,
        y: i8
    }

With this, our code works as we copy the coordinate when passing it through the function. Macros can be utilized by many packages and frameworks, from JavaScript Object Notation (JSON) serialization to entire web frameworks. In fact, here is the classic example of running a basic server in the Rocket framework:

#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use] extern crate rocket;
#[get("/hello/<name>/<age>")]
fn hello(name: String, age: u8) -> String {
    format!("Hello, {} year old named {}!", age, name)
}
fn main() {
    rocket::ignite().mount("/", routes![hello]).launch();
}

This is a striking resemblance to the Python Flask application example at the beginning of the chapter. These macros are acting exactly like our decorators in Python, which is not surprising as a decorator in Python is a form of metaprogramming that wraps a function.

This wraps up our brief introduction to the Rust language for Python developers. We are now able to move on to other concepts, such as structuring our code and building fully fledged programs coded in Rust.