Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Speed Up Your Python with Rust

You're reading from   Speed Up Your Python with Rust Optimize Python performance by creating Python pip modules in Rust with PyO3

Arrow left icon
Product type Paperback
Published in Jan 2022
Publisher Packt
ISBN-13 9781801811446
Length 384 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Maxwell Flitton Maxwell Flitton
Author Profile Icon Maxwell Flitton
Maxwell Flitton
Arrow right icon
View More author details
Toc

Table of Contents (16) Chapters Close

Preface 1. Section 1: Getting to Understand Rust
2. Chapter 1: An Introduction to Rust from a Python Perspective FREE CHAPTER 3. Chapter 2: Structuring Code in Rust 4. Chapter 3: Understanding Concurrency 5. Section 2: Fusing Rust with Python
6. Chapter 4: Building pip Modules in Python 7. Chapter 5: Creating a Rust Interface for Our pip Module 8. Chapter 6: Working with Python Objects in Rust 9. Chapter 7: Using Python Modules with Rust 10. Chapter 8: Structuring an End-to-End Python Package in Rust 11. Section 3: Infusing Rust into a Web Application
12. Chapter 9: Structuring a Python Flask App for Rust 13. Chapter 10: Injecting Rust into a Python Flask App 14. Chapter 11: Best Practices for Integrating Rust 15. Other Books You May Enjoy

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.

You have been reading a chapter from
Speed Up Your Python with Rust
Published in: Jan 2022
Publisher: Packt
ISBN-13: 9781801811446
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image