Inheritance
Python, unlike Rust, supports inheritance.
Each class in Python can inherit attributes and methods from a parent class.
class Parent:
def __init__(self, name):
self.name = name
def greet(self):
print(f"Hello, {self.name}!")
# Declare `Child` as a subclass of `Parent`
class Child(Parent):
def __init__(self, name, age):
# Call the parent class's constructor
super().__init__(name)
self.age = age
child = Child("Alice", 7)
# `Child` inherits the `greet` method from `Parent`, so we can call it
child.greet() # Prints "Hello, Alice!"
pyo3
and inheritance
pyo3
supports inheritance as well, via additional attributes on the #[pyclass]
macro.
To understand how it works, let's try to translate the Python example above to Rust. We'll start with defining
the base class, Parent
:
#![allow(unused)] fn main() { use pyo3::prelude::*; #[pyclass(subclass)] struct Parent { name: String, } #[pymethods] impl Parent { #[new] fn new(name: String) -> Self { Parent { name } } fn greet(&self) { println!("Hello, {}!", self.name); } } }
You can spot one new attribute in the #[pyclass]
macro: subclass
. This attribute tells pyo3
that this class
can be subclassed, and it should generate the necessary machinery to support inheritance.
Now let's define the Child
class, which inherits from Parent
:
#![allow(unused)] fn main() { #[pyclass(extends=Parent)] struct Child { age: u8, } }
We're using the extends
attribute to specify that Child
is a subclass of Parent
.
Things get a bit more complicated when it comes to the constructor:
#![allow(unused)] fn main() { #[pymethods] impl Child { #[new] fn new(name: String, age: u8) -> PyClassInitializer<Self> { let parent = Parent::new(name); let child = Self { age }; PyClassInitializer::from(parent).add_subclass(child) } } }
Whenever you initialize a subclass, you need to make sure that the parent class is initialized first.
We start by calling Parent::new
to create an instance of the parent class. We then initialize Child
, via Self { age }
.
We then use PyClassInitializer
to return both the parent and child instances together.
Even though Child
doesn't have a greet
method on the Rust side, you'll be able to call it from Python since the
generated Child
class inherits it from Parent
.
Nested inheritance
PyClassInitializer
can be used to build arbitrarily deep inheritance hierarchies.
For example, if Child
had its own subclass, you could call add_subclass
again to add yet another subclass to the chain.
#![allow(unused)] fn main() { #[pyclass(extends=Child)] struct Grandchild { hobby: String, } #[pymethods] impl Grandchild { #[new] fn new(name: String, age: u8, hobby: String) -> PyClassInitializer<Self> { let child = Child::new(name, age); let grandchild = Self { hobby }; PyClassInitializer::from(child).add_subclass(grandchild) } } }
Limitations
pyo3
supports two kinds of superclasses:
- A Python class defined in Rust, via
#[pyclass]
- A Python built-in class, like
PyDict
orPyList
It currently doesn't support using a custom Python class as the parent class for a class defined in Rust.
Exercise
The exercise for this section is located in 02_classes/05_inheritance