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

Keeping track of scopes and lifetimes

In Python, we do have the concept of scope. It is generally enforced in functions. For instance, we can call the Python function defined here:

def add_and_square(one: int, two: int) -> int:
    total: int = one + two
    return total * total

In this case, we can access the return variable. However, we will not be able to access the total variable. Outside of this, most of the variables are accessible throughout the program. With Rust, it is different. Like typing, Rust is aggressive with scopes. Once a variable is passed into a scope, it is deleted when the scope is finished. Rust manages to maintain memory safety without garbage collection with the borrowing rules. Rust deletes its variables without garbage collection by wiping all variables out of scope. It can also define scopes with curly brackets. A classic way of demonstrating scopes can be done by the following code:

fn main() {
    let one: String = String::from("one");
    // start of the inner-scope
    { 
        println!("{}", &one);
        let two: String = String::from("two");
    } 
    // end of the inner-scope
    println!("{}", one);
    println!("{}", two);
}

If we try and run this code, we get the error code defined here:

println!("{}", two);
        ^^^ not found in this scope

We can see that the variable one can be accessed in the inner-scope as it was defined outside the outer-scope. However, the variable two is defined in the inner-scope. Once the inner-scope is finished, we can see by the error that we cannot access the variable two outside the inner-scope. We must remember that the scope of functions is a little stronger. From revising borrowing rules, we know that when we move a variable into the scope of a function, it cannot be accessed outside of the scope of the function if the variable is not borrowed as it is moved. However, we can still alter a variable inside another scope like another function, and still then access the changed variable. To do this, we must do a mutable borrow, and then must dereference (using *) the borrowed mutable variable, alter the variable, and then access the altered variable outside the function, as we can see with the following code:

fn alter_number(number: &mut i8) {
    *number += 1
}
fn print_number(number: i8) {
    println!("print function scope: {}", number);
}
    
fn main() {
    let mut one: i8 = 1;
    print_number(one);
    alter_number(&mut one);
    println!("main scope: {}", one);
}

This gives us the following output:

print function scope: 1
main scope: 2

With this, we can see that that if we are comfortable with our borrowing, we can be flexible and safe with our variables. Now that we have explored the concept of scopes, this leads naturally to lifetimes, as lifetimes can be defined by scopes. Remember that a borrow is not sole ownership. Because of this, there is a risk that we could reference a variable that's deleted. This can be demonstrated in the following classic demonstration of a lifetime:

fn main() {
    let one;
    {
        let two: i8 = 2;
        one = &two;
    } // -----------------------> two lifetime stops here
    println!("r: {}", one);
}

Running this code gives us the following error:

    one = &two;
     ^^^^ borrowed value does not live long enough
} // -----------------------> two lifetime stops here
- 'two' dropped here while still borrowed
println!("r: {}", one);
                  --- borrow later used here

What has happened here is that we state that there is a variable called one. We then define an inner-scope. Inside this scope, we define an integer two. We then assign one to be a reference of two. When we try and print one in the outer-scope, we can't, as the variable it is pointing to has been deleted. Therefore, we no longer get the issue that the variable is out of scope, it's that the lifetime of the value that the variable is pointing to is no longer available, as it's been deleted. The lifetime of two is shorter than the lifetime of one.

While it is great that this is flagged when compiling, Rust does not stop here. This concept also translates functions. Let's say that we build a function that references two integers, compares them, and returns the highest integer reference. The function is an isolated piece of code. In this function, we can denote the lifetimes of the two integers. This is done by using the ' prefix, which is a lifetime notation. The names of the notations can be anything you wish, but it's a general convention to use a, b, c, and so on. Let's look at an example:

fn get_highest<'a>(first_number: &'a i8, second_number: &'\
  a     i8) -> &'a i8 {
    if first_number > second_number {
        return first_number
    } else {
        return second_number
    }
}
fn main() {
    let one: i8 = 1;
    {
        let two: i8 = 2;
        let outcome: &i8 = get_highest(&one, &two);
        println!("{}", outcome);
    }
}

As we can see, the first_number and second_number variables have the same lifetime notation of a. This means that they have the same lifetimes. We also have to note that the get_highest function returns an i8 with a lifetime of a. As a result, both first_number and second_number variables can be returned, which means that we cannot use the outcome variable outside of the inner-scope. However, we know that our lifetimes between the variables one and two are different. If we want to utilize the outcome variable outside of the inner-scope, we must tell the function that there are two different lifetimes. We can see the definition and implementation here:

fn get_highest<'a, 'b>(first_number: &'a i8, second_ \
  number:   &'b i8) -> &'a i8 {
    if first_number > second_number {
        return first_number
    } else {
        return &0
    }
}
fn main() {
    let one: i8 = 1;
    let outcome: &i8;
    {
        let two: i8 = 2;
        outcome = get_highest(&one, &two);
    }
    println!("{}", outcome);
}

Again, the lifetime a is returned. Therefore, the parameter with the lifetime b can be defined in the inner-scope as we are not returning it in the function. Considering this, we can see that lifetimes are not exactly essential. We can write comprehensive programs without touching lifetimes. However, they are an extra tool. We don't have to let scopes completely constrain us with lifetimes.

We are now at the final stages of knowing enough Rust to be productive Rust developers. All we need to understand now is building structs and managing them with macros. Once this is done, we can move onto the next chapter of structuring Rust programs. In the next section, we will cover the building of structs.