-
Notifications
You must be signed in to change notification settings - Fork 790
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
[Question] Return different sub-classes from one function? #1637
Comments
I feel like I've figured out the solution to this once already, but I don't remember where :( The tuple (Subclass, Baseclass) should be possible to convert to a PyObject but it clearly doesn't right now. Maybe we should rethink the subclassing strategy and require a field of the subclass to be of type Base? |
The guide has a paragraph on inheritance with an example that looks like what you are trying to do. But this seems like a weird thing to use inheritance for (do you happen to use inheritance for other reasons?). If you just want to return the enum to Python code, you should probably implement IntoPy for the enum. Something like this: use pyo3::prelude::*;
#[pyclass]
struct A{}
#[pyclass]
struct B{}
enum Thing {
A,
B,
None
}
impl IntoPy<PyObject> for Thing {
fn into_py(self, py: Python) -> PyObject {
match self {
Self::A => A{}.into_py(py),
Self::B => B{}.into_py(py),
Self::None => py.None()
}
}
} |
I'm probably looking right past it, but I can't see where in the examples there is a function that returns different sub-classes. 😕
At first I wanted to return a struct that contained that enum. However that enum has a lifetime and that is not allowed in pyo3. |
I disagree. It's very common in Python to use subclasses for what we use an enum in Rust. |
Oh, like the factory pattern in Java? That does make sense - I've just never seen (never noticed, probably) any Python code doing it.
The lifetime issue is not something you can paper over easily because Rust and Python's ownership mechanics are fundamentally different, but there are solutions for it:
What is right for you depends on how and why you're using references.
I might have answered a bit too quickly here 😳 I think the basic issue is that for a function to dynamically return one of potentially different Python objects, that object should be a |
Of course, I completely understand that. That's why I was trying to convert the enum into python classes with a parent class for common behavior. I have some (soft) constraints on what I can do to the code with the lifetime. Basically we have made a Rust library to parse some protocol and would like to provide a Python API to use it in scripts. In this library a In an ideal situation I would like the parsing library to not have to clone/copy the parser's buffer to the frame and only do that in the Python API. That brought me to essentially the design in your example, where I convert each variant into a specific class, with the addition of a base class for common methods and data.
Yes, as @birkenfeld noted, you can turn |
I did do that (with a custom attribute macro) to turn an enum variant into the appropriate subclass, and again. The IntoPyObject @mejrs suggested will probably work, while for the FromPy I ended up hacking something matching the qualified type name and casting (safe as long as nobody inherits from your base class on Python's side) |
Agreed - I've been wondering about how we might implement
I've thought about exactly this in the past. To allow safely converting
I don't think that this is possible to do in Rust's type system, so we would need to rely on To add my own piece to this discussion, I think the If I put my experiments together into a couple of PRs, in PyO3 0.14 we could write the original example something like this: // PingFrame::new returns Py<PingFrame>
#[pymethods]
impl PingFrame {
#[new]
pub fn new(py: Python, sequence: u8) -> Py<PingFrame> {
let frame = Frame::new_ping(sequence);
let ping_frame: Self = frame.try_into().unwrap();
pyo3::new_base(py, frame)
.add_subclass(ping_frame)
.finish()
}
}
// And the parse function returns Py<Frame>
fn parse(&mut self, py: Python, byte: u8) -> PyResult<Option<Py<Frame>>> {
match self.parser.parse(byte) {
Ok(f) => Ok(Some(PingFrame::new(py, 5).into_super())),
Err(e) => return Err(ParseError { err: e }.into())
}
} Perhaps my ideas are best explained in a series of PRs and issues (I can put more stuff together at the weekend). The relevant pieces of my ideas for this issue are the following:
|
It might not be possible in the type system, but in the proc-macro? At least adding But your proposal is also nice, although it won't allow access to the base class data if you take a |
That's very true, I think both would be doable in the proc macro. Though the other downside (that I forgot to mention above) is that sometimes we might not know the memory layout of I guess we could always have different behaviour for Python base types vs Rust base types.
I completely agree with this downside. That's one of the main reasons I've been thinking so hard on this topic. I think it should be possible to make |
Hi! I ran into this question while looking into how instances of a PyO3 subclasses should be handled when they appear as return types. I believe the discussion above is likely quite outdated, so I figured I might as well share my solution (which works with current PyO3), in case anyone else rediscovers this question. fn function_that_returns_a_subtype(py: Python) -> PyResult<PyObject> {
let initializer: PyClassInitializer<SuperType> = PyClassInitializer::from(
todo!("Initialize an instance of SuperType.")
);
let object = if branch() {
let initializer: PyClassInitializer<SubTypeA> = initializer.add_subclass(
todo!("Initialize an instance of SubTypeA.")
);
PyCell::new(py, initializer)?.to_object(py)
} else {
let initializer: PyClassInitializer<SubTypeB> = initializer.add_subclass(
todo!("Initialize an instance of SubTypeB.")
);
PyCell::new(py, initializer)?.to_object(py)
};
Ok(object)
} Do you think it would be useful to add something along these lines to the PyO3 guide? Because if so, I'd be happy to turn it into a PR. |
My 2 pence is yes it would be good to have something in the guide. I just spent a while looking before ending up here and your solution seems to do exactly what I wanted (thanks for that!). Actually I wasn't even trying to return different subclasses from the same function but just to construct an instance of a sub-pyclass from rust code in general. If there is a better way to do that then I think it would be good to add that to the guide somewhere as well. |
Is it possible to return different sub-classes from a function?
In Rust I have an enum containing data
In Python I though I would expose this by having a
Frame
base class and a sub-class for each type of frame:Now I'm trying to have a function that can return any of the sub-classes depending on the frame enum variant. Is that possible?
I tried to use
PyFrame
andPyObject
in the return type, but that doesn't seem to work.The text was updated successfully, but these errors were encountered: