Numeric Type Special Methods
When creating a new numeric data type, you must provide
definitions for the essential mathematical and logical operators. When
we write an expression using the usual +, -, *, and /, Python transforms
this to method function calls. Consider the following:
v1= MyClass(10,20)
v2= MyClass(20,40)
x = v1 + v2
In this case, Python will evaluate line 3 as if you had
written:
x = v1.__add__( v2 )
Every arithmetic operator is transformed into a method function
call. By defining the numeric special methods, your class willwork with
the built-in arithmetic operators. There are, however, some subtleties
to this.
Forward, Reverse and In-Place Method Functions. First, there are as many as three variant methods required to
implement each operation. For example, * is implemented by
__mul__
, __rmul__
and
__imul__
. There are forward and reverse special
methods so that you can assure that your operator is properly
commutative. There is an in-place special method so that you can
implement augmented assignment efficiently (see the section called “Augmented Assignment”).
You don't need to implement all three versions. If you implement
just the forward version, and your program does nothing too odd or
unusual, everything will work out well. The reverse name is used for
special situations that involve objects of multiple classes.
Python makes two attempts to locate an appropriate method function
for an operator. First, it tries a class based on the left-hand operand
using the "forward" name. If no suitable special method is found, it
tries the the right-hand operand, using the "reverse" name.
Consider the following:
v1= MyClass(10,20)
x = v1 * 14
y = 28 * v1
Both lines 2 and 3 require conversions between the built-in
integer type and MyClass
. For line 2, the forward
name is used. The expression v1*14
is evaluated as if it
was
x = v1.__mul__( 14 )
For line 3, the reverse name is used. The expression
28*v1
is evaluated as if it was
y = v1.__rmul__( 28 )
The Operator Algorithm. The algorithm for determing what happens with
x
op
y
is approximately as follows.
Historically, as Python has evolved, so have the ins and outs of
argument coercion. In the future, beginning with Python 3.0, the older
notion of type coercion and the coerce
function
will be dropped altogether, so we'll focus on the enduring features
that will be preserved. Section 3.4.8 of the Python
Language Reference covers this in more detail; along with
the caveat that the rules have gotten too complex.
Note than a special method function can return the value
NotImplemented
. This indicates that the operation
can't work directly only the values, and another operation should be
chosen. The rules provide for a number of alternative operations, this
allows a class to be designed in a way that will cooperate successfully
with potential future subclasses.
-
The expression
string
%
anything
is a special case and
is handled first. This assures us that the value of
anything
is left untouched by any other
rules. Generally, it is a tuple or a dictionary, and should be left
as such.
-
If this is an augmented assignment statement (known as an
in-place operator, e.g.,
a
+=
b
)
where the left operand implements __iop__
, then
the __iop__
special method is invoked without
any coercion. These in-place operators permit you to do an efficient
udpate the left operand object instead of creating a new
object.
-
As a special case, if an operator's left operand is an object
of a superclass of the right operand's class, the right operand's
__rop__
(subclass) method is tried first. If
this is not implemented or returns
NotImplemented
, then the the left operand's
__op__
(superclass) method is used. This is
done so that a subclass can completely override binary operators,
even for built-in types.
-
Generally,
x.__op__
(
y
) is tried
first. If this is not implemented or returns
NotImplemented
,
y.__rop__
(
x
) is tried
second. In the case of
The following functions are the “forward” operations,
used to implement the associated expressions.
method function |
original expression |
__add__ (
self
,
other
) |
self
+
other
|
__sub__ (
self
,
other
) |
self
-
other
|
__mul__ (
self
,
other
) |
self
*
other
|
__div__ (
self
,
other
) |
self
/
other
|
__mod__ (
self
,
other
) |
self
%
other
|
__divmod__ (
self
,
other
) |
divmod (
self
,
other
) |
__pow__ (
self
,
other
, [
modulo
] ) |
self
**
other
or pow
(
self
,
other
,
[
modulo
] ) |
__lshift__ (
self
,
other
) |
self
<<
other
|
__rshift__ (
self
,
other
) |
self
>>
other
|
__and__ (
self
,
other
) |
self
and
other
|
__xor__ (
self
,
other
) |
self
xor
other
|
__or__ (
self
,
other
) |
self
or
other
|
The method functions in this group are used to resolve operators
using by attempting them using a reversed sense.
method function |
original expression |
__radd__ (
self
,
other
) |
other
+
self
|
__rsub__ (
self
,
other
) |
other
-
self
|
__rmul__ (
self
,
other
) |
other
*
self
|
__rdiv__ (
self
,
other
) |
other
/
self
|
__rmod__ (
self
,
other
) |
other
%
self
|
__rdivmod__ (
self
,
other
) |
divmod (
other
,
self
) |
__rpow__ (
self
,
other
) |
other
**
self
or pow
(
other
,
self
) |
__rlshift__ (
self
,
other
) |
other
<<
self
|
__rrshift__ (
self
,
other
) |
other
>>
self
|
__rand__ (
self
,
other
) |
other
and
self
|
__rxor__ (
self
,
other
) |
other
xor
self
|
__ror__ (
self
,
other
) |
other
or
self
|
The method functions in the following group implement the basic
unary operators.
method function |
original expression |
__neg__ (
self
) |
-
self
|
__pos__ (
self
) |
+
self
|
__abs__ (
self
) |
abs (
self
) |
__invert__ (
self
) |
~
self
|
__complex__ (
self
) |
complex
(
self
) |
__int__ (
self
) |
int (
self
) |
__long__ (
self
) |
long (
self
) |
__float__ (
self
) |
float (
self
) |
__oct__ (
self
) |
oct (
self
) |
__hex__ (
self
) |
hex (
self
) |
Rational Number Example. Consider a small example of a number-like class. The the section called “Rational Numbers” exercise in Chapter 21, Classes
describes the basic structure of a class to
handle rational math, where every number is represented as a fraction.
We'll add some of the special methods required to make this a proper
numeric type. We'll finish this in the exercises.
class Rational( object ):
def __init__( self, num, denom= 1L ):
self.n= long(num)
self.d= long(denom)
def __add__( self, other ):
return Rational( self.n*other.d + other.n*self.d,
self.d*other.d )
def __str__( self ):
return "%d/%d" % ( self.n, self.d )
This class has enough methods defined to allow us to add fractions
as follows:
>>>
x = Rational( 3, 4 )
>>>
y = Rational( 1, 3 )
>>>
print x+y
7/12
>>>
In order to complete this class, we would need to provide most of
the rest of the basic special method names (there is no need to provide
a definition for __del__
). We would also complete
the numeric special method names.
Additionally, we would have to provide correct algorithms that
reduced fractions, plus an additional conversion to respond with a mixed
number instead of an improper fraction. We'll revisit this in the
exercises.
Conversions From Other Types. For your class to be used successfully, your new numeric type
should work in conjunction with existing Python types. You will need
to use the isinstance
function to examine the
arguments and make appropriate conversions.
Consider the following expressions:
x = Rational( 22, 7 )
y = x+3
z = x+0.5
Variables y
and z
should be
created as Rational
fractions. However, our
initial __add__
function assumed that the
other
object is a
Rational
object. Generally, numeric classes must
be implemented with tests for various other data types and appropriate
conversions.
We have to use the isinstance
function to
perform checks like the following: isinstance( other, int
)
. This allows us to detect the various Python built-in
types.
If the result of
isinstance
(
other
,
factory
)
is true in any of the following cases, some type of simple conversion
should be done, if possible.
-
isinstance( other, complex ). You may want to raise an exception here, since it's hard to
see how to make rational fractions and complex numbers
conformable. If this is a common situation in your application,
you might need to write an even more sophisticated class that
implements complex numbers as a kind of rational fraction. Another
choice is to write a version of the abs
function of the complex number, which creates a proper rational
fraction for the complex magnitude of the given value.
-
isinstance( other, float ). One choice is to truncate the value of
other
to long
, using the
built-in long
function and treat it as a
whole number, the other choice is to determine a fraction that
approximates the floating point value.
-
isinstance( other, (int,long) ). Any of these means that the other
value
is clearly the numerator of a fraction, with a denominator of
1.
-
isinstance( other, str ) or isinstance( other, unicode ) or
isinstance( other, basestring ). Any of these might convert the other
value to a long
using the built-in
long
function. If the conversion fails, an
exception will be thrown, which will make the error obvious. The
basestring
type, by the way, is the
superclass for ASCII strings (str
) and
Unicode strings (unicode
).
-
isinstance( other, Rational ). This indicates that the other
value is an
instance of our Rational
class; we can do
the processing as expected, knowing that the object has all the
methods and attributes we need.
Here is a version of __sub__
with an example
of type checking. If the
other
argument is an
instance of the class Rational
, we can perform
the subtract operation. Otherwise, we attempt to convert the
other
argument to an instance of
Rational
and attempt the subtraction between two
Rational
s.
def __sub__( self, other ):
if isinstance(other,Rational):
return Rational( self.n*other.d - other.n*self.d,
self.d*other.d )
else:
return self - Rational(long(other))
An alternative to the last line of code is the following.
return Rational( self.n-long(other)*self.d, self.d )
While this second version performs somewhat quicker, it expresses
the basic rational addition algorithm twice, once in the
if:
suite and again in the
else:
suite. A principle of object oriented programming is to maximize reuse
and minimize restating an algorithm. My preference is to state the
algorithm exactly once and reuse it as much as possible.
Reverse Operators. In many cases, Python will reverse the two operands, and use a
function like __rsub__
or
__rdiv__
. For example:
def __rsub__( self, other ):
if isinstance(other,Rational):
return Rational( other.n*self.d - self.n*other.d,
self.d*other.d )
else:
return Rational(long(other)) - self
You can explore this behavior with short test programs like the
following:
x = Rational( 3,4 )
print x-5
print 5-x