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
functools.total_ordering doesn't work with metaclasses #88771
Comments
Consider the following example that attempts to make a series of types sortable by their class name: from functools import total_ordering
@total_ordering
class SortableMeta(type):
def __new__(cls, name, bases, ns):
return super().__new__(cls, name, bases, ns)
def __lt__(self, other):
if not isinstance(other, SortableMeta):
pass
return self.__name__ < other.__name__
def __eq__(self, other):
if not isinstance(other, SortableMeta):
pass
return self.__name__ == other.__name__
class B(metaclass=SortableMeta):
pass
class A(metaclass=SortableMeta):
pass
print(A < B)
print(A > B) This should just print "True", "False", but instead the second comparison raises this exception: Traceback (most recent call last):
File "total_ordering_metaclass.py", line 27, in <module>
print(A > B)
File "lib/python3.9/functools.py", line 91, in _gt_from_lt
op_result = self.__lt__(other)
TypeError: expected 1 argument, got 0 The problem here is that functools considers .__special__ to be equivalent to operator.special. I'm pretty sure this should be invoking "self < other" rather than self.__lt__(other). |
Adding versions after confirming the bug is the same on 3.10 |
The one twist is that if type(b) is a strict subtype of type(a), then "a < b" first calls type(b).__gt__(b, a), then falls back to type(a).__lt__(a, b). Example: >>> class Int(int):
... def __gt__(self, other):
... print("Here!")
... return int(self) > int(other)
...
...
>>> 5 < Int(6)
Here!
True see Line 683 in da2e673
So I think replacing "a.__lt__(b)" with "a < b" would be an unnecessary change in behavior, since "a < b" still has to decide which method to use. I think instead "a.__lt__(b)" could be replaced with "type(a).__lt__(a, b)" |
To add support for metaclasses, replacing 'self.__lt__(other)' with 'type(self).__lt__(self, other)' will work. The performance degradation for the common case is about 25%: $ py -m timeit -s 'from tmp import a, b' 'a >= b' # op_result = type(self).__lt__(self, other)
1000000 loops, best of 5: 283 nsec per loop
$ py -m timeit -s 'from tmp import a, b' 'a >= b' # op_result = self.__lt__(other)
1000000 loops, best of 5: 227 nsec per loop |
Do you all have preference for 1) expanding the range of use cases to cover metaclasses but incurring a performance hit for common cases, or 2) leaving it as-is and documenting that it doesn't work for metaclasses? |
If we do need the slow approach for the meta-type case, (i.e.: type(self).__lt__(other)) then why not decide on implementation at decoration time? The decorator has enough information to know if this strategy is necessary and can put different special methods in place. (But also, wouldn't 'from operator import lt ... lt(self, other)' be a bit faster, and also more general, just because it's doing specialized dispatch in C rather than an additional Python function call / method dispatch? I have not benchmarked myself, so please ignore if you've already tested this.) |
[Glyph]
We could do that and not incur performance issues. However, it would expand the API and double the size of the code. We've had no other reports about this issue since total_ordering() was added in Python 2.7, nor have there been any questions about it on StackOverflow. Do you think anyone other than yourself would use this feature? It seems like a rare niche use case, and the workaround is simple. For production code, I usually recommend writing out the other three methods (that is less magical, runs faster, easier to test, has fewer dependencies, and makes the tracebacks more readable.
The lt() function emulates the '<' operator, so we still have the problems with reflection that we were trying to avoid by calling the __lt__ method directly. |
If the preference is to not support this use-case, then I'd rather the decorator simply raise an exception and tell me this is going to be a problem so I can eagerly implement the workaround.
It does seem like there are various times that one might want to make classes orderable, for example if they are bound to IDL objects or database tables; the use-case where I ran into this was something like that.
Wait, is the suggestion here that @total_ordering is not suitable for production? |
I don't really have a preference. Was just letting you know the pros and cons. I'll put together a PR with the "type(self).__lt__(self, other)" substitutions. Let me know if that is the outcome you want.
It works fine, but as noted in the docs, the indirect code isn't as good as just writing-out the methods by hand. |
My preference would be to fix it one way or another; the stdlib should be correct before it's performant. If, as you say, it's better to write the methods longhand anyway for ease of debugging, then it makes sense to write them out for performance, too. |
IMO the PR should be accepted and have approved it accordingly. @total_ordering, as mentioned by Raymond, wasn't ever the most efficient way to implement ordering on a class. It is the most readable and easy way to get a correct implementation though so it's preferred where performance isn't the highest priority. |
Note: 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: