[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

AltStyle によって変換されたページ (->オリジナル) /