Added more documentation on how mixed-mode arithmetic should be implemented. I

also noticed and fixed a bug in Rational's forward operators (they were
claiming all instances of numbers.Rational instead of just the concrete types).
This commit is contained in:
Jeffrey Yasskin 2008-01-31 07:44:11 +00:00
parent e973c61238
commit b23dea6adb
3 changed files with 222 additions and 13 deletions

View File

@ -99,3 +99,144 @@ The numeric tower
3-argument form of :func:`pow`, and the bit-string operations: ``<<``, 3-argument form of :func:`pow`, and the bit-string operations: ``<<``,
``>>``, ``&``, ``^``, ``|``, ``~``. Provides defaults for :func:`float`, ``>>``, ``&``, ``^``, ``|``, ``~``. Provides defaults for :func:`float`,
:attr:`Rational.numerator`, and :attr:`Rational.denominator`. :attr:`Rational.numerator`, and :attr:`Rational.denominator`.
Notes for type implementors
---------------------------
Implementors should be careful to make equal numbers equal and hash
them to the same values. This may be subtle if there are two different
extensions of the real numbers. For example, :class:`rational.Rational`
implements :func:`hash` as follows::
def __hash__(self):
if self.denominator == 1:
# Get integers right.
return hash(self.numerator)
# Expensive check, but definitely correct.
if self == float(self):
return hash(float(self))
else:
# Use tuple's hash to avoid a high collision rate on
# simple fractions.
return hash((self.numerator, self.denominator))
Adding More Numeric ABCs
~~~~~~~~~~~~~~~~~~~~~~~~
There are, of course, more possible ABCs for numbers, and this would
be a poor hierarchy if it precluded the possibility of adding
those. You can add ``MyFoo`` between :class:`Complex` and
:class:`Real` with::
class MyFoo(Complex): ...
MyFoo.register(Real)
Implementing the arithmetic operations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We want to implement the arithmetic operations so that mixed-mode
operations either call an implementation whose author knew about the
types of both arguments, or convert both to the nearest built in type
and do the operation there. For subtypes of :class:`Integral`, this
means that :meth:`__add__` and :meth:`__radd__` should be defined as::
class MyIntegral(Integral):
def __add__(self, other):
if isinstance(other, MyIntegral):
return do_my_adding_stuff(self, other)
elif isinstance(other, OtherTypeIKnowAbout):
return do_my_other_adding_stuff(self, other)
else:
return NotImplemented
def __radd__(self, other):
if isinstance(other, MyIntegral):
return do_my_adding_stuff(other, self)
elif isinstance(other, OtherTypeIKnowAbout):
return do_my_other_adding_stuff(other, self)
elif isinstance(other, Integral):
return int(other) + int(self)
elif isinstance(other, Real):
return float(other) + float(self)
elif isinstance(other, Complex):
return complex(other) + complex(self)
else:
return NotImplemented
There are 5 different cases for a mixed-type operation on subclasses
of :class:`Complex`. I'll refer to all of the above code that doesn't
refer to ``MyIntegral`` and ``OtherTypeIKnowAbout`` as
"boilerplate". ``a`` will be an instance of ``A``, which is a subtype
of :class:`Complex` (``a : A <: Complex``), and ``b : B <:
Complex``. I'll consider ``a + b``:
1. If ``A`` defines an :meth:`__add__` which accepts ``b``, all is
well.
2. If ``A`` falls back to the boilerplate code, and it were to
return a value from :meth:`__add__`, we'd miss the possibility
that ``B`` defines a more intelligent :meth:`__radd__`, so the
boilerplate should return :const:`NotImplemented` from
:meth:`__add__`. (Or ``A`` may not implement :meth:`__add__` at
all.)
3. Then ``B``'s :meth:`__radd__` gets a chance. If it accepts
``a``, all is well.
4. If it falls back to the boilerplate, there are no more possible
methods to try, so this is where the default implementation
should live.
5. If ``B <: A``, Python tries ``B.__radd__`` before
``A.__add__``. This is ok, because it was implemented with
knowledge of ``A``, so it can handle those instances before
delegating to :class:`Complex`.
If ``A<:Complex`` and ``B<:Real`` without sharing any other knowledge,
then the appropriate shared operation is the one involving the built
in :class:`complex`, and both :meth:`__radd__` s land there, so ``a+b
== b+a``.
Because most of the operations on any given type will be very similar,
it can be useful to define a helper function which generates the
forward and reverse instances of any given operator. For example,
:class:`rational.Rational` uses::
def _operator_fallbacks(monomorphic_operator, fallback_operator):
def forward(a, b):
if isinstance(b, (int, long, Rational)):
return monomorphic_operator(a, b)
elif isinstance(b, float):
return fallback_operator(float(a), b)
elif isinstance(b, complex):
return fallback_operator(complex(a), b)
else:
return NotImplemented
forward.__name__ = '__' + fallback_operator.__name__ + '__'
forward.__doc__ = monomorphic_operator.__doc__
def reverse(b, a):
if isinstance(a, RationalAbc):
# Includes ints.
return monomorphic_operator(a, b)
elif isinstance(a, numbers.Real):
return fallback_operator(float(a), float(b))
elif isinstance(a, numbers.Complex):
return fallback_operator(complex(a), complex(b))
else:
return NotImplemented
reverse.__name__ = '__r' + fallback_operator.__name__ + '__'
reverse.__doc__ = monomorphic_operator.__doc__
return forward, reverse
def _add(a, b):
"""a + b"""
return Rational(a.numerator * b.denominator +
b.numerator * a.denominator,
a.denominator * b.denominator)
__add__, __radd__ = _operator_fallbacks(_add, operator.add)
# ...

