Dynamic dispatch with Python objects
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 16.2k
- Forks
- 1k
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 66
Description
Hi, I'm trying to pass custom objects through Python to Rust. I need to store one of these in a main struct. The Python classes have some properties of their own and a method I'm calling from the main struct.
This is the Python API I'm shooting for:
main = rust.MainClass()
sub_1 = rust.SubClass01()
sub_2 = rust.SubClass02()
sub_1.a_value = 2
sub_2.a_boolean = True
main.inner_class = sub_1
print(main.do_the_thing())
This is the code I have on the rust side.
#[pyclass]
#[derive(Clone)]
pub struct BaseClass {}
#[pymethods]
impl BaseClass {
fn do_something(&self) -> f64 {
-1.0
}
#[new]
fn new() -> Self {
BaseClass {}
}
}
#[pyclass(extends=BaseClass)]
#[derive(Clone)]
pub struct SubClass01 {
#[pyo3(get, set)]
a_value: u8,
}
#[pymethods]
impl SubClass01 {
fn do_something(&self) -> f64 {
1.0
}
#[new]
fn new() -> (Self, BaseClass) {
(SubClass01 { a_value: 0 }, BaseClass::new())
}
}
#[pyclass(extends=BaseClass)]
#[derive(Clone)]
pub struct SubClass02 {
#[pyo3(get, set)]
a_boolean: bool,
}
#[pymethods]
impl SubClass02 {
fn do_something(&self) -> f64 {
if self.a_boolean {
2.0
} else {
0.0
}
}
#[new]
fn new() -> (Self, BaseClass) {
(SubClass02 { a_boolean: true }, BaseClass::new())
}
}
#[pyclass]
pub struct MainClass {
#[pyo3(get, set)]
inner_class: BaseClass
}
#[pymethods]
impl MainClass {
fn do_the_thing(&self) -> f64 {
self.inner_class.do_something()
}
#[new]
fn new() -> Self {
MainClass { inner_class: BaseClass {} }
}
}
The problem is that the subclasses are never set. MainClass always calls BaseClass (the print at the end prints -1.0). I can sort of see why this happens on the rust side since I'm setting BaseClass by default and as the type (I'm guessing the subclasses are getting downcasted?).
I trait objects would be they way to solve this but I don't see how that works in the Pyo3/python side.
Here's what I tried to do with trait objects:
// Yeah this is a terrible trait name :)
pub trait Instanceable {
fn do_some(&self) {}
}
impl Instanceable for SubClass01 {
fn do_some(&self) { println!("555"); }
}
impl Default for SubClass01 {
fn default() -> Self {
SubClass01 { a_value: 0 }
}
}
#[pyclass]
pub struct MainClass {
some_class: Box<dyn Instanceable>
}
#[pymethods]
impl MainClass {
#[new]
fn new() -> Self {
MainClass {
some_class: Box::new(SubClass01::default())
}
}
#[setter(some_class)]
fn set_class<T: Instanceable>(&mut self, cls: T) -> PyResult<()> {
Box::new(cls);
Ok(())
}
}
In this case I can't compile because "a python method can't have a generic type parameter", which makes sense.
How do I pass a custom object to the setter? Is this kind of API or dynamic dispatch possible with PyO3?
Thanks!
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
The issue provides no repository file or test entry point. Start by reproducing the Python and Rust examples using PyO3's #[pyclass] inheritance and setter entry points, then determine whether the requested dynamic dispatch is supported and what a completed API should guarantee.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, rust
- Domain
- api, backend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 20/100