-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Check scope's __dict__ instead of using hasattr when registering classes and exceptions #2335
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
Check scope's __dict__ instead of using hasattr when registering classes and exceptions #2335
Conversation
@@ -917,7 +917,7 @@ class generic_type : public object { | |||
PYBIND11_OBJECT_DEFAULT(generic_type, object, PyType_Check) | |||
protected: | |||
void initialize(const type_record &rec) { | |||
if (rec.scope && hasattr(rec.scope, rec.name)) | |||
if (rec.scope && hasattr(rec.scope, "__dict__") && rec.scope.attr("__dict__").contains(rec.name)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potentially, we could drop hasattr(rec.scope, "__dict__")
, if we're happy throwing an exception when the user tries to use a scope that doesn't have a __dict__
attribute?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea of throwing an exception in this situation worries me, a lot.
I'm surprised to see the strong runtime performance concerns from both Yannick and Boris. Is it correct that this code only runs when a pybind11 module is imported? Is that so slow that people complain already? What's the incremental extra runtime overhead? My first guess is it's no measurable, given all the other things happening at import time (files are opened, dlls loaded), but maybe I'm overlooking something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm surprised to see the strong runtime performance concerns from both Yannick and Boris.
It's not really a concern. It is only run once at import time, so it hardly matter.
What's the incremental extra runtime overhead?
I wasn't measuring import whatever
, but rather this specific patch in isolation.
>>> timeit.timeit(stmt='B().__dict__.get("X")', setup='class A:\n def X(self):pass\nclass B(A):pass')
0.17064448199380422
>>> timeit.timeit(stmt='hasattr(B(), "X")', setup='class A:\n def X(self):pass\nclass B(A):pass')
0.12798299700079951
Again, this is executed exactly once. It really shouldn't be a concern. I just wanted to satisfy my own curiosity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not too worried about it, either! Just want to point it out.
The reasoning is/was, if hasattr(rec.scope, "__dict__")
fails, most likely the later scope.attr(name) = *this;
will also fail/throw an exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I misunderstood this code initially (github is helpfully hiding the next line...). Would this be logically equivalent?
if (rec.scope && hasattr(rec.scope, rec.name) && hasattr(rec.scope, "__dict__") && rec.scope.attr("__dict__").contains(rec.name))
Now suspecting: the existing hasattr
returns true, but we want to raise an error only if we find that the attribute is defined in __dict__
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how to solve this, though. Checking hasattr
/getattr
is too wide (i.e., checks base classes), while __dict__
is too strict (would miss the crazy case where __getattr__
/__setattr__
, or __getattribute__
/__setattribute__
, would be overwritten).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we shouldn't fix it? If it's this hard to do it right, perhaps we shouldn't "fix" it and break things that currently work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting suggestion, yes. I must say the current situation feels wrong, though, where it matters in which order you register a class first in the base class or first in a derived class.
And the above shouldn't break too much. there's a thing where, if getattr
and setattr
are overwritten, things might already be broken anyway. In any case, the scope
should be a class or a module, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In any case, the
scope
should be a class or a module, no?
Do you want the good news or the bad news? The bad news?
>>> def f():print('f')
...
>>> def g():print('g')
...
>>> f.g = g
>>> del g
>>> f.g()
g
>>> f()
f
The good news is that using functions as scopes isn't allowed with pybind. I tried. I really really tried to make pybind accept a function object as a scope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm indeed not sure if this is good or bad news :-/
It used to work.
This is so horribly expensive. By the way, there's a detail I omitted. Oh, by the way, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Yannick and Boris for the explanations re runtime performance! Based on that this fix looks great to me, my only suggestion is to harden the test a bit.
Needs rebasing. |
277b828
to
1b55758
Compare
Rebased. I do still want to give this solution another look, btw, now that we're a few months later and potentially wiser. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR was included in Google-global testing: no issues found.
This change looks fine to me -- runtime cost of pybind11 module initialization is usually not much of a concern (it's very fast.), so the extra check is negligible. One question though: you added some tests that ensure that the new kinds of scoped class declarations work, but are we ever checking that one cannot define the same class twice within the same module? IIRC no, in which case it would be good to add an extra test for that. |
OK, if everyone's fine with not supporting weird metaclasses overriding On potential improvement (mentioned above) would be this:
But this is too complex? I'll still quickly add a test! :-) |
I searched for the error message, couldn't find it in the tests, and added |
…ses and exceptions, to allow registering the same name in a derived class scope
aa61989
to
8368c76
Compare
c2e4dba
to
8368c76
Compare
584046f
to
7b4a47c
Compare
…cate_class and test_factory_constructors.py::test_init_factory_alias
7b4a47c
to
0f66572
Compare
OK, this is finally green, and I'm fed up with this. I'm merging this.
|
Thanks everyone for all the reviews and help! :-) |
…re-enabling test_register_duplicate_class again
…tional changes from pybind#2335" This reverts commit ca33a80.
…tional changes from pybind#2335" This reverts commit ca33a80.
…duplicate_class (#2564) * Demonstrate test_factory_constructors.py failure without functional changes from #2335 * Revert "Demonstrate test_factory_constructors.py failure without functional changes from #2335" This reverts commit ca33a80. * Fix test crash where registered Python type gets garbage collected * Clean up some more internal structures when class objects go out of scope * Reduce length of std::erase_if-in-C++20 comment * Clean up code for cleaning up type internals * Move cleaning up of type info in internals to tp_dealloc on pybind11_metaclass
This allows registering the same name in a derived class scope, and fixes #1624
Disadvantage:
__getattribute__
/__setattribute__
(maybe now it already wouldn't, either?).