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

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.

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