Movatterモバイル変換


[0]ホーム

URL:


Packt
Search iconClose icon
Search icon CANCEL
Subscription
0
Cart icon
Your Cart(0 item)
Close icon
You have no products in your basket yet
Save more on your purchases!discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Profile icon
Account
Close icon

Change country

Modal Close icon
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timerSALE ENDS IN
0Days
:
00Hours
:
00Minutes
:
00Seconds
Home> Programming> Software Architecture> Speed Up Your Python with Rust
Speed Up Your Python with Rust
Speed Up Your Python with Rust

Speed Up Your Python with Rust: Optimize Python performance by creating Python pip modules in Rust with PyO3

Arrow left icon
Profile Icon Maxwell Flitton
Arrow right icon
Can$39.99Can$44.99
Full star iconFull star iconFull star iconFull star iconHalf star icon4.9(14 Ratings)
eBookJan 2022384 pages1st Edition
eBook
Can$39.99 Can$44.99
Paperback
Can$55.99
Subscription
Free Trial
Arrow left icon
Profile Icon Maxwell Flitton
Arrow right icon
Can$39.99Can$44.99
Full star iconFull star iconFull star iconFull star iconHalf star icon4.9(14 Ratings)
eBookJan 2022384 pages1st Edition
eBook
Can$39.99 Can$44.99
Paperback
Can$55.99
Subscription
Free Trial
eBook
Can$39.99 Can$44.99
Paperback
Can$55.99
Subscription
Free Trial

What do you get with eBook?

Product feature iconInstant access to your Digital eBook purchase
Product feature icon Download this book inEPUB andPDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature iconDRM FREE - Read whenever, wherever and however you want
Product feature iconAI Assistant (beta) to help accelerate your learning
OR

Contact Details

Modal Close icon
Payment Processing...
tickCompleted

Billing Address

Table of content iconView table of contentsPreview book icon Preview Book

Speed Up Your Python with Rust

Chapter 1: An Introduction to Rust from a Python Perspective

Due to its speed and safety, it is no surprise thatRust is the new language gaining in popularity. However, with success comes criticism. Despite Rust's popularity as an impressive language, it has also gained the label of being hard to learn, an idea which isn't quite grounded in reality.

In this chapter, we will cover all of Rust's quirks that will be new to a Python developer. If Python is your main language, concepts such as basic memory management and typing can initially slow down your ability to quickly write productive Rust code due to the compiler failing to compile the code. However, this can quickly be overcome by learning the rules around Rust features, such as variable ownership, lifetimes, and so on, as Rust is a memory-safe language. Consequently, we must keep track of our variables as they usually get deleted instantly when they go out of scope. If this does not make sense yet, don't worry; we will cover this concept in theKeeping track of scopes and lifetimes section.

In this chapter, we will also be covering the basics of syntax, while you will be setting up a Rust environment on your own computer in the next chapter. Do not worry though, you can code all the examples in this chapter on the free online Rust playground.

In particular, we will cover the following topics in this chapter:

  • Understanding the differences between Python and Rust
  • Understanding variable ownership
  • Keeping track of scopes and lifetimes
  • Building structs as opposed to objects
  • Metaprogramming with macros instead of decorators

Technical requirements

As this is just an introduction, all the Python examples in the chapter can be implemented with a free online Python interpreter such ashttps://replit.com/languages/python3.

The same goes for all the Rust examples. These can be implemented using the free online Rust playground found athttps://play.rust-lang.org/.

The code covered in the chapter can be found athttps://github.com/PacktPublishing/Speed-up-your-Python-with-Rust/tree/main/chapter_one.

Understanding the differences between Python and Rust

Rust can sometimes be described as asystems language. As a result, it can sometimes be labeledby software engineers in a way that is similar to C++: fast, hard to learn, dangerous, and time-consuming to code in. As a result, most of you mainlyworking in dynamic languages such as Python could be put off. However, Rust is memory-safe, efficient, and productive. Once we have gotten over some of the quirks that Rust introduces, nothing is holding you back from exploiting Rust's advantages by using it to write fast, safe, and efficient code. Seeing as there are so many advantages to Rust, we will explore them in the next section.

Why fuse Python with Rust?

When it comesto picking a language, there is usually a trade-off between resources, speed, and development time. Dynamic languages such as Python became popular as computing power increased. We were able to use the extra resources we had to manage our memory with garbage collectors. As a result, developing software became easier, quicker, and safer. As we will cover later in theKeeping track of scopes and lifetimes section, poor memory management can lead to somesecurity flaws. The exponential increase in computing power over the years is known asMoore's Law. However, this is not continuing to hold and in 2019, Nvidia's CEO Jensen Huang suggested that as chip components get closer to the size of individual atoms, it has gotten harder to keep up with the pace of Moore's Law, thus declaring it dead (https://www.cnet.com/news/moores-law-is-dead-nvidias-ceo-jensen-huang-says-at-ces-2019/).

However, with the rise of big data, our need to pick up faster languages to satisfy our needs is increasing. This is where languages such as Golang and Rust enter. These languagesare memory-safe, yet they compile and have significant speed increases. What makes Rust even more unique is that it has managed to achieve memory safety withoutgarbage collection. To appreciate this, let's briefly describegarbage collection: this is where the program temporarily stops, checks all the variables to see which ones are no longer being used, and deletes those that are not. Considering that Rust does not have to do this, it is a significant advantage as Rust does not have to keep stopping to clean up the variables. This was demonstrated in Discord's 2020 blog postWhy Discord is switching from Go to Rust:https://blog.discord.com/why-discord-is-switching-from-go-to-rust-a190bbca2b1f#:~:text=The%20service%20we%20switched%20from,is%20in%20the%20hot%20path. In this post, we can see that Golang just could not keep up with Rust, as demonstrated in the graph they presented:

Figure 1.1 – Golang is spiky and Rust is the flat line below Golang

