-
Notifications
You must be signed in to change notification settings - Fork 220
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
Better handling for Python subclasses of types that use nb::new_() #859
Conversation
In Python, `__new__` takes a first argument indicating the type of object it should create, which allows one to derive from classes that use `__new__` and obtain reasonable semantics. nanobind's `nb::new_()` wrapper currently ignores this argument, with somewhat surprising results: ``` // C++ struct Base { ... }; nb::class_<Base>(m, "Base").def(nb::new_(...))...; class Derived(mod.Base): pass >>> type(Derived()) is mod.Base True ``` This PR makes that case work more like it does in Python, so that `Derived()` produces an instance of `Derived`. This is possible safely because both `Base` and `Derived` believe they wrap the same C++ type. Since we can't easily intervene before the `Base` object is created, we use a call policy to intervene afterwards, and return an object of type `Derived` that wraps a `Base*` without ownership. The original `Base` object's lifetime is maintained via a keep-alive from the returned wrapper. There is not much actual code, but it's subtle so the comments are pretty long. This feature requires allowing a call policy's postcall hook to modify the return value, which was not previously supported. I chose to implement this by allowing the postcall hook to take the return value via `PyObject*&`; then a passed `PyObject*` can initialize either that or to the documented `handle`. I didn't document this new capability, because it's somewhat obscure and easy to mess up the reference counting; I figure anyone that really needs it will be able to figure it out. An alternative if you don't want to add this ability to call policies in general would be to define a new function attribute just for the new-fixup case, but that felt more invasive to me.
I'm am concerned about the implications of Since the original commit, there have been several commits to work around limitations and address corner cases. With #861 and #859 on top, the complexity is now reaching a level that I feel is starting to be unreasonable for this project given its "nano" scope. My first reaction is that it would be better to explain the limitations of the current feature rather than making it fully general as you propose to do in these two commits. |
Thanks for the feedback. I personally lean pretty far in the direction of taking on additional implementation complexity in order to handle all the cases rather than just the common ones, but I acknowledge that's not always the correct tradeoff, and a lot of the problems solved by these two PRs are theoretical. The impetus for this PR #859 (the actual problem I'm trying to solve) was some code that did:
When #861 only showed up as a problem in a particularly tortured test case that I wrote for #859, not in any real code, so I've closed it. The support in #859 for cases where |
98def19
to
92d9cb3
Compare
Thank you @oremanj! |
In Python,
__new__
takes a first argument indicating the type of object it should create, which allows one to derive from classes that use__new__
and obtain reasonable semantics. nanobind'snb::new_()
wrapper currently ignores this argument, with somewhat surprising results:This PR makes that case work more like it does in Python, so that
Derived()
produces an instance ofDerived
. This is possible safely because bothBase
andDerived
believe they wrap the same C++ type. Since we can't easily intervene before theBase
object is created, we use a call policy to intervene afterwards, and return an object of typeDerived
that wraps aBase*
without ownership. The originalBase
object's lifetime is maintained via a keep-alive from the returned wrapper.This feature requires allowing a call policy's postcall hook to modify the return value, which was not previously supported. I chose to implement this by allowing the postcall hook to take the return value via
PyObject*&
; then a passedPyObject*
can initialize either that or the documentedhandle
. I didn't document this new capability, because it's somewhat obscure and easy to mess up the reference counting; I figure anyone that really needs it will be able to figure it out. An alternative if you don't want to add this ability to call policies in general would be to define a new function attribute just for the new-fixup case, but that felt more invasive to me.