[Python-Dev] Decimal & returning NotImplemented (or not)

Nick Coghlan ncoghlan at iinet.net.au
Wed Mar 2 11:23:34 CET 2005


Neil Schemenauer wrote:
>  IMO, Decimal should
> be returning NotImplemented instead of raising TypeError.

I agree, but I'm also interested in the fact that nobody commented on this 
before Python 2.4 went out. I read the reference page on numeric coercion more 
times than I care to count, and still didn't register that there might be an 
issue with raising the TypeError - and that's only me. Relative to people like 
Raymond and Facundo, I spent comparatively little time working on Decimal :)

I think the problem arose because the natural thing to do in Python code is to 
push the type inspection for operations into a common method, and have that 
method raise TypeError if the other argument isn't acceptable.

Trying to convert that exception to a 'NotImplemented' return value is a pain 
(it's less of a pain in C, since you're already checking for error returns 
instead of using exceptions). The error return idiom is really unnatural in 
Python code. Mixing the error return for the special methods with raising an 
exception for non-special methods is exceedingly ugly (particularly for Decimal, 
since operations through Context objects should *always* raise a TypeError for 
bad arguments. If the special methods return NotImplemented instead of raising 
an exception, then Context has to convert those returns to TypeErrors).

Additionally, returning NotImplemented means that direct calls to special 
methods may not raise an exception in response to a bad argument. Compare:

Py> 1 .__add__("")
NotImplemented
Py> "".__add__(1)
Traceback (most recent call last):
   File "<stdin>", line 1, in ?
TypeError: cannot concatenate 'str' and 'int' objects

Hmm, perhaps a decorator would help here. Something like:

class OperatorTypeError(TypeError): pass

class operatormethod(object):
     def __init__(self, method):
         self._method = method
         self._obj = None
     def __get__(self, obj, type=None):
         self._obj = obj
         return self
     def __call__(*args, **kwds):
         self = args[0]
         obj = self._obj
         try:
             if obj is None:
                 return self._method(*args[1:], **kwds)
             return self._method(obj, *args[1:], **kwds)
         except OperatorTypeError:
             return NotImplemented

Then do:

Py> class C:
...     def _add(self, other):
...         raise OperatorTypeError
...     __add__ = operatormethod(_add)
...     __radd__ = __add__
...
Py> C()._add(1)
Traceback (most recent call last):
   File "<stdin>", line 1, in ?
   File "<stdin>", line 3, in _add
__main__.OperatorTypeError
Py> C().__add__(1)
NotImplemented
Py> C() + 1
Traceback (most recent call last):
   File "<stdin>", line 1, in ?
TypeError: unsupported operand type(s) for +: 'instance' and 'int'
Py> 1 + C()
Traceback (most recent call last):
   File "<stdin>", line 1, in ?
TypeError: unsupported operand type(s) for +: 'int' and 'instance'

That makes it easy to write natural Python code (raising a specific subclass of 
type error), while still returning NotImplemented so the operator coercion works 
correctly.

 >  That
 > could be considered a bug but I'd file it under new functionality
 > for the purposes of backporting (i.e. fix it in 2.5 only).

Given that I'm already ambivalent about changing this for 2.4, it won't take 
much to convince me that this should be left to 2.5.

Particularly if we actually try to find a way to make it easier to 'do the right 
thing', rather than just changing Decimal.

Cheers,
Nick.

-- 
Nick Coghlan   |   ncoghlan at email.com   |   Brisbane, Australia
---------------------------------------------------------------
             http://boredomandlaziness.skystorm.net


More information about the Python-Dev mailing list