-
-
Notifications
You must be signed in to change notification settings - Fork 31.7k
Modifying class __dict__ inside __set_name__ #72983
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
Comments
This behavior occurs at least in python-3.6.0b3 and python-3.6.0b4. Modifying the class __dict__ inside the __set_name__ method of a descriptor that is used inside that class can lead to a bug that possibly prevents other descriptors from being initialized (the call to their __set_name__ is prevented). That happens because internally the cpython interpreter iterates the class __dict__ when calling all the __set_name__ methods. Changing the class __dict__ while iterating it leads to undefined behavior. This is the line in the source code https://github.com/python/cpython/blob/master/Objects/typeobject.c#L7010 See the attached file for an example where the bug can be observed. It defines a "desc" class that implements the __set_name__ method where it prints the name and modifies the class __dict__ of the containing class. Later a class is defined that has multiple instances of the "desc" class as class attributes. Depending on the number of attributes not all of them are initialized. When you see the underlying C-Code the reason for this behavior is obvious but in python itself the problem might not be apparent. To fix this bug the class __dict__ could be cashed and then the __set_name__ methods could be called while iterating over that copy. |
Ow! I can confirm the bug still happens on latest trunk. Nice finding! |
Proposed patch iterates a copy of __dict__. |
Rather than taking a snapshot of the whole class namespace, I think it will make more sense to make a new empty dictionary of descriptors to notify, and then split the current loop into two loops:
Serhiy's patch effectively already does that via the initial PyDict_Copy, but that approach also redundantly copies items that don't define __set_name__ into the snapshot and then filters them out on the second pass. Reviewing the patch also made me realise we're currently missing a specification of the expected behaviour in https://docs.python.org/dev/reference/datamodel.html#creating-the-class-object. I suggest adding the following paragraph between the one about setting __class__ and the one about calling class descriptors: """
|
I considered this option, but if save only descriptors with the __set_name__ attribute, this would need either double looking up the __set_name__ attribute or packing it in a tuple with a value. All this too cumbersome. I chose copying all dictionary as the simplest way. |
I was thinking that the notification dict could consist of mappings from attribute names to already bound __set_name__ methods, so the double lookup would be avoided that way (but hadn't actually sent the review where I suggested that). That is, on the second loop, the actual method call would be: tmp = PyObject_CallFunctionObjArgs(value, type, key, NULL); Have I missed something that would prevent that from working? |
Error message contains value->ob_type->tp_name. |
Ah, I'd missed that. In that case, I think my suggestion to change the name of the local variable is still valid, while the specific approach just needs a comment saying it's handled as a full namespace snapshot so the descriptor type name can be reported in any error messages. |
Here is much more complex patch that implements Nick's suggestion. |
I personally prefer the first patch, which iterates over a copy of __dict__. Making a copy of a dict is actually a pretty fast operation, I would even expect that it is faster than the proposed alternative, creating tuples. Even if the second approach should be faster, the added code complexity is not worth the effort, as this is a code path where speed shouldn't matter much. |
I'd be OK with starting with the simpler patch, and only looking at the more complex one if the benchmark suite shows a significant slowdown (I'd be surprised if it did, as it should mainly impact start-up, and class dicts generally aren't that big) |
(Plus, as Martin noted, the tuple creation overhead could easily make the more complex patch slower in many cases) |
New changeset 6b8f7d1e2ba4 by Serhiy Storchaka in branch '3.6': New changeset 18ed518d2eef by Serhiy Storchaka in branch 'default': |
Regarding the docs suggestion above, I need to make some other docs changes for bpo-23722, so I'll roll that update in with those. Given that, I think we can mark this issue as resolved. |
Misc/NEWS
so that it is managed by towncrier #552Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
The text was updated successfully, but these errors were encountered: