new-style classes multiplication error message isn't very informative

J

Jon Guyer

This is a fake line to confuse the stupid top-posting filter at gmane

We have a rather complicated class that, under certain circumstances, knows
that it cannot perform various arithmetic operations, and so returns
NotImplemented. As a trivial example:
... def __mul__(self, other):
... return NotImplemented
... Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: unsupported operand type(s) for *: 'instance' and 'instance'

This error message isn't hugely meaningful to many of our users (and in
complicated expressions, I'd certainly benefit from knowing exactly which
subclasses of 'my' are involved), but it beats the behavior with new-style
classes:
... def __mul__(self, other):
... return NotImplemented
... Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: can't multiply sequence to non-int

After a lot of googling and a lot of pouring over abstract.c, I now
understand that object() is defined with a tp_as_sequence, and so the error
message is the result of the last-ditch effort to do sequence concatentation.

What if I don't want to permit sequence concatenation?
Is there a way to unset tp_as_sequence?
Should I be inheriting from a different class? We started inheriting from
object because we want a __new__ method.

The "'instance' and 'instance'" message would be OK, but even better is the
result of this completely degenerate class:
... pass
... ... pass
... Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: unsupported operand type(s) for *: 'my' and 'your'

That's an error message I can actually do something with. Is there any way
to get this behavior when I do have a __mul__ method and sometimes return
NotImplemented?

We're doing most of our development in Python 2.3, if it matters.
 
S

Steven D'Aprano

We have a rather complicated class that, under certain circumstances, knows
that it cannot perform various arithmetic operations, and so returns
NotImplemented. As a trivial example:

... def __mul__(self, other):
... return NotImplemented
...
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: unsupported operand type(s) for *: 'instance' and 'instance'

This error message isn't hugely meaningful to many of our users (and in
complicated expressions, I'd certainly benefit from knowing exactly which
subclasses of 'my' are involved)

Why don't you raise the exception yourself?

(Note the difference between NotImplemented and NotImplementedError.)

.... def __mul__(self, other):
.... raise NotImplementedError("Can't multiply %s yet!" %
.... self.__class__.__name__)
....Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "<stdin>", line 3, in __mul__
NotImplementedError: Can't multiply Parrot yet!


I've always considered NotImplementedError to be for situations that I
just haven't got around to implementing *yet*. For operations which should
not be implemented because they aren't meaningful, I define my own
exceptions.


I'd never noticed the behaviour of Python where it takes a return value of
NotImplemented and raises a ValueError. Unless somebody can tell me why
this is justified, I consider this at best a wart. If I return something,
that's my return value! I don't see why arithmetic operations are special
enough to justify this special behaviour.
 
J

Jon Guyer

Steven D'Aprano said:
Why don't you raise the exception yourself?

(Note the difference between NotImplemented and NotImplementedError.)

Because although A may not know how to multiply B, B might know how to multiply A
I'd never noticed the behaviour of Python where it takes a return value of
NotImplemented and raises a ValueError. Unless somebody can tell me why
this is justified, I consider this at best a wart. If I return something,
that's my return value! I don't see why arithmetic operations are special
enough to justify this special behaviour.

Python Reference Manual, Section 3.2:

----

NotImplemented This type has a single value. There is a single object with
this value. This object is accessed through the built-in name
NotImplemented. Numeric methods and rich comparison methods may return this
value if they do not implement the operation for the operands provided. (The
interpreter will then try the reflected operation, or some other fallback,
depending on the operator.) Its truth value is true.

----

This is exactly the behavior we want. Our code paths are simpler and less
error prone if A and B don't both know how to multiply with each other, and
this seems to be exactly what NotImplemented and __mul__/__rmul__ are
designed for.
 
Z

ziga.seilnacht

Jon said:
We have a rather complicated class that, under certain circumstances, knows
that it cannot perform various arithmetic operations, and so returns
NotImplemented. As a trivial example:

... def __mul__(self, other):
... return NotImplemented
...
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: unsupported operand type(s) for *: 'instance' and 'instance'

This error message isn't hugely meaningful to many of our users (and in
complicated expressions, I'd certainly benefit from knowing exactly which
subclasses of 'my' are involved), but it beats the behavior with new-style
classes:

... def __mul__(self, other):
... return NotImplemented
...
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: can't multiply sequence to non-int

After a lot of googling and a lot of pouring over abstract.c, I now
understand that object() is defined with a tp_as_sequence, and so the error
message is the result of the last-ditch effort to do sequence concatentation.

What if I don't want to permit sequence concatenation?
Is there a way to unset tp_as_sequence?
Should I be inheriting from a different class? We started inheriting from
object because we want a __new__ method.

The "'instance' and 'instance'" message would be OK, but even better is the
result of this completely degenerate class:

... pass
...
... pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: unsupported operand type(s) for *: 'my' and 'your'

That's an error message I can actually do something with. Is there any way
to get this behavior when I do have a __mul__ method and sometimes return
NotImplemented?

We're doing most of our development in Python 2.3, if it matters.

This is a bug in Python. See this thread:
http://mail.python.org/pipermail/python-dev/2005-December/059046.html

and this patch:
http://sourceforge.net/tracker/?group_id=5470&atid=305470&func=detail&aid=1390657

for more details.
 
G

google

S

Steven D'Aprano

Because although A may not know how to multiply B, B might know how to multiply A

Given the way Python is now, that's fair enough. But see below:

Python Reference Manual, Section 3.2:

----

NotImplemented This type has a single value. There is a single object
with this value. This object is accessed through the built-in name
NotImplemented. Numeric methods and rich comparison methods may return
this value if they do not implement the operation for the operands
provided. (The interpreter will then try the reflected operation, or
some other fallback, depending on the operator.) Its truth value is
true.

----

This is exactly the behavior we want. Our code paths are simpler and
less error prone if A and B don't both know how to multiply with each
other, and this seems to be exactly what NotImplemented and
__mul__/__rmul__ are designed for.

And I argue that they *shouldn't be*. If my code returns some object,
Python shouldn't muck about with it. Making a special case behaviour for
arithmetic is poor design -- arithmetic isn't special enough to justify
the special behaviour.

If you need to trigger a special behaviour (such as "I don't know how to
do this multiplication, please try the other object") the correct way to
do it is with an exception, just as iterators use the StopIteration
exception to trigger special behaviour, or list iteration use IndexError.
Returning a magic value that is captured and handled specially is just
wrong.

Exceptions are more generous too: Python could use your exception's
message string when printing the traceback, instead of making its own.

All of the above is, of course, in my not-so-humble opinion, and none of
it solves your problem. Sorry.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,576
Members
45,054
Latest member
LucyCarper

Latest Threads

Top