-
Notifications
You must be signed in to change notification settings - Fork 784
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dynamic dispatch with Python objects #877
Comments
Hi @diegogangl, thanks for this, it's a good question. You are right that Rust's mechanism of doing this kind of polymorphism is through trait objects. If you're looking to keep Python's semblance of shared mutability, you're probably looking for With that in mind, I have a couple of ideas which should help you move forward: Option 1: Trait ObjectsAs The downside of this approach is on the Python side, where you have to add use std::rc::Rc;
use std::cell::RefCell;
pub trait Instanceable {
fn do_some(&self) {}
}
#[pyclass]
#[derive(Clone)]
pub struct Instantiator {
obj: Rc<dyn Instanceable>
}
struct SubClass01Inner {
a_value: RefCell<u8>,
}
impl Instanceable for SubClass01Inner {
fn do_some(&self) { println!("{}", self.a_value.borrow()); }
}
#[pyclass]
#[derive(Clone)]
pub struct SubClass01 {
inner: Rc<SubClass01Inner>
}
#[pymethods]
impl SubClass01 {
#[new]
fn new() -> Self {
SubClass01 {
inner: Rc::new(SubClass01Inner { a_value: RefCell::new(0) })
}
}
#[getter(a_value)]
fn get_a_value(&self) -> u8 {
*self.inner.a_value.borrow()
}
#[setter(a_value)]
fn set_a_value(&mut self, value: u8) {
self.inner.a_value.replace(value);
}
fn instantiator(&self) -> Instantiator {
Instantiator {
obj: self.inner.clone()
}
}
}
impl Default for SubClass01 {
fn default() -> Self {
SubClass01::new()
}
}
#[pyclass]
pub struct MainClass {
#[pyo3(get, set)]
instantiator: Instantiator
}
#[pymethods]
impl MainClass {
#[new]
fn new() -> Self {
MainClass {
instantiator: SubClass01::default().instantiator()
}
}
fn do_the_thing(&self) {
self.instantiator.obj.do_some()
}
} This would give you an experience in Python like so: >>> import rust
>>> m = rust.MainClass()
>>> m.do_the_thing()
0
>>> s = rust.SubClass01()
>>> m.instantiator = s.instantiator()
>>> s.a_value = 5
>>> m.do_the_thing()
5 Option 2: Python DispatchThe alternative to the above is to give up on the dispatch on the Rust side, and instead resolve the right subclass method to call using a Python method call. To do that, you just need to modify the
With that, you get a Python API that looks a bit more natural to those users:
|
However, I think that neither solution is ideal - the trait object route is very messy at the moment, and the python dispatch route gives up most of the typing on the Rust side. I'll let you know if I have any further thoughts to refine this with pyo3's existing implementation. This kind of polymorphic dispatch is also a pattern that does crop up in Python APIs from time-to-time, so it's probably something we want to think about supporting better in pyo3. Marking as help-wanted in case anyone else has ideas what a nice solution for this could look like. |
Hey @davidhewitt , thanks for looking into this! Option 1 would the best, since I'd rather not give up Rust typing. What worries me about the 1st option is how memory management seems to be getting more complicated with borrows, derefs, etc. I think another option would be handling all the dispatch from Rust's side. Like having a method in Another sub-option could be making a config struct (that is a pyclass). Then I can build that in Python, pass it to the setter in |
Yep, I only showed one possible way that option 1 could be arranged - you could tweak that kind of thing as suits you to get an API more like what you're happy with. Would be interested to see what your final solution looks like - maybe we can use it as experience to improve this part of pyo3. |
Sure, I will post back here once I have some final code 👍 |
Are there any further thoughts on this feature? This sort of pattern is a very natural API to write from the Rust perspective |
I haven't had time to think about it myself. All input welcome! |
I've been working on something that may need this feature soon and had a weird idea/question. Would it be possible to define a macro (say This may be wishful thinking, but this could potentially also allow a user to define some custom class on the Python side that implements said protocol and use it with a Rust implementation that uses trait objects. EDIT: I will try sending a minimal example code as soon as I flesh this out in my head 😅 . |
Any update to support |
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:
This is the code I have on the rust side.
The problem is that the subclasses are never set.
MainClass
always callsBaseClass
(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:
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!
The text was updated successfully, but these errors were encountered: