[Python-3000] Updated and simplified PEP 3141: A Type Hierarchy for Numbers

Jeffrey Yasskin jyasskin at gmail.com
Thu May 17 02:31:50 CEST 2007


I've updated PEP3141 to remove the algebraic classes and bring the
numeric hierarchy much closer to scheme's design. Let me know what you
think. Feel free to send typographical and formatting problems just to
me. My schedule's a little shaky the next couple weeks, but I'll make
updates as quickly as I can.



PEP: 3141
Title: A Type Hierarchy for Numbers
Version: $Revision: 54928 $
Last-Modified: $Date: 2007-04-23 16:37:29 -0700 (Mon, 23 Apr 2007) $
Author: Jeffrey Yasskin <jyasskin at gmail.com>
Status: Draft
Type: Standards Track
Content-Type: text/x-rst
Created: 23-Apr-2007
Post-History: Not yet posted


Abstract
========

This proposal defines a hierarchy of Abstract Base Classes (ABCs) (PEP
3119) to represent number-like classes. It proposes a hierarchy of
``Number :> Complex :> Real :> Rational :> Integer`` where ``A :> B``
means "A is a supertype of B", and a pair of ``Exact``/``Inexact``
classes to capture the difference between ``floats`` and
``ints``. These types are significantly inspired by Scheme's numeric
tower [#schemetower]_.

Rationale
=========

Functions that take numbers as arguments should be able to determine
the properties of those numbers, and if and when overloading based on
types is added to the language, should be overloadable based on the
types of the arguments. For example, slicing requires its arguments to
be ``Integers``, and the functions in the ``math`` module require
their arguments to be ``Real``.

Specification
=============

This PEP specifies a set of Abstract Base Classes with default
implementations. If the reader prefers to think in terms of Roles (PEP
3133), the default implementations for (for example) the Real ABC
would be moved to a RealDefault class, with Real keeping just the
method declarations.

Although this PEP uses terminology from PEP 3119, the hierarchy is
intended to be meaningful for any systematic method of defining sets
of classes, including Interfaces. I'm also using the extra notation
from PEP 3107 (Function Annotations) to specify some types.


Exact vs. Inexact Classes
-------------------------

Floating point values may not exactly obey several of the properties
you would expect. For example, it is possible for ``(X + -X) + 3 ==
3``, but ``X + (-X + 3) == 0``. On the range of values that most
functions deal with this isn't a problem, but it is something to be
aware of.

Therefore, I define ``Exact`` and ``Inexact`` ABCs to mark whether
types have this problem. Every instance of ``Integer`` and
``Rational`` should be Exact, but ``Reals`` and ``Complexes`` may or
may not be. (Do we really only need one of these, and the other is
defined as ``not`` the first?)::

    class Exact(metaclass=MetaABC): pass
    class Inexact(metaclass=MetaABC): pass


Numeric Classes
---------------

We begin with a Number class to make it easy for people to be fuzzy
about what kind of number they expect. This class only helps with
overloading; it doesn't provide any operations. **Open question:**
Should it specify ``__add__``, ``__sub__``, ``__neg__``, ``__mul__``,
and ``__abs__`` like Haskell's ``Num`` class?::

    class Number(metaclass=MetaABC): pass


Some types (primarily ``float``) define "Not a Number" (NaN) values
that return false for any comparison, including equality with
themselves, and are maintained through operations. Because this
doesn't work well with the Reals (which are otherwise totally ordered
by ``<``), Guido suggested we might put NaN in its own type. It is
conceivable that this can still be represented by C doubles but be
included in a different ABC at runtime. **Open issue:** Is this a good
idea?::

    class NotANumber(Number):
        """Implement IEEE 754 semantics."""
        def __lt__(self, other): return false
        def __eq__(self, other): return false
        ...
        def __add__(self, other): return self
        def __radd__(self, other): return self
        ...

Complex numbers are immutable and hashable. Implementors should be
careful that they 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::

    class Complex(Hashable, Number):
        """A ``Complex`` should define the operations that work on the
        Python ``complex`` type. If it is given heterogenous
        arguments, it may fall back on this class's definition of the
        operations. These operators should never return a
        TypeError as long as both arguments are instances of Complex
        (or even just implement __complex__).
        """
        @abstractmethod
        def __complex__(self):
            """This operation gives the arithmetic operations a fallback.
            """
            return complex(self.real, self.imag)
        @property
        def real(self):
            return complex(self).real
        @property
        def imag(self):
            return complex(self).imag

I define the reversed operations here so that they serve as the final
fallback for operations involving instances of Complex. **Open
issue:** Should Complex's operations check for ``isinstance(other,
Complex)``? Duck typing seems to imply that we should just try
__complex__ and succeed if it works, but stronger typing might be
justified for the operators. TODO: analyze the combinations of normal
and reversed operations with real and virtual subclasses of Complex::

        def __radd__(self, other):
            """Should this catch any type errors and return
            NotImplemented instead?"""
            return complex(other) + complex(self)
        def __rsub__(self, other):
            return complex(other) - complex(self)
        def __neg__(self):
            return -complex(self)
        def __rmul__(self, other):
            return complex(other) * complex(self)
        def __rdiv__(self, other):
            return complex(other) / complex(self)

        def __abs__(self):
            return abs(complex(self))

        def conjugate(self):
            return complex(self).conjugate()

        def __hash__(self):
            """Two "equal" values of different complex types should
            hash in the same way."""
	    return hash(complex(self))


The ``Real`` ABC indicates that the value is on the real line, and
supports the operations of the ``float`` builtin. Real numbers are
totally ordered. (NaNs were handled above.)::

    class Real(Complex, metaclass=TotallyOrderedABC):
        @abstractmethod
	def __float__(self):
	    """Any Real can be converted to a native float object."""
	    raise NotImplementedError
        def __complex__(self):
            """Which gives us an easy way to define the conversion to
            complex."""
            return complex(float(self))
        @property
        def real(self): return self
        @property
        def imag(self): return 0

        def __radd__(self, other):
            if isinstance(other, Real):
                return float(other) + float(self)
            else:
                return super(Real, self).__radd__(other)
        def __rsub__(self, other):
            if isinstance(other, Real):
                return float(other) - float(self)
            else:
                return super(Real, self).__rsub__(other)
        def __neg__(self):
            return -float(self)
        def __rmul__(self, other):
            if isinstance(other, Real):
                return float(other) * float(self)
            else:
                return super(Real, self).__rmul__(other)
        def __rdiv__(self, other):
            if isinstance(other, Real):
                return float(other) / float(self)
            else:
                return super(Real, self).__rdiv__(other)
        def __rdivmod__(self, other):
            """Implementing divmod() for your type is sufficient to
            get floordiv and mod too.
            """
            if isinstance(other, Real):
                return divmod(float(other), float(self))
            else:
                return super(Real, self).__rdivmod__(other)
        def __rfloordiv__(self, other):
            return divmod(other, self)[0]
        def __rmod__(self, other):
            return divmod(other, self)[1]

	def __trunc__(self):
            """Do we want properfraction, floor, ceiling, and round?"""
            return trunc(float(self))

        def __abs__(self):
            return abs(float(self))

There is no way to define only the reversed comparison operators, so
these operations take precedence over any defined in the other
type. :( ::

        def __lt__(self, other):
            """The comparison operators in Python seem to be more
            strict about their input types than other functions. I'm
            guessing here that we want types to be incompatible even
            if they define a __float__ operation, unless they also
            declare themselves to be Real numbers.
            """
            if isinstance(other, Real):
                return float(self) < float(other)
            else:
                return NotImplemented

        def __le__(self, other):
            if isinstance(other, Real):
                return float(self) <= float(other)
            else:
                return NotImplemented

        def __eq__(self, other):
            if isinstance(other, Real):
                return float(self) == float(other)
            else:
                return NotImplemented


There is no built-in rational type, but it's straightforward to write,
so we provide an ABC for it::

    class Rational(Real, Exact):
        """rational.numerator and rational.denominator should be in
        lowest terms.
        """
        @abstractmethod
        @property
        def numerator(self):
            raise NotImplementedError
        @abstractmethod
        @property
        def denominator(self):
            raise NotImplementedError

        def __float__(self):
            return self.numerator / self.denominator


    class Integer(Rational):
	@abstractmethod
	def __int__(self):
	    raise NotImplementedError
	def __float__(self):
	    return float(int(self))
        @property
        def numerator(self): return self
        @property
        def denominator(self): return 1

	def __ror__(self, other):
	    return int(other) | int(self)
	def __rxor__(self, other):
	    return int(other) ^ int(self)
	def __rand__(self, other):
	    return int(other) & int(self)
	def __rlshift__(self, other):
	    return int(other) << int(self)
	def __rrshift__(self, other):
	    return int(other) >> int(self)
	def __invert__(self):
	    return ~int(self)

        def __radd__(self, other):
            """All of the Real methods need to be overridden here too
            in order to get a more exact type for their results.
            """
            if isinstance(other, Integer):
                return int(other) + int(self)
            else:
                return super(Integer, self).__radd__(other)
        ...

        def __hash__(self):
            """Surprisingly, hash() needs to be overridden too, since
            there are integers that float can't represent."""
            return hash(int(self))


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 ``Complex`` and ``Real`` with::

    class MyFoo(Complex): ...
    MyFoo.register(Real)

TODO(jyasskin): Check this.


Rejected Alternatives
=====================

The initial version of this PEP defined an algebraic hierarchy
inspired by a Haskell Numeric Prelude [#numericprelude]_ including
MonoidUnderPlus, AdditiveGroup, Ring, and Field, and mentioned several
other possible algebraic types before getting to the numbers. I had
expected this to be useful to people using vectors and matrices, but
the NumPy community really wasn't interested. The numbers then had a
much more branching structure to include things like the Gaussian
Integers and Z/nZ, which could be Complex but wouldn't necessarily
support things like division. The community decided that this was too
much complication for Python, so the proposal has been scaled back to
resemble the Scheme numeric tower much more closely.

References
==========

.. [#pep3119] Introducing Abstract Base Classes
   (http://www.python.org/dev/peps/pep-3119/)

.. [#pep3107] Function Annotations
   (http://www.python.org/dev/peps/pep-3107/)

.. [3] Possible Python 3K Class Tree?, wiki page created by Bill Janssen
   (http://wiki.python.org/moin/AbstractBaseClasses)

.. [#numericprelude] NumericPrelude: An experimental alternative
hierarchy of numeric type classes
   (http://darcs.haskell.org/numericprelude/docs/html/index.html)

.. [#schemetower] The Scheme numerical tower
   (http://www.swiss.ai.mit.edu/ftpdir/scheme-reports/r5rs-html/r5rs_8.html#SEC50)


Acknowledgements
================

Thanks to Neil Norwitz for encouraging me to write this PEP in the
first place, to Travis Oliphant for pointing out that the numpy people
didn't really care about the algebraic concepts, to Alan Isaac for
reminding me that Scheme had already done this, and to Guido van
Rossum and lots of other people on the mailing list for refining the
concept.

Copyright
=========

This document has been placed in the public domain.


More information about the Python-3000 mailing list