DEV Community

Cover image for Abstraction in Rust and Python. Simple examples
Antonov Mike
Antonov Mike

Posted on • Edited on

Abstraction in Rust and Python. Simple examples

Disclaimer

The article contains code samples, but not theory and does not pretend to explain the theoretical basis of polymorphism in depth, nor does it explain why and when polymorphism should be used.
The article demonstrates how to implement abstraction in Rust and Python, but does not explain practical applications of polymorphism in real projects.

Abstraction

Abstraction is a fundamental concept in both Rust and Python, allowing for the encapsulation of complex implementations behind simpler interfaces. This makes code more modular, easier to maintain, and helps in reducing the complexity of the system. Let's explore how abstraction works in both languages with a simple example.

In Rust

we can use traits to define a contract for what a class should do without specifying how it does it. This is a form of abstraction. Here's a basic example:

// Define a trait named `Printer`
trait Printer {
    // Declare a method `print_data` that takes a string slice and returns nothing
    fn print_data(&self, data: &str);
}

// Implement the `Printer` trait for a struct named `DataPrinter`
struct DataPrinter;

impl Printer for DataPrinter {
    fn print_data(&self, data: &str) {
        println!("Data: {}", data);
    }
}

fn main() {
    let printer = DataPrinter;
    printer.print_data("Hello, Rust!");
}
Enter fullscreen mode Exit fullscreen mode

In this Rust example, Printer is an abstraction that defines a print_data method. The DataPrinter struct implements this trait, providing the actual implementation of print_data. The main function doesn't need to know how print_data is implemented; it only needs to know that DataPrinter is a Printer. The print_data method in the Printer trait is declared but does nothing. This method is then implemented in the DataPrinter struct to print the data. This setup allows for the possibility of other structs implementing the Printer trait with their own implementations of print_data, or they could use the default (no-op) implementation if they don't need to print anything.

In Python

it's more about encapsulation and duck typing because Python is a dynamically typed language. Here's how you might achieve a similar effect to the Rust example:

class Printer:
    def print_data(self, data):
        raise NotImplementedError("Subclasses must implement this method")

class DataPrinter(Printer):
    def print_data(self, data):
        print(f"Data: {data}")

def main():
    printer = DataPrinter()
    printer.print_data("Hello, Python!")

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

In this Python example, Printer is an abstract base class (ABC) that defines a print_data method. The DataPrinter class inherits from Printer and provides an implementation for print_data. The main function uses a DataPrinter instance without needing to know the details of how print_data is implemented.

Summary

Both Rust and Python support abstraction, allowing you to hide the complexity of an implementation behind a simpler interface. Rust achieves this through traits, which are a way to define shared behavior across types, while Python uses abstract base classes (ABCs) and duck typing to achieve a similar effect. The key takeaway is that both languages allow you to define a contract for what a class should do without specifying how it does it, making your code more modular and easier to maintain.

Other articles about the similarities and differences between Rust and Python

  1. Python Classes vs. Rust Traits
  2. Python classes vs Rust structures
  3. Polymorphism in Rust and Python
  4. Abstraction in Rust and Python
  5. Encapsulation in Rust and Python
  6. Composition in Rust and Python

Top comments (1)

Collapse
 
antonov_mike profile image
Antonov Mike
trait Printer {
    fn print_data(&self, data: &str);
}

struct DataPrinter;
Enter fullscreen mode Exit fullscreen mode

Declaring a method that does nothing, especially in the context of traits and structs in Rust, serves several purposes:

  1. Trait Definition: Defining a method in a trait that does nothing (i.e., has an empty body) is a way to specify a default behavior that can be overridden by any struct that implements the trait. This is useful for creating a base or interface that can be extended by other structs. The method in the trait acts as a placeholder for functionality that might be implemented differently by each struct that implements the trait.
  2. Default Behavior: By providing a default implementation of a method (even if it does nothing), you can ensure that any struct implementing the trait will have a method with that name. This is beneficial for code organization, as it groups related functionality under a single trait, making the code easier to understand and maintain.
  3. Flexibility and Extensibility: It allows for more flexible and extensible designs. Structs can choose to implement the method to provide specific functionality, or they can rely on the default (no-op) implementation. This flexibility is particularly useful in larger codebases where different parts of the code might need to interact with the same trait but require different implementations of its methods.
  4. Clarity and Documentation: Declaring a method that does nothing can also serve as documentation. It explicitly states that a method is part of the trait's contract, even if it doesn't do anything by default. This can be helpful for future developers working with the code, as it clearly indicates the expected interface of the trait and what methods are available for use.