Rust for Python Programmers #5 — Lists
Welcome back, fellow Pythonistas-turned-Rust enthusiasts! In this edition of “Rust for Python Programmers,” we’re shifting our focus to another fundamental data type: lists. While Python’s lists are a staple for any Python programmer, Rust offers alternatives with its array and vector types. Let’s dive into the world of lists, exploring operations like insertion, removal, slicing, and indexing in both languages while unraveling new Rust concepts.
Lists in Python
In Python, lists are dynamic arrays that can hold elements of different types. They’re versatile, allowing you to perform a variety of operations easily:
my_list = [1, 2, 3, 4, 5]
# Insertion
my_list.append(6)
my_list.insert(2, 7)
# Removal
my_list.remove(3)
popped_element = my_list.pop()
# Slicing
sublist = my_list[1:4]
# Get by index
element = my_list[0]
Rust’s Array and Vector
Rust offers two primary alternatives to Python lists: arrays and vectors.
Arrays
Arrays in Rust are fixed-size, making them more like Python tuples. They’re declared like this:
let my_array = [1, 2, 3, 4, 5];
// Insertion (not possible with arrays)
// Removal (not possible with arrays)
// Slicing
let slice = &my_array[1..4];
// Get by index
let element = my_array[0];
Arrays are stack-allocated, which means their size is determined at compile-time. They’re suitable for cases where the size is known and fixed.
Vectors
Vectors are dynamic arrays in Rust, similar to Python lists, and are part of Rust’s standard library.
use std::vec::Vec;
let mut my_vector: Vec<i32> = vec![1, 2, 3, 4, 5];
// Insertion
my_vector.push(6);
my_vector.insert(2, 7);
// Removal
my_vector.remove(3);
let popped_element = my_vector.pop();
// Slicing
let slice = &my_vector[1..4];
// Get by index
let element = my_vector[0];
use std::vec::Vec;
: This line imports theVec
type from Rust's standard library to use it in your code.Vec<i32>
: This declares a newVec
containing 32-bit signed integers (i32
). Rust requires specifying the type contained in theVec
.vec![1, 2, 3, 4, 5]
: This macro creates a newVec
with the specified elements.&my_vector[1..4]
: The&
symbol indicates that we're taking a reference to a slice of theVec
. Rust's indexing is zero-based, so[1..4]
extracts elements at indices 1, 2, and 3.
In Rust, you need to declare variables as mutable (mut
) if you want to modify them, enforcing a strong sense of ownership and mutability that helps prevent common programming errors.
Vectors are heap-allocated and can grow or shrink dynamically, making them suitable for situations where the size is not known in advance or needs to change over time.
Heterogeneous Vector
In Rust, vectors (represented by Vec<T>
) are homogeneous, meaning they can only contain elements of a single type T
. Unlike Python lists, you cannot have a Rust vector that directly contains elements of different types, such as ints, floats, and strings, in the same vector.
However, you can achieve similar functionality by using Rust’s enum
to create an enum that can hold different types of data.
An enum
in Rust is a custom data type representing a type with one of several values. In this case, MyEnum
is an enum type that can have three different variants: Integer
, Float
, and Text
. Each variant can hold a different type of data.
fn main() {
// Creating instances of MyEnum variants
let int_value = MyEnum::Integer(42);
let float_value = MyEnum::Float(3.14);
let text_value = MyEnum::Text(String::from("Hello, Rust!"));
// Accessing values inside enum variants
match int_value {
MyEnum::Integer(val) => println!("Integer: {}", val),
_ => (),
}
match float_value {
MyEnum::Float(val) => println!("Float: {}", val),
_ => (),
}
match text_value {
MyEnum::Text(val) => println!("Text: {}", val),
_ => (),
}
}
Integer(i32)
: This variant represents an integer value of typei32
. It can hold signed 32-bit integer values.Float(f64)
: This variant represents a floating-point value of typef64
. It can hold 64-bit double-precision floating-point values.Text(String)
: This variant represents a text value of typeString
. It can hold a string of characters.
You can then create a vector of that enum type, effectively creating a heterogeneous container. Here's an example:
enum MyEnum {
Integer(i32),
Float(f64),
Text(String),
}
fn main() {
let mixed_vector: Vec<MyEnum> = vec![
MyEnum::Integer(42),
MyEnum::Float(3.14),
MyEnum::Text("Hello, Rust!".to_string()),
];
// Accessing elements
for item in &mixed_vector {
match item {
MyEnum::Integer(val) => println!("Integer: {}", val),
MyEnum::Float(val) => println!("Float: {}", val),
MyEnum::Text(val) => println!("Text: {}", val),
}
}
}
In this example, we define an enum MyEnum
that can hold different types of data: integers, floats, and strings. We then create a Vec<MyEnum>
to store instances of this enum, effectively creating a vector that can hold different types.
So, while Rust vectors themselves are homogeneous, you can use enums to create heterogeneous collections, and you can have vectors of vectors to r and ns.
Vector of Vectors
Rust allows you to create vectors of vectors. You can have a Vec<Vec<T>>
, where each inner vector can hold elements of the same type T
. Here's an example:
fn main() {
let vector_of_vectors: Vec<Vec<i32>> = vec![
vec![1, 2, 3],
vec![4, 5],
vec![6, 7, 8, 9],
];
// Accessing elements
for inner_vector in &vector_of_vectors {
for element in inner_vector {
print!("{} ", element);
}
println!();
}
}
In this example, vector_of_vectors
is a vector of vectors containing integers (i32
), and you can access and manipulate the inner vectors as needed.
Wrapping Up
In summary, while Python’s lists are dynamic and flexible, Rust’s arrays and vectors offer precise control, safety, and performance. Although Rust Vectors are homogeneous, we can achieve a kind of heterogeneous vector by using enums. We can also have vectors of vectors and this may be useful for Python programmers since it is a common construction in Python code. On the other hand, Rust arrays are more comparable to Python tuples since they are fixed-sized.