[Python-Dev] Exceptions in comparison operators
Mark Shannon
mark at hotpy.org
Tue Mar 13 19:35:33 CET 2012
Guido van Rossum wrote:
> Mark, did you do anything with my reply?
Not yet.
I noticed the difference when developing my HotPy VM
(latest incarnation thereof) which substitutes a sequence of low-level
bytecodes for the high-level ones when tracing.
(A bit like PyPy but much more Python-specific and amenable to
interpretation, rather than compilation)
I generate all the code sequences for binary ops from a template
and noticed the slight difference when running the test suite.
My implementation of equals follows the same pattern as the arithmetic
operators (which is why I was wondering if that were the correct behaviour).
My definition of op1 == op2:
def surrogate_eq(op1, op2):
if $overrides(op1, op2, '__eq__'):
if op2?__eq__:
result = op2$__eq__(op1)
if result is not NotImplemented:
return result
if op1?__eq__:
result = op1$__eq__(op2)
if result is not NotImplemented:
return result
else:
if op1?__eq__:
result = op1$__eq__(op2)
if result is not NotImplemented:
return result
if op2?__eq__:
result = op2$__eq__(op1)
if result is not NotImplemented:
return result
return op1 is op2
Where:
x$__op__ means special lookup (bypassing the instance dictionary):
x?__op__ means has the named special method i.e.
any('__op__' in t.__dict__ for t in type(op).__mro__))
and
$overrides(op1, op2, 'xxx') means that
type(op2) is a proper subtype of type(op1)
*and* type(op1).__dict__['xxx'] != type(op2).__dict__['xxx']
It would appear that the current version is:
def surrogate_eq(op1, op2):
if is_proper_subtype_of( type(op1), type(op1) ):
if op2?__eq__:
result = op2$__eq__(op1)
if result is not NotImplemented:
return result
if op1?__eq__:
result = op1$__eq__(op2)
if result is not NotImplemented:
return result
else:
if op1?__eq__:
result = op1$__eq__(op2)
if result is not NotImplemented:
return result
if op2?__eq__:
result = op2$__eq__(op1)
if result is not NotImplemented:
return result
return op1 is op2
Which means that == behaves differently to + for
subtypes which do not override the __eq__ method.
Thus:
class MyValue1:
def __init__(self, val):
self.val = val
def __lt__(self, other):
print("lt")
return self.val < other.val
def __gt__(self, other):
print("gt")
return self.val > other.val
def __add__(self, other):
print("add")
return self.val + other.val
def __radd__(self, other):
print("radd")
return self.val + other.val
class MyValue2(MyValue1):
pass
a = MyValue1(1)
b = MyValue2(2)
print(a + b)
print(a < b)
currently prints the following:
add
3
gt
True
Cheers,
Mark.
>
> On Mon, Mar 5, 2012 at 10:41 AM, Guido van Rossum <guido at python.org> wrote:
>> On Mon, Mar 5, 2012 at 4:41 AM, Mark Shannon <mark at hotpy.org> wrote:
>>> Comparing two objects (of the same type for simplicity)
>>> involves a three stage lookup:
>>> The class has the operator C.__eq__
>>> It can be applied to operator (descriptor protocol): C().__eq__
>>> and it produces a result: C().__eq__(C())
>>>
>>> Exceptions can be raised in all 3 phases,
>>> but an exception in the first phase is not really an error,
>>> its just says the operation is not supported.
>>> E.g.
>>>
>>> class C: pass
>>>
>>> C() == C() is False, rather than raising an Exception.
>>>
>>> If an exception is raised in the 3rd stage, then it is propogated,
>>> as follows:
>>>
>>> class C:
>>> def __eq__(self, other):
>>> raise Exception("I'm incomparable")
>>>
>>> C() == C() raises an exception
>>>
>>> However, if an exception is raised in the second phase (descriptor)
>>> then it is silenced:
>>>
>>> def no_eq(self):
>>> raise Exception("I'm incomparable")
>>>
>>> class C:
>>> __eq__ = property(no_eq)
>>>
>>> C() == C() is False.
>>>
>>> But should it raise an exception?
>>>
>>> The behaviour for arithmetic is different.
>>>
>>> def no_add(self):
>>> raise Exception("I don't add up")
>>>
>>> class C:
>>> __add__ = property(no_add)
>>>
>>> C() + C() raises an exception.
>>>
>>> So what is the "correct" behaviour?
>>> It is my opinion that comparisons should behave like arithmetic
>>> and raise an exception.
>> I think you're probably right. This is one of those edge cases that
>> are so rare (and always considered a bug in the user code) that we
>> didn't define carefully what should happen. There are probably some
>> implementation-specific reasons why it was done this way (comparisons
>> use a very different code path from regular binary operators) but that
>> doesn't sound like a very good reason.
>>
>> OTOH there *is* a difference: as you say, C() == C() is False when the
>> class doesn't define __eq__, whereas C() + C() raises an exception if
>> it doesn't define __add__. Still, this is more likely to have favored
>> the wrong outcome for (2) by accident than by design.
>>
>> You'll have to dig through the CPython implementation and find out
>> exactly what code needs to be changed before I could be sure though --
>> sometimes seeing the code jogs my memory.
>>
>> But I think of x==y as roughly equivalent to
>>
>> r = NotImplemented
>> if hasattr(x, '__eq__'):
>> r = x.__eq__(y)
>> if r is NotImplemented and hasattr(y, '__eq__'):
>> r = y.__eq__(x)
>> if r is NotImplemented:
>> r = False
>>
>> which would certainly suggest that (2) should raise an exception. A
>> possibility is that the code looking for the __eq__ attribute
>> suppresses *all* exceptions instead of just AttributeError. If you
>> change no_eq() to return 42, for example, the comparison raises the
>> much more reasonable TypeError: 'int' object is not callable.
>>
>> --
>> --Guido van Rossum (python.org/~guido)
>
>
>
More information about the Python-Dev
mailing list