View File

@ -292,7 +292,13 @@ class Rational(Real, Exact):
# Concrete implementation of Real's conversion to float. # Concrete implementation of Real's conversion to float.
def __float__(self): def __float__(self):
"""float(self) = self.numerator / self.denominator""" """float(self) = self.numerator / self.denominator
It's important that this conversion use the integer's "true"
division rather than casting one side to float before dividing
so that ratios of huge integers convert without overflowing.
"""
return self.numerator / self.denominator return self.numerator / self.denominator

View File

@ -179,16 +179,6 @@ class Rational(RationalAbc):
else: else:
return '%s/%s' % (self.numerator, self.denominator) return '%s/%s' % (self.numerator, self.denominator)
""" XXX This section needs a lot more commentary
* Explain the typical sequence of checks, calls, and fallbacks.
* Explain the subtle reasons why this logic was needed.
* It is not clear how common cases are handled (for example, how
does the ratio of two huge integers get converted to a float
without overflowing the long-->float conversion.
"""
def _operator_fallbacks(monomorphic_operator, fallback_operator): def _operator_fallbacks(monomorphic_operator, fallback_operator):
"""Generates forward and reverse operators given a purely-rational """Generates forward and reverse operators given a purely-rational
operator and a function from the operator module. operator and a function from the operator module.
@ -196,10 +186,82 @@ class Rational(RationalAbc):
Use this like: Use this like:
__op__, __rop__ = _operator_fallbacks(just_rational_op, operator.op) __op__, __rop__ = _operator_fallbacks(just_rational_op, operator.op)
In general, we want to implement the arithmetic operations so
that mixed-mode operations either call an implementation whose
author knew about the types of both arguments, or convert both
to the nearest built in type and do the operation there. In
Rational, that means that we define __add__ and __radd__ as:
def __add__(self, other):
if isinstance(other, (int, long, Rational)):
# Do the real operation.
return Rational(self.numerator * other.denominator +
other.numerator * self.denominator,
self.denominator * other.denominator)
# float and complex don't follow this protocol, and
# Rational knows about them, so special case them.
elif isinstance(other, float):
return float(self) + other
elif isinstance(other, complex):
return complex(self) + other
else:
# Let the other type take over.
return NotImplemented
def __radd__(self, other):
# radd handles more types than add because there's
# nothing left to fall back to.
if isinstance(other, RationalAbc):
return Rational(self.numerator * other.denominator +
other.numerator * self.denominator,
self.denominator * other.denominator)
elif isinstance(other, Real):
return float(other) + float(self)
elif isinstance(other, Complex):
return complex(other) + complex(self)
else:
return NotImplemented
There are 5 different cases for a mixed-type addition on
Rational. I'll refer to all of the above code that doesn't
refer to Rational, float, or complex as "boilerplate". 'r'
will be an instance of Rational, which is a subtype of
RationalAbc (r : Rational <: RationalAbc), and b : B <:
Complex. The first three involve 'r + b':
1. If B <: Rational, int, float, or complex, we handle
that specially, and all is well.
2. If Rational falls back to the boilerplate code, and it
were to return a value from __add__, we'd miss the
possibility that B defines a more intelligent __radd__,
so the boilerplate should return NotImplemented from
__add__. In particular, we don't handle RationalAbc
here, even though we could get an exact answer, in case
the other type wants to do something special.
3. If B <: Rational, Python tries B.__radd__ before
Rational.__add__. This is ok, because it was
implemented with knowledge of Rational, so it can
handle those instances before delegating to Real or
Complex.
The next two situations describe 'b + r'. We assume that b
didn't know about Rational in its implementation, and that it
uses similar boilerplate code:
4. If B <: RationalAbc, then __radd_ converts both to the
builtin rational type (hey look, that's us) and
proceeds.
5. Otherwise, __radd__ tries to find the nearest common
base ABC, and fall back to its builtin type. Since this
class doesn't subclass a concrete type, there's no
implementation to fall back to, so we need to try as
hard as possible to return an actual value, or the user
will get a TypeError.
""" """
def forward(a, b): def forward(a, b):
if isinstance(b, RationalAbc): if isinstance(b, (int, long, Rational)):
# Includes ints.
return monomorphic_operator(a, b) return monomorphic_operator(a, b)
elif isinstance(b, float): elif isinstance(b, float):
return fallback_operator(float(a), b) return fallback_operator(float(a), b)