Figure 1.1 – Golang is spiky and Rust is the flat line below Golang (image source: https://blog.discord.com/why-discord-is-switching-from-go-to-rust-a190bbca2b1f#:~:text=The%20service%20we%20switched%20from,is%20in%20the%20hot%20path)

The comments on the post were full of people complaining that Discord used an out-of-date version of Golang. Discord responded to this by stating that they tried a range of Golang versions, and they all had similar results. With this, it makes sense to get the best of both worlds without much compromise. We can use Python for prototyping and complex logic. The extensive range of third-party libraries that Python has combined with the flexible object-oriented programming it supports make it an ideal language for solving real-world problems. However, it's slow and is not efficient with the use of resources. This is where we reach for Rust.

Rust is a bit more restrictive in the way we can lay out and structure the code; however, it's fast, safe, and efficient when implementing multithreading. Combining these two languages enables a Python developer to have a powerful tool in their belt that their Python codecan use when needed. The time investment needed to learn and fuse Rust is low. All we must do is package Rust and install it in our Python system usingpip and understand a few quirks that Rust has that are different from Python. We can start this journey by looking at how Rust handles strings in the next section. However, before we explore strings, we have to first understand how Rust is run compared to Python.

If you have built a web app in Python using Flask, you will have seen multiple tutorials sporting the following code:

from flask import Flaskapp = Flask(__name__)@app.route("/")def home():    return "Hello, World!"    if __name__ == "__main__":    app.run(debug=True)

What we must note here is the last two lines of the code. Everything above that defines a basic Flask web app and a route. However, the running of the app in the last two lines only executes if the Python interpreter is directly running the file. This means that other Python files can import the Flask app from this file without running it. This is referred to by many as anentry point.

You import everything you need in this file, and for the application to run, we get our interpreter to run this script. We can nest any code under theif __name__ == "__main__": line of code. It will not run unless the file is directly hit by the Python interpreter. Rust has a similar concept. However, this is more essential, as opposed to Python that just has it as a nice-to-have feature. In the Rust playground (see theTechnical requirements section), we can type in the following code if it is not there already:

fn main() {    println!("hello world");}

This is theentry point. The Rust program gets compiled, and then runs themain function. If whatever you've coded is not accessed by themain function, it will never run. Here, we are already getting a sense of the safety enforced by Rust. We will see more of this throughout the book.

Now that we have our program running, we can move on to understanding the difference between Rust and Python when it comes to strings.

Passing strings in Rust

In Python,strings are flexible. Wecan pretty much do what we want with them. While technically, Python strings cannot be changed under the hood, in Python syntax, we can chop andchange them, pass themanywhere, and convert them into integers or floats (if permitted) without having to think too much about it. We can do all of this with Rust too. However, we must plan beforehand what we are going to do. To demonstrate this, we can dive right in by making our ownprint function and calling it, as seen in the following code:

fn print(input: str) {    println!("{}", input);}fn main() {    print("hello world");}

In Python, a similar program would work. However, when we run it in the Rust playground, we get the following error:

error[E0277]: the size for values of type 'str' cannot be known at compilation time

This is because we cannot specify what the maximum size is. We don't get this in Python; therefore, we must take a step back and understand how variables are assigned in memory. When the code compiles, it allocates memory for different variables in the stack. When the code runs, it stores data in the heap. Strings can be various sizes so we cannot be sure at compile time how much memory we can allocate to theinput parameterof our function when compiling. What we arepassing in is astring slice. We can remedy this by passingin a string and converting our string literal to a string before passing it into our function as seen here:

fn print(input: String) {    println!("{}", input);}fn main() {    let string_literal = "hello world";    print(string_literal.to_string());}

Here, we can see that we have used theto_string() function to convert our string literal into a string. To understand whyString is accepted, we need to understand what a string is.

A string is a type of wrapper implemented as a vector of bytes. This vector holds a reference to a string slice in the heap memory. It then holds the amount of data available to the pointer, and the length of the string literal. For instance, if we have a string of the string literalone, it can be denoted by the following diagram:

Figure 1.2 – String relationship to str

Figure 1.2 – String relationship to str

Considering this, we can understand why we can guarantee the size ofString when we pass it into our function. It will always be a pointer to the string literal with some meta-information about the string literal. If we can just make a reference to the string literal, we can passthis into our function as it is just a referenceand we can therefore guarantee that the size of the reference will stay the same. This can be done by borrowing using the& operator as shown in the following code:

fn print(input_string: &str) {    println!("{}", input_string);}fn main() {    let test_string = &"Hello, World!";    print(test_string);}

We will cover the concept of borrowing later in the chapter but, for now, we understand that, unlike Python, we must guarantee the size of the variable being passed into a function. We can use borrowing and wrappers such as strings to handle this. It may not come as a surprise, but this does not just stop at strings. Considering this, we can move on to the next section to understand the differences between Python and Rust when it comes to floats and integers.

Sizing up floats and integers in Rust

Like strings, Python manages floats and integers with ease and simplicity. We can pretty much dowhatever we want with them. For instance, the followingPython code will result in6.5:

result = 1 + 2.2result = result + 3.3

However, there is a problem when we try to just execute the first line in Rust with the following line of Rust code:

let result = 1 + 2.2;

It results in an error telling us that a float cannot be added to an integer. This error highlights one of the pain points that Python developers go through when learning Rust, as Rust enforces typing aggressively by refusing to compile if typing is not present and consistent. However, while this is an initial pain, aggressive typing does help in the long run as it maintains safety.

Type annotation in Python is gaining popularity. This is where the type of the variable is declared for parameters of functions or variables declared, enabling some editors to highlight when the types are inconsistent. The same happens in JavaScript with TypeScript. We can replicate the Python code at the start of this section with the following Rust code:

let mut result = 1.0 + 2.2;result = result + 3.3;

It has to benoted that theresult variable must be declared as amutable variable with themut notation. Mutable means that the variable can be changed. This is because Rust automatically assigns all variables as immutable unless we use themut notation.

Now thatwe haveseen the effects of types and mutability, we should really exploreintegersandfloats. Rust has two types of integers:signed integers, which are denoted byi, andunsigned integers, denoted byu. Unsignedintegers only house positive numbers, whereas signed integers house positive and negative integers. This does not just stop here. In Rust, we can alsodenote the size of the integer that is allowed. This can be calculated by using binary. Now, understanding how to use binary notation to describe numbers in detail is not really needed. However, understanding the simple rule that the sizecan be calculated by raising two to the power of thenumber of bits can give us an understanding of how big an integer is allowed to be. We can calculate all the integer sizes that we can utilize in Rust with the following table:

Table 1.1 – Size of integer types

Table 1.1 – Size of integer types

As we can see, we can get to very high numbers here. However, it is not the best idea to assign all variables and parameters asu128 integers. This is because the compiler will set aside this amount of memory each time when compiling. This is not very efficient considering that it's unlikely that we will be using such large numbers. It must be noted that the changes in each jump are so large it is pointless graphing it. Each jump in bits completely overshadows all the others, resulting in a flat line along thex axis and a huge spike at the last graphed number of bits. However, we also must be sure that our assignment is not too small. We can demonstrate this with the Rust code as follows:

let number: u8 = 255;let breaking_number: u8 = 256;  

Our compiler will be OK with thenumber variable. However, it will throw the error shown next when assigning thebreaking_number variable:

literal '256' does not fit into the type 'u8' whose range is '0..=255'

This is because there are 256 integers between 0 -> 255, as we include 0. We can change our unsigned integer to a signed one with the following line of Rust code:

let number: i8 = 255;

This gives us the following error:

literal '255' does not fit into the type 'i8' whose range is '-128..=127'

In this error, we arereminded that the bits are are allocated memory space. Therefore, ani8 integer must accommodate positive and negative integers within the same number of bits. As a result, we can only support a magnitude that is half of the integer of an unsigned integer.

When it comes to floats, our choices are more limited. Here, Rust accommodates bothf32 andf64 floating points. Declaring these floating-point variables requires the same syntax as integers:

let float: f32 = 20.6;

It must be noted that we can also annotate numbers with suffixes, as shown in the following code:

let x = 1u8;

Here,x has a value of 1 with the type ofu8. Now that we have covered floats and integers, we can use vectors and arrays to store them.

Managing data in Rust's vectors and arrays

With Python, we havelists. We can stuff anythingwe want into these lists with theappend function and these lists are, by default, mutable. Python tuples are technically not lists, but we can treat them as immutable arrays. With Rust, we havearrays andvectors. Arrays are the most basic of the two. Defining and looping through an array is straightforward in Rust, as we can see in the following code:

let array: [i32; 3] = [1, 2, 3];println!("array has {} elements", array.len());for i in array.iter() {    println!("{}", i);}

If we try and append another integer onto our array with thepush function, we will not be able toeven if the array is mutable. If we add a fourthelement to our array definition that is not an integer, the program will refuse to compile as all of the elements in the array have to be the same. However, this is not entirely true.

Later in this chapter, wewill coverstructs. In Python, the closest comparison to objects is structs as they have their own attributes and functions. Structs can also havetraits, whichwe will also discuss later. In terms of Python, the closest comparison totraits ismixins. Therefore, a range of structs can be housed in anarray if they all have the same trait in common. When looping through the array, the compiler will only allow us to execute functions from that trait as this is all we can ensure will be consistent throughout the array.

The same rules in terms of type or trait consistency also apply tovectors. However, vectors place their memory on the heap and are expandable. Like everything in Rust, they are, by default, immutable. However, applying themut tag will enable us to add and manipulate the vector. In the following code, we define a vector, print the length of the vector, append another element to the vector, and then loop through the vector printing all elements:

let mut str_vector: Vec<&str> = vec!["one", "two", \  "three"];println!("{}", str_vector.len());str_vector.push("four");for i in str_vector.iter() {    println!("{}", i);}

This gives us the following output:

3onetwothreefour

We can see that our append worked.

Consideringthe rules about consistency, vectors and arraysmight seem a little restrictive to a Python developer. However, if they are, sit back and ask yourself why. Why would you want to put in a range of elements that do not have any consistency? Although Python allows you to do this, how could you loop through a list with inconsistent elements and confidently perform operations on them without crashing the program?

With this in mind, we are starting to see the benefits and safety behind this restrictive typing system. There are some ways in which we can put in different elements that are not structs bound by the same trait. Considering this, we will explore how we can store and access our varied data elements via hashmaps in Rust in the next section.

Replacing dictionaries with hashmaps

Hashmaps in Rustare essentially dictionariesin Python. However, unlike our previous vectors and arrays, we want to have a range of different data types housed in a hashmap (although we can also do this with vectors and arrays). To achieve this, we can useEnums. Enums are, well, Enums, and we have the exact same concept in Python. However, instead of it being an Enum, we merely have a Python object that inherits theEnum object as seen in the following code:

from enum import Enum class Animal(Enum):    STRING = "string"    INT = "int"

Here, we can use the Enum to save us from usingraw strings in our Python code when picking aparticular category. With a code editor known as an IDE, this is very useful, but it's understandable if a Python developer has neverused them as they are not enforced anywhere. Not using them makes the code more prone to mistakes and harder to maintain when categories change and so on, but there is nothing in Python stopping the developer from just using a raw string to describe an option. In Rust, we are going to want our hashmap to accept strings and integers. To do this, we are going to have to carry out the following steps:

  1. Create an Enum to handle multiple data types.
  2. Create a new hashmap and insert values belonging to the Enum we created instep 1.
  3. Test the data consistency by looping through the hashmap and match all possible outcomes.
  4. Build a function that processes data extracted from the hashmap.
  5. Use the function to process outcomes from getting a value from the hashmap.

Therefore, we are going to create an Enum that houses this using the following code:

enum Value {    Str(&'static str),    Int(i32),}

Here, we can see that we have introduced the statement'static. This denotes alifetime and basically states that the reference remains for the rest of the program's lifetime. We will cover lifetimes in theKeeping track of scopes and lifetimes section.

Now thatwe have defined our Enum, we can buildour ownmutable hashmap andinsert an integer and a string into it with the following code:

use std::collections::HashMap;let mut map = HashMap::new();map.insert("one", Value::Str("1"));map.insert("two", Value::Int(2));

Now that our hashmap is housing a single type that houses the two types we defined, we must handle them.

Remember, Rust has strong typing. Unlike Python, Rust will not allow us to compile unsafe code (Rust can compile in an unsafe context but this is not default behavior). We must handle every possible outcome, otherwise the compiler will refuse to compile. We can do this with amatch statement as seen in the following code:

for (_key, value) in &map {    match value {        Value::Str(inside_value) => {            println!("the following value is an str: {}", \                 inside_value);        }        Value::Int(inside_value) => {            println!("the following value is an int: {}", \                 inside_value);        }    }}

In this code sample, we have looped through a borrowed reference to the hashmap using&. Again, we will cover borrowing later on in theUnderstanding variable ownership section. We prefix thekey with a_. This is telling the compiler that we are not going to use the key. We don't have to do this as the compiler will still compile the code; however, it will complain by issuing a warning. The value that we are retrieving from the hashmap is ourValue Enum. In thismatch statement, we can match the field of our Enum, and unwrap and access the inside value that we denote asinside_value, printing it to the console.

Runningthe code gives us the printout to the terminal as follows:

the following value is an int: 2the following value is an str: 1

It mustbe noted that Rust is not going to let anything slip by the compiler. If we remove the match for ourInt field for our Enum, then the compiler will throw the error seen here:

18 |           match value {   |           ^^^^^ pattern '&Int(_)' not covered   |   = help: ensure that all possible cases are being      handled,    possibly by adding wildcards or more match arms   = note: the matched value is of type '&Value'

This is because we have to handle every single possible outcome. Because we have been explicit that only values that can be housed in our Enum can be inserted into the hashmap, we know that there are only two possible types that can be extracted from our hashmap. We have nearly covered enough about hashmaps to use them effectively in Rust programs. One last concept that we must cover is the Enum calledOption.

Considering that we have arrays and vectors, we will not be using our hashmaps primarily for looping through outcomes. Instead, we will be retrieving values from them when we need them. Like in Python, the hashmap has aget function. In Python, if the key thatis being searched is not in the dictionary, then theget function will returnNone. It is then left to the developer to decidewhat to do with it. However, in Rust, the hashmap will return aSome orNone. To demonstrate this, let's try to get a value belonging to a key that we know is not there:

  1. Start by running the following code:
    let outcome: Option<&Value> = map.get("test");println!("outcome passed");let another_outcome: &Value = \    map.get("test").unwrap();println!("another_outcome passed");

    Here, we can see that we can access the reference to theValue Enum wrapped inOption with theget function. We then directly access the reference to theValue Enum using theunwrap function.

  2. However, we know that thetest key is not in the hashmap. Because of this, theunwrap function will cause the program to crash, as seen in the following output from the previous code:
    thread 'main' panicked at 'called 'Option::unwrap()' on a 'None' value', src/main.rs:32:51

    We can see that the simpleget function did not crash the program. However, we didn't manage to get the string"another_outcome passed" to print out to the console. We can handle this with amatch statement.

    However, this is going to be amatch statement within amatch statement.

  3. In orderto reduce the complexity, we shouldexplore Rust functions to process ourvalue Enum. This can be done with the following code:
    fn process_enum(value: &Value) -> () {    match value {        Value::Str(inside_value) => {            println!("the following value is an str: \              {}", inside_value);        }        Value::Int(inside_value) => {            println!("the following value is an int: \              {}", inside_value);        }    }}

    The function does not really give us any new logic to explore. The-> () expression is merely stating that the function is not returning anything.

  4. If we are going to return a string, for instance, the expression would be-> String. We do not need the-> () expression; however, it can be helpful for developers to quickly understand what's going on with the function. We can then use this function to process the outcome from ourget function with the following code:
    match map.get("test") {    Some(inside_value) => {        process_enum(inside_value);    }    None => {        println!("there is no value");    }}

We nowknow enough to utilize hashmaps in ourprograms. However, we must notice that we have not really handled errors; we have either printed out that nothing was found or let theunwrap function just result in an error. Considering this, we will move on to the next section on handling errors in Rust.

Error handling in Rust

Handling errorsin Python is straightforward. We have atry block thathouses anexcept block underneath. In Rust, we have aResult wrapper. This works in the same way as anOption. However, instead of havingSome orNone, we haveOk orErr.

To demonstrate this, we can build on the hashmap that was defined in the previous section. We acceptOption from aget function applied to the hashmap. Our function will check to see whether the integer retrieved from the hashmap is above a threshold. If it's above the threshold, we will return a true value. If not, then it is false.

The problem is that there might not be a value inOption. We also know that theValue Enum might not be an integer. If any of this is the case, we should return an error. If not, we return a Boolean. This function can be seen here:

fn check_int_above_threshold(threshold: i32,     get_result: Option<&Value>) -> Result<bool, &'static \      str> {    match get_result {      Some(inside_value) => {        match inside_value {          Value::Str(_) => return Err(            "str value was supplied as opposed to \              an int which is needed"),                Value::Int(int_value) => {                    if int_value > &threshold {                        return Ok(true)                    }                    return Ok(false)                }             }        }        None => return Err("no value was supplied to be \          checked")    }}

Here, we cansee that theNone result fromOption instantly returns anerror with a helpful message as to why we are returning an error. With theSome value, we utilize anothermatch statement to return an error with a helpful message that we cannot supply a string to check the threshold if theValue is a string. It must be noted thatValue::Str(_) has a_ in it. This means that we do not care what the value is because we are not going to use it. In the final part, we check to see whether the integer is above the threshold returningOk values that are either true or false. We implement this function with the following code:

let result: Option<&Value> = map.get("two");let above_threshold: bool = check_int_above_threshold(1, \    result).unwrap();println!("it is {} that the threshold is breached", \    above_threshold);

This gives us the following output in the terminal:

it is true that the threshold is breached

If we up the first parameter in ourcheck_int_above_threshold function to 3, we get the following output:

it is false that the threshold is breached

If wechange the key inmap.get tothree, we get the followingterminal output:

thread 'main' panicked at 'called 'Result::unwrap()' on an 'Err' value: "no value was supplied to be checked"'

If we change the key inmap.get toone, we get the following terminal output:

thread 'main' panicked at 'called 'Result::unwrap()' on an 'Err' value: "str value was supplied as opposed to an int

We can add extra signposting to the unwrap with theexpect function. This function unwraps the result and adds an extra message to the printout if there is an error. With the following implementation, the message"an error happened" will be added to the error message:

let second_result: Option<&Value> = map.get("one");let second_threshold: bool = check_int_above_threshold(1, \    second_result).expect("an error happened");

We can also directly throw an error if needed with the following code:

panic!("throwing some error");

We can also check to see whether the result is an error by using theis_err function as seen here:

result.is_err()

This returns abool, enabling us to alter the direction of our program if we come across an error. As we can see, Rust gives us a range of ways in which we can throw and manage errors.

We cannow handle enough of Rust's quirks to write basic scripts. However, if the program gets a little more complicated, we fall into other pitfalls such asvariable ownership and lifetimes. In the next section, we cover the basics of variable ownership so we can continue to use our variables throughout a range of functions and structs.

Understanding variable ownership

As we pointed out in the introduction discussing why we should use Rust, Rust doesn't have a garbage collector; however, it is still memory-safe. We do this to keep the resourceslow and the speed high. However, how do we achieve memory safety without a garbage collector? Rust achieves this by enforcing some strict rules around variable ownership.

Like typing, these rules are enforced when the code is being compiled. Any violation of these rules will stop the compilation process. This can lead to a lot of initial frustration for Python developers, as Python developers like to use their variables as and when they want. If they pass a variable into a function, they also expect that variable to still be able to be mutated outside the function if they want. This can lead to issues when implementing concurrent executions. Python also allows this by running expensive processes under the hood to enable the multiple references with cleanup mechanisms when the variable is no longer referenced.

As a result, this mismatch in coding style gives Rust the false label of having a steep learning curve. If we learn the rules, we only must rethink our code a little, as the helpful compiler enables us to adhere to them easily. You'll also be surprised how this approach is not as restrictive as it sounds. Rust's compile-time checking is done to protect against the following memory errors:

  • Use after frees: This is where memory is accessed once it has been freed, which can cause crashes. It can also allow hackers to execute code via this memory address.
  • Dangling pointers: This is where a reference points to a memory address that no longer houses the data that the pointer was referencing. Essentially, this pointer now points to null or random data.
  • Double frees: This is where allocated memory is freed, and then freed again. This can cause the program to crash and increases the risk of sensitive data being revealed. This also enables a hacker to execute arbitrary code.
  • Segmentation faults: This is where the program tries to access the memory it's not allowed to access.
  • Buffer overrun: An example of this is reading off the end of an array. This can cause the program to crash.

Rust managesto protect against these errors by enforcing the following rules:

  • Values are owned by the variables assigned to them.
  • As soon as the variable goes out of scope, it is deallocated from the memory it is occupying.
  • Values can be used by other variables, if we adhere to the conventions around copying, moving, immutable borrowing, and mutable borrowing.

To really feel comfortable navigating these rules in code, we will explore copying, moving, immutable borrowing, and mutable borrowing in more detail.

Copy

This iswhere the value is copied. Once it has been copied, the new variable owns the value, and the existing variable also owns its own value:

Figure 1.3 – Variable Copy path

Figure 1.3 – Variable Copy path

As wecan see with the pathway diagram inFigure 1.3, we can continue to use both variables. If the variable has aCopy trait, the variable will automatically copy the value. This can be achieved by the following code:

let one: i8 = 10;let two: i8 = one + 5;println!("{}", one);println!("{}", two);

The fact that we can print out both theone andtwo variables means we know thatone has been copied and the value of this copy has been utilized bytwo.Copy is the simplest reference operation; however, if the variable being copied does not have aCopy trait, then the variable must be moved. To understand this, we will now explore moving as a concept.

Move

This is wherethe value is moved from one variable to another. However, unlikeCopy, the original variable no longer owns the value:

Figure 1.4 – Variable Move path

Figure 1.4 – Variable Move path

Looking at the path diagram inFigure 1.4, we can see thatone can no longer be used as it's beenmoved totwo. We mentioned in theCopy section that if the variable does not have theCopy trait, then the variable is moved. In the following code, we show this by doing what we did in theCopy section but usingString as this does not have aCopy trait:

let one: String = String::from("one");let two: String = one + " two";println!("{}", two);println!("{}", one);

Running this gives the following error:

let one: String = String::from("one");    --- move occurs because 'one' has type     'String', which does not implement the     'Copy' traitlet two: String = one + " two";        ------------ 'one' moved due to usage in operatorprintln!("{}", two);println!("{}", one);        ^^^ value borrowed here after move

This is really where the compiler shines. It tells us that the string does not implement theCopy trait. It then shows us where the move occurs. It is no surprise that many developerspraise the Rust compiler. We can get round this by using theto_owned function with the following code:

let two: String = one.to_owned() + " two";

It is understandable to wonder why Strings do not have theCopy trait. This is because the string is a pointer to a string slice. Copying actually means copying bits. Considering this, if we were to copy strings, we would have multiple unconstrained pointers to the same string literal data, which would be dangerous. Scope also plays a role when it comes to moving variables. In order to see how scope forces movement, we need to explore immutable borrows in the next section.

Immutable borrow

This is where one variable can reference the value of another variable. If the variable that isborrowing the value falls out of scope, the value is not deallocated from memory as the variable borrowing the value does not have ownership:

Figure 1.5 – Immutable borrow path

Figure 1.5 – Immutable borrow path

We can see with the path diagram inFigure 1.5 thattwo borrows the value fromone. When this is happening,one is kind of locked. We can still copy and borrowone; however, we cannot do a mutable borrow or move whiletwo is still borrowing the value. This is because if we have mutable and immutable borrows of the same variable, the data of that variable could change through the mutable borrow causing an inconsistency. Considering this, we can see that we can have multiple immutable borrows atone time while only having one mutable borrow at any one time. Oncetwo is finished, we can do anything we want toone again. To demonstrate this, we can go back to creating our ownprint function with the following code:

fn print(input_string: String) -> () {    println!("{}", input_string);}

With this, we create a string and pass it through ourprint function. We then try and print the string again, as seen in the following code:

let one: String = String::from("one");print(one);println!("{}", one);

If we try and run this, we will get an error stating thatone was moved into ourprint function and therefore cannot be used inprintln!. We can solve this by merely accepting a borrow of a string using& in our function, as denoted in the following code:

fn print(input_string: &String) -> () {    println!("{}", input_string);}

Now we can pass a borrowed reference into ourprint function. After this, we can still access the| variable, as seen in the following code:

let one: String = String::from("one");print(&one);let two: String = one + " two";println!("{}", two);

Borrows are safe and useful. As our programs grow, immutable borrows are safe ways to passvariables through to other functions in other files. We are nearly at the end of our journey toward understanding the rules. The only concept left that we must explore is mutable borrows.

Mutable borrow

This is where another variable can reference and write the value of another variable. If the variable that is borrowing the value falls out of scope, the value is not deallocated from memoryas the variable borrowing the value does not have ownership. Essentially, a mutable borrow has the same path as an immutable borrow. The only difference is that while the value is being borrowed, the original variable cannot be used at all. It will be completely locked down as the value might be altered when being borrowed. The mutable borrow can be moved into another scope like a function, but cannot be copied as we cannot have multiple mutable references, as stated in the previous section.

Considering all that we have covered on borrowing, we can see a certain theme. We can see that scopes play a big role in implementing the rules that we have covered. If the concept of scopes is unclear, passing a variable into a function is changing scope as a function is its own scope. To fully appreciate this, we need to move on to exploring scopes and lifetimes.

Keeping track of scopes and lifetimes

In Python, we do have the concept ofscope. It is generally enforced in functions. For instance, we cancall 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 thetotal 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 definescopes 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 variableone can be accessed in the inner-scope as it was defined outside the outer-scope. However, the variabletwo is defined in the inner-scope. Once the inner-scope is finished, we can see by the error that we cannot access the variabletwo 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: 1main scope: 2

With this, we cansee 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 borrowedprintln!("r: {}", one);                  --- borrow later used here

What has happened here is that we state that there is a variable calledone. We then define an inner-scope. Inside this scope, we define an integertwo. We then assignone to be a reference oftwo. When we try and printone in the outer-scope, we can't, as the variable it ispointing 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 oftwo is shorter than the lifetime ofone.

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 usea,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 wecan see, thefirst_number andsecond_number variables have the same lifetime notation ofa. This means that they have the same lifetimes. We also have to note that theget_highest function returns ani8 with a lifetime ofa. As a result, bothfirst_number andsecond_number variables can be returned, which means that we cannot use theoutcome variable outside of the inner-scope. However, we know that our lifetimes between the variablesone andtwo are different. If we want to utilize theoutcome 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 lifetimea is returned. Therefore, the parameter with the lifetimeb can be defined in the inner-scope as we are not returning it in the function. Considering this, we can seethat 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.

Building structs instead of objects

In Python, we usea lot of objects. In fact, everything you work with in Python is an object. In Rust, the closest thing we can get to objects is structs. To demonstrate this, let's build an object in Python, and then replicate the behavior in Rust. For our example, we will build a basic stock object as seen in the following code:

class Stock:    def __init__(self, name: str, open_price: float,\      stop_loss: float = 0.0, take_profit: float = 0.0) \        -> None:        self.name: str = name        self.open_price: float = open_price        self.stop_loss: float = stop_loss        self.take_profit: float = take_profit        self.current_price: float = open_price    def update_price(self, new_price: float) -> None:        self.current_price = new_price

Here, we can see that we have two mandatory fields, which are the name and price of the stock. We can also have an optional stop loss and an optional take profit. This means that if the stockcrosses one of these thresholds, it forces a sale, so we don't continue to lose more money or keep letting the stock rise to the point where it crashes. We then have a function that merely updates the current price of the stock. We could add extra logic here on the thresholds for it to return a bool for whether the stock should be sold or not if needed. For Rust, we define the fields with the following code:

struct Stock {    name: String,    open_price: f32,    stop_loss: f32,    take_profit: f32,    current_price: f32}

Now we have our fields for the struct, we need to build the constructor. We can build functions that belong to our struct with animpl block. We build our constructor with the following code:

impl Stock {    fn new(stock_name: &str, price: f32) -> Stock {        return Stock{            name: String::from(stock_name),             open_price: price,            stop_loss: 0.0,            take_profit: 0.0,            current_price: price        }    }}

Here, we cansee that we have defined some default values for some of the attributes. To build an instance, we use the following code:

let stock: Stock = Stock::new("MonolithAi", 95.0);

However, we have not exactly replicated our Python object. In the Python object__init__, there were some optional parameters. We can do this by adding the following functions to ourimpl block:

    fn with_stop_loss(mut self, value: f32) -> Stock {        self.stop_loss = value;        return self    }    fn with_take_profit(mut self, value: f32) -> Stock {        self.take_profit = value;        return self    }

What we do here is take in our struct, mutate the field, and then return it. Building a new stock with a stop loss involves calling our constructor followed by thewith_stop_loss function as seen here:

let stock_two: Stock = Stock::new("RIMES",\    150.4).with_stop_loss(55.0);

With this, our RIMES stock will have an open price of150.4, current price of150.4, and a stop loss of55.0. We can chain multiple functions as they return the stock struct. We can create a stockstruct with a stop loss and a take profit with the following code:

let stock_three: Stock = Stock::new("BUMPER (former known \  as ASF)", 120.0).with_take_profit(100.0).\    with_stop_loss(50.0);

We can continue chaining with as many optional variables as we want. This also enables us to encapsulate the logic behind defining these attributes. Now that we have all our constructor needs sorted, we need to edit theupdate_price attribute. This can be done by implementing the following function in theimpl block:

fn update_price(&mut self, value: f32) {    self.current_price = value;}

This can be implemented with the following code:

let mut stock: Stock = Stock::new("MonolithAi", 95.0);stock.update_price(128.4);println!("here is the stock: {}", stock.current_price);

It has to be noted that the stock needs to be mutable. The preceding code gives us the following printout:

here is the stock: 128.4

There is only one concept left to explore for structs and this is traits. As we have stated before, traits are like Python mixins. However,traits can also act as a data type because we know that a struct that has the trait has those functions housed in the trait. To demonstrate this, we can create aCanTransfer trait in the following code:

trait CanTransfer {    fn transfer_stock(&self) -> ();        fn print(&self) -> () {        println!("a transfer is happening");    }}

If we implementthe trait for a struct, the instance of the struct can utilize theprint function. However, thetransfer_stock function doesn't have a body. This means that we must define our own function if it has the same return value. We can implement the trait for our struct using the following code:

impl CanTransfer for Stock {    fn transfer_stock(&self) -> () {        println!("the stock {} is being transferred for \          £{}", self.name, self.current_price);    }}

We can now use our trait with the following code:

let stock: Stock = Stock::new("MonolithAi", 95.0);stock.print();stock.transfer_stock();

This gives us the following output:

a transfer is happeningthe stock MonolithAi is being transferred for £95

We can make our own function that will print and transfer the stock. It will accept all structs that implement ourCanTransfer trait and we can use all the trait's functions in it, as seen here:

fn process_transfer(stock: impl CanTransfer) -> () {    stock.print();    stock.transfer_stock();}

We can see thattraits are a powerful alternative to object inheritance; they reduce the amount of repeated code for structs that fit in the same group. There is no limit to the number of traits that a struct can implement. This enables us to plug traits in and out, adding a lot of flexibility to our structs when maintaining code.

Traits are not the only way by which we can manage how structs interact with the rest of the program; we can achieve metaprogramming with macros, which we will explore in the next section.

Metaprogramming with macros instead of decorators

Metaprogramming can generally be described as a way in which the program can manipulateitself based on certain instructions. Consideringthe 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 thesimplest form of metaprogramming, another form of metaprogramming in Rust ismacros.

You mayhave noticed throughout the chapter that some of the functions that we use, such as theprintln! function, have an! at the end. This isbecause 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 amatch 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 thefn term that is used for defining functions, we define our macro usingmacro_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 thex 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, wemust remember that macros are a last resort. Our example shows that our macro alters the state even though it is not directlydemonstrated in themain 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 ourdecorators 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 theprint function that we created and therefore we cannot use it in the finalprintln!. 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 aCopy trait. Now, we could try andcode aCopy trait ourselves, but thiswould be convoluted and would require advanced knowledge. Luckily for us, we can implement theCopy andClone traits by utilizing aderive 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. Macroscan be utilized by many packages and frameworks, fromJavaScript 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 surprisingas a decorator in Python is a form of metaprogramming that wraps a function.

This wrapsup 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.

Summary

In this chapter, we explored the role of Rust in today's landscape, showing that Rust's paradigm-changing position is a result of being memory-safe, while not having any garbage collection. With this, we understood why it beats most languages (including Golang) when it comes to speed. We then went over the quirks that Rust has when it comes to strings, lifetimes, memory management, and typing, so we can write safe and efficient Rust code as Python developers. We then covered structs and traits to the point where we could mimic the basic functionality of a Python object with mixins, utilizing their traits as types for the Rust struct while we were at it.

We covered the basic concepts of lifetimes and borrowing. This enables us to have more control over how we implement our structs and functions within our program, giving us multiple avenues to turn to when solving a problem. With all this, we can safely code single-page applications with confidence over concepts that would stump someone who has never coded in Rust. However, we know, as experienced Python developers, that any serious program worth coding spans multiple pages. Considering this, we can use what we have learned here to move on to the next chapter, where we set up a Rust environment on our own computers and learn how to structure Rust code over multiple files, enabling us to get one step closer to building packages in Rust and installing them withpip.

Questions

  1. Why can we not simply copy a String?
  2. Rust has strong typing. In which two ways can we enable a container such as a vector or hashmap to contain multiple different types?
  3. How are Python decorators and Rust macros the same?
  4. What is the Python equivalent to amain function in Rust?
  5. Why can we get a higher integer value with the same number of bytes with an unsigned integer than a signed integer?
  6. Why do we have to be strict with lifetimes and scopes when coding in Rust?
  7. Can we reference a variable when it has been moved?
  8. What can you do to an original variable if it is currently being borrowed in an immutable state?
  9. What can you do to an original variable if it is currently being borrowed in a mutable state?

Answers

  1. This is because a String is essentially a pointer toVec<u8> with some metadata. If we copy this, then we will have multiple unconstrained pointers to the same string literal, which will introduce errors with concurrency, mutability, and lifetimes.
  2. We can use an Enum, which means that the type being accepted into the container can be one of those types housed in the Enum. When reading the data, we can then use amatch statement to manage all possible data types that could be read from the container. The second way is to create a trait that multiple different structs implement. However, the only interaction that we can have from the container read when this is the case is the functions that the trait implements.
  3. They both wrap around the code and alter the implementation or attributes of the code that they are wrapping without directly returning anything.
  4. The Python equivalent isif __name__ == "__main__":.
  5. A signed integer must accommodate positive and negative values, whereas an unsigned integer only accommodates positive values.
  6. This is because there is no garbage collection; as a result, variables get deleted when they shift out of the scope of where they were created. If we do not consider lifetimes, we could reference a variable that has been deleted.
  7. No, the ownership of the variable has essentially been moved and there are no references to the original variable anymore.
  8. We can still copy and borrow the original variable; however, we cannot perform a mutable borrow.
  9. We cannot use the original variable at all as the state of the variable might be altered.

Further reading

  • Hands-On Functional Programming in Rust (2018) by Andrew Johnson, Packt Publishing
  • Mastering Rust (2019) by Rahul Sharma and Vesa Kaihlavirta, Packt Publishing
  • The Rust Programming Language (2018):https://doc.rust-lang.org/stable/book/
Left arrow icon

Page1 of 11

Right arrow icon
Download code iconDownload Code

Key benefits

  • Learn to implement Rust in a Python system without altering the entire system
  • Write safe and efficient Rust code as a Python developer by understanding the essential features of Rust
  • Build Python extensions in Rust by using Python NumPy modules in your Rust code

Description

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.

Who is this book for?

This book is for Python developers who want to speed up their Python code with Rust and implement Rust in a Python system without altering the entire system. You'll be able to learn about all topics relating to Rust programming. Basic knowledge of Python is required to get the most out of this book.

What you will learn

  • Explore the quirks of the Rust programming language that a Python developer needs to understand to code in Rust
  • Understand the trade-offs for multiprocessing and thread safety to write concurrent code
  • Build and manage a software project with cargo and crates
  • Fuse Rust code with Python so that Python can import and run Rust code
  • Deploy a Python Flask application in Docker that utilizes a private Rust pip module
  • Inspect and create your own Python objects in Rust

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date :Jan 21, 2022
Length:384 pages
Edition :1st
Language :English
ISBN-13 :9781801812320
Category :

What do you get with eBook?

Product feature iconInstant access to your Digital eBook purchase
Product feature icon Download this book inEPUB andPDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature iconDRM FREE - Read whenever, wherever and however you want
Product feature iconAI Assistant (beta) to help accelerate your learning
OR

Contact Details

Modal Close icon
Payment Processing...
tickCompleted

Billing Address

Product Details

Publication date :Jan 21, 2022
Length:384 pages
Edition :1st
Language :English
ISBN-13 :9781801812320
Category :
Concepts :

Packt Subscriptions

See our plans and pricing
Modal Close icon
$19.99billed monthly
Feature tick iconUnlimited access to Packt's library of 7,000+ practical books and videos
Feature tick iconConstantly refreshed with 50+ new titles a month
Feature tick iconExclusive Early access to books as they're written
Feature tick iconSolve problems while you work with advanced search and reference features
Feature tick iconOffline reading on the mobile app
Feature tick iconSimple pricing, no contract
$199.99billed annually
Feature tick iconUnlimited access to Packt's library of 7,000+ practical books and videos
Feature tick iconConstantly refreshed with 50+ new titles a month
Feature tick iconExclusive Early access to books as they're written
Feature tick iconSolve problems while you work with advanced search and reference features
Feature tick iconOffline reading on the mobile app
Feature tick iconChoose a DRM-free eBook or Video every month to keep
Feature tick iconPLUS own as many other DRM-free eBooks or Videos as you like for just Can$6 each
Feature tick iconExclusive print discounts
$279.99billed in 18 months
Feature tick iconUnlimited access to Packt's library of 7,000+ practical books and videos
Feature tick iconConstantly refreshed with 50+ new titles a month
Feature tick iconExclusive Early access to books as they're written
Feature tick iconSolve problems while you work with advanced search and reference features
Feature tick iconOffline reading on the mobile app
Feature tick iconChoose a DRM-free eBook or Video every month to keep
Feature tick iconPLUS own as many other DRM-free eBooks or Videos as you like for just Can$6 each
Feature tick iconExclusive print discounts

Frequently bought together


Game Development with Rust and WebAssembly
Game Development with Rust and WebAssembly
Read more
Apr 2022476 pages
Full star icon3.3 (6)
eBook
eBook
Can$39.99Can$44.99
Can$55.99
Speed Up Your Python with Rust
Speed Up Your Python with Rust
Read more
Jan 2022384 pages
Full star icon4.9 (14)
eBook
eBook
Can$39.99Can$44.99
Can$55.99
Practical WebAssembly
Practical WebAssembly
Read more
May 2022232 pages
Full star icon5 (4)
eBook
eBook
Can$46.99Can$52.99
Can$65.99
Stars icon
TotalCan$177.97
Game Development with Rust and WebAssembly
Can$55.99
Speed Up Your Python with Rust
Can$55.99
Practical WebAssembly
Can$65.99
TotalCan$177.97Stars icon

Table of Contents

15 Chapters
Section 1: Getting to Understand RustChevron down iconChevron up icon
Section 1: Getting to Understand Rust
Chapter 1: An Introduction to Rust from a Python PerspectiveChevron down iconChevron up icon
Chapter 1: An Introduction to Rust from a Python Perspective
Technical requirements
Understanding the differences between Python and Rust
Understanding variable ownership
Keeping track of scopes and lifetimes
Building structs instead of objects
Metaprogramming with macros instead of decorators
Summary
Questions
Answers
Further reading
Chapter 2: Structuring Code in RustChevron down iconChevron up icon
Chapter 2: Structuring Code in Rust
Technical requirements
Managing our code with crates and Cargo instead of pip
Structuring code over multiple files and modules
Building module interfaces
Interacting with the environment
Summary
Questions
Answers
Further reading
Chapter 3: Understanding ConcurrencyChevron down iconChevron up icon
Chapter 3: Understanding Concurrency
Technical requirements
Introducing concurrency
Basic asynchronous programming with threads
Running multiple processes
Customizing threads and processes safely
Summary
Questions
Answers
Further reading
Section 2: Fusing Rust with PythonChevron down iconChevron up icon
Section 2: Fusing Rust with Python
Chapter 4: Building pip Modules in PythonChevron down iconChevron up icon
Chapter 4: Building pip Modules in Python
Technical requirements
Configuring setup tools for a Python pip module
Packaging Python code in a pip module
Configuring continuous integration
Summary
Questions
Answers
Further reading
Chapter 5: Creating a Rust Interface for Our pip ModuleChevron down iconChevron up icon
Chapter 5: Creating a Rust Interface for Our pip Module
Technical requirements
Packaging Rust with pip
Building a Rust interface with the pyO3 crate
Building tests for our Rust package
Comparing speed with Python, Rust, and Numba
Summary
Questions
Answers
Further reading
Chapter 6: Working with Python Objects in RustChevron down iconChevron up icon
Chapter 6: Working with Python Objects in Rust
Technical requirements
Passing complex Python objects into Rust
Inspecting and working with custom Python objects
Constructing our own custom Python objects in Rust
Summary
Questions
Answers
Further reading
Chapter 7: Using Python Modules with RustChevron down iconChevron up icon
Chapter 7: Using Python Modules with Rust
Technical requirements
Exploring NumPy
Building a model in NumPy
Using NumPy and other Python modules in Rust
Recreating our NumPy model in Rust
Summary
Questions
Answers
Further reading
Chapter 8: Structuring an End-to-End Python Package in RustChevron down iconChevron up icon
Chapter 8: Structuring an End-to-End Python Package in Rust
Technical requirements
Breaking down a catastrophe modeling problem for our package
Building an end-to-end solution as a package
Utilizing and testing our package
Summary
Further reading
Section 3: Infusing Rust into a Web ApplicationChevron down iconChevron up icon
Section 3: Infusing Rust into a Web Application
Chapter 9: Structuring a Python Flask App for RustChevron down iconChevron up icon
Chapter 9: Structuring a Python Flask App for Rust
Technical requirements
Building a basic Flask application
Defining our data access layer
Building a message bus
Summary
Questions
Answers
Further reading
Chapter 10: Injecting Rust into a Python Flask AppChevron down iconChevron up icon
Chapter 10: Injecting Rust into a Python Flask App
Technical requirements
Fusing Rust into Flask and Celery
Deploying Flask and Celery with Rust
Deploying with a private GitHub repository
Fusing Rust with data access
Deploying Rust nightly in Flask
Summary
Questions
Answers
Further reading
Chapter 11: Best Practices for Integrating RustChevron down iconChevron up icon
Chapter 11: Best Practices for Integrating Rust
Technical requirements
Keeping our Rust implementation simple by piping data to and from Rust
Giving the interface a native feel with objects
Keeping data-parallelism simple with Rayon
Further reading
Why subscribe?
Other Books You May EnjoyChevron down iconChevron up icon
Other Books You May Enjoy
Packt is searching for authors like you
Share Your Thoughts

Recommendations for you

Left arrow icon
Debunking C++ Myths
Debunking C++ Myths
Read more
Dec 2024226 pages
Full star icon5 (1)
eBook
eBook
Can$35.99Can$40.99
Can$50.99
Go Recipes for Developers
Go Recipes for Developers
Read more
Dec 2024350 pages
eBook
eBook
Can$35.99Can$40.99
Can$50.99
50 Algorithms Every Programmer Should Know
50 Algorithms Every Programmer Should Know
Read more
Sep 2023538 pages
Full star icon4.5 (68)
eBook
eBook
Can$44.99Can$50.99
Can$63.99
Can$63.99
Asynchronous Programming with C++
Asynchronous Programming with C++
Read more
Nov 2024424 pages
Full star icon5 (1)
eBook
eBook
Can$37.99Can$42.99
Can$53.99
Modern CMake for C++
Modern CMake for C++
Read more
May 2024504 pages
Full star icon4.7 (12)
eBook
eBook
Can$44.99Can$50.99
Can$63.99
Learn Python Programming
Learn Python Programming
Read more
Nov 2024616 pages
Full star icon5 (1)
eBook
eBook
Can$35.99Can$40.99
Can$50.99
Learn to Code with Rust
Learn to Code with Rust
Read more
Nov 202457hrs 40mins
Video
Video
Can$94.99
Modern Python Cookbook
Modern Python Cookbook
Read more
Jul 2024818 pages
Full star icon4.9 (21)
eBook
eBook
Can$49.99Can$55.99
Can$69.99
Right arrow icon

Customer reviews

Top Reviews
Rating distribution
Full star iconFull star iconFull star iconFull star iconHalf star icon4.9
(14 Ratings)
5 star92.9%
4 star7.1%
3 star0%
2 star0%
1 star0%
Filter icon Filter
Top Reviews

Filter reviews by




Wei Ji LeongJan 25, 2024
Full star iconFull star iconFull star iconFull star iconFull star icon5
The book has been really good for picking up Rust from a Pythonista perspective. It does a comprehensive job at walking through setups for those that might not have done Python package management before, but it may be good still to come with some experience to pick things up quicker. Overall, would recommend!
Feefo Verified reviewFeefo
Imre BekeFeb 06, 2024
Full star iconFull star iconFull star iconFull star iconFull star icon5
Szeretném a Python környezetben nagy adatállományok feldolgozását gyorsítani, minden olyan eszköz lehetőség ami ebben lehetőséget rejt az érdekel és ki szeretném próbálni.
Feefo Verified reviewFeefo
Priya R ShastriFeb 28, 2022
Full star iconFull star iconFull star iconFull star iconFull star icon5
In 11 chapters, the author Maxwell an experienced software engineer goes from Introduction to RUST to using them in advance.The first chapter introduces RUST as a programming language and how it can be easily used when used with Python and can be installed using pip.All the examples in the book work with a Python interpreter and Rust interpreter.In chapter 2 the author discusses structuring the code in RUST. The file can be listed as hello_world.rs . Using cargo one can compile the RUST file. The author highlughts the benefilts of documentation while coding.In chapter 3 Understanding concurrency, basics of concurrent programming are discussed. In chapter 4 Building pip modules in python are discussed. In chapter 5 creating a rust interface for the pip module details of the rust interface are discussed. Use of a python adapter to test rust binary file is discussed. In chapter 6 working with python objects are discussed. In the chapters , 8, 9 the creation of a flask application for RUST are discussed. In chapter 11 best practices for integrating RUST in python are stated.Overall good book for using RUST with python adapter. Useful for data applications using PYthon and RUST.
Amazon Verified reviewAmazon
DmitryMar 19, 2022
Full star iconFull star iconFull star iconFull star iconFull star icon5
The book starts by giving you an introduction to Rust. You will like this introduction if you already have some experience with programming languages(I assume you know Python), as it skips the boring basics like for instance the loops. Can’t promise you’ll become a Rust guru after reading the first chapter. I, for instance, am still blurry on the concept of borrowed variables, but I can read the code and even managed to write a few programs in Rust after reading this book.The rest of the book will tell you how to put Rust code into your Python project, how to structure the project, how to pass objects and call methods between these two languages. A lot of examples in the book are simple algorithms that you will speed up by simply using Rust instead of Python. The book also warns against the cases where you won’t get any speed improvement from the rewrite. Some examples in the book go beyond simple algorithmic challenges and cover how you can make Rust code part of your Flask API.
Amazon Verified reviewAmazon
Eduard StefanescuFeb 06, 2022
Full star iconFull star iconFull star iconFull star iconFull star icon5
This book is really beneficial to a Python developer who wishes to create apps or services with Rust. There is a straightforward transition that keeps Python and pip packages in mind while developing apps with Rust.In addition to the transition examples and applications, Maxwell Flitton explains the advantages of utilizing Rust rather than Python in certain scenarios.It was my first book and my first opportunity to learn Rust, and I can now see much more clearly where there is a rationale for Python and when there is a case for Rust.This book is a gem for a Python developer who wants to learn Rust. It will help you not only learn how to develop apps with Rust but also offer insights into Rust's capabilities.
Amazon Verified reviewAmazon
  • Arrow left icon Previous
  • 1
  • 2
  • 3
  • Arrow right icon Next

People who bought this also bought

Left arrow icon
50 Algorithms Every Programmer Should Know
50 Algorithms Every Programmer Should Know
Read more
Sep 2023538 pages
Full star icon4.5 (68)
eBook
eBook
Can$44.99Can$50.99
Can$63.99
Can$63.99
Event-Driven Architecture in Golang
Event-Driven Architecture in Golang
Read more
Nov 2022384 pages
Full star icon4.9 (11)
eBook
eBook
Can$44.99Can$50.99
Can$63.99
The Python Workshop Second Edition
The Python Workshop Second Edition
Read more
Nov 2022600 pages
Full star icon4.6 (22)
eBook
eBook
Can$46.99Can$52.99
Can$65.99
Template Metaprogramming with C++
Template Metaprogramming with C++
Read more
Aug 2022480 pages
Full star icon4.6 (14)
eBook
eBook
Can$42.99Can$47.99
Can$59.99
Domain-Driven Design with Golang
Domain-Driven Design with Golang
Read more
Dec 2022204 pages
Full star icon4.4 (19)
eBook
eBook
Can$40.99Can$45.99
Can$56.99
Right arrow icon

About the author

Profile icon Maxwell Flitton
Maxwell Flitton
LinkedIn iconGithub icon
Maxwell Flitton is a software engineer who works for the open source financial loss modeling foundation OasisLMF. In 2011, Maxwell achieved his Bachelor of Science degree in nursing from the University of Lincoln, UK. While working 12-hour shifts in the A&E departments of hospitals, Maxwell obtained another degree in physics from the Open University in the UK and then moved on to another milestone, with a postgraduate diploma in physics and engineering in medicine from UCL in London. He's worked on numerous projects such as medical simulation soft ware for the German government and supervising computational medicine students at Imperial College London. He also has experience in financial tech and Monolith AI.
Read more
See other products by Maxwell Flitton
Getfree access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

How do I buy and download an eBook?Chevron down iconChevron up icon

Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.

If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.

Please Note: Packt eBooks are non-returnable and non-refundable.

Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:

  • You may make copies of your eBook for your own use onto any machine
  • You may not pass copies of the eBook on to anyone else
How can I make a purchase on your website?Chevron down iconChevron up icon

If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:

  1. Register on our website using your email address and the password.
  2. Search for the title by name or ISBN using the search option.
  3. Select the title you want to purchase.
  4. Choose the format you wish to purchase the title in; if you order the Print Book, you get a free eBook copy of the same title. 
  5. Proceed with the checkout process (payment to be made using Credit Card, Debit Cart, or PayPal)
Where can I access support around an eBook?Chevron down iconChevron up icon
  • If you experience a problem with using or installing Adobe Reader, the contact Adobe directly.
  • To view the errata for the book, see www.packtpub.com/support and view the pages for the title you have.
  • To view your account details or to download a new copy of the book go to www.packtpub.com/account
  • To contact us directly if a problem is not resolved, use www.packtpub.com/contact-us
What eBook formats do Packt support?Chevron down iconChevron up icon

Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.

You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.

What are the benefits of eBooks?Chevron down iconChevron up icon
  • You can get the information you need immediately
  • You can easily take them with you on a laptop
  • You can download them an unlimited number of times
  • You can print them out
  • They are copy-paste enabled
  • They are searchable
  • There is no password protection
  • They are lower price than print
  • They save resources and space
What is an eBook?Chevron down iconChevron up icon

Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.

When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.

For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.


[8]ページ先頭

©2009-2025 Movatter.jp