578 lines
18 KiB
Plaintext
578 lines
18 KiB
Plaintext
PEP: 3141
|
||
Title: A Type Hierarchy for Numbers
|
||
Version: $Revision$
|
||
Last-Modified: $Date$
|
||
Author: Jeffrey Yasskin <jyasskin@gmail.com>
|
||
Status: Draft
|
||
Type: Standards Track
|
||
Content-Type: text/x-rst
|
||
Created: 23-Apr-2007
|
||
Post-History: 25-Apr-2007, 16-May-2007, 02-Aug-2007
|
||
|
||
|
||
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 :> Integral`` 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 ``Integrals``, and the functions in the ``math`` module require
|
||
their arguments to be ``Real``.
|
||
|
||
Specification
|
||
=============
|
||
|
||
This PEP specifies a set of Abstract Base Classes, and suggests a
|
||
general strategy for implementing some of the methods. It uses
|
||
terminology from PEP 3119, but the hierarchy is intended to be
|
||
meaningful for any systematic method of defining sets of classes.
|
||
|
||
The type checks in the standard library should use these classes
|
||
instead of the concrete built-ins.
|
||
|
||
|
||
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. ::
|
||
|
||
class Number(metaclass=ABCMeta): pass
|
||
|
||
|
||
Most implementations of complex numbers will be hashable, but if you
|
||
need to rely on that, you'll have to check it explicitly: mutable
|
||
numbers are supported by this hierarchy. ::
|
||
|
||
class Complex(Number):
|
||
"""Complex defines the operations that work on the builtin complex type.
|
||
|
||
In short, those are: a conversion to complex, .real, .imag, +, -,
|
||
*, /, abs(), .conjugate, ==, and !=.
|
||
|
||
If it is given heterogenous arguments, and doesn't have special
|
||
knowledge about them, it should fall back to the builtin complex
|
||
type as described below.
|
||
"""
|
||
|
||
@abstractmethod
|
||
def __complex__(self):
|
||
"""Return a builtin complex instance."""
|
||
|
||
def __bool__(self):
|
||
"""True if self != 0."""
|
||
return self != 0
|
||
|
||
@abstractproperty
|
||
def real(self):
|
||
"""Retrieve the real component of this number.
|
||
|
||
This should subclass Real.
|
||
"""
|
||
raise NotImplementedError
|
||
|
||
@abstractproperty
|
||
def imag(self):
|
||
"""Retrieve the real component of this number.
|
||
|
||
This should subclass Real.
|
||
"""
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __add__(self, other):
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __radd__(self, other):
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __neg__(self):
|
||
raise NotImplementedError
|
||
|
||
def __pos__(self):
|
||
"""+self
|
||
|
||
Coerces self to whatever class defines the method.
|
||
"""
|
||
raise NotImplementedError
|
||
|
||
def __sub__(self, other):
|
||
return self + -other
|
||
|
||
def __rsub__(self, other):
|
||
return -self + other
|
||
|
||
@abstractmethod
|
||
def __mul__(self, other):
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __rmul__(self, other):
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __div__(self, other):
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __rdiv__(self, other):
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __pow__(self, exponent):
|
||
"""Like division, a**b should promote to complex when necessary."""
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __rpow__(self, base):
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __abs__(self):
|
||
"""Returns the Real distance from 0."""
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def conjugate(self):
|
||
"""(x+y*i).conjugate() returns (x-y*i)."""
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __eq__(self, other):
|
||
raise NotImplementedError
|
||
|
||
def __ne__(self, other):
|
||
return not (self == other)
|
||
|
||
|
||
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 except for NaNs (which this PEP basically ignores). ::
|
||
|
||
class Real(Complex):
|
||
"""To Complex, Real adds the operations that work on real numbers.
|
||
|
||
In short, those are: a conversion to float, trunc(), divmod,
|
||
%, <, <=, >, and >=.
|
||
|
||
Real also provides defaults for the derived operations.
|
||
"""
|
||
|
||
@abstractmethod
|
||
def __float__(self):
|
||
"""Any Real can be converted to a native float object."""
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __trunc__(self):
|
||
"""Truncates self to an Integral.
|
||
|
||
Returns an Integral i such that:
|
||
* i>=0 iff self>0
|
||
* abs(i) <= abs(self).
|
||
* for any Integral j satisfying the first two conditions,
|
||
abs(i) >= abs(j) [i.e. i has "maximal" abs among those]
|
||
i.e. "truncate towards 0".
|
||
"""
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __floor__(self):
|
||
"""Finds the greatest Integral <= self."""
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __ceil__(self):
|
||
"""Finds the least Integral >= self."""
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __round__(self, ndigits:Integral=None):
|
||
"""Rounds self to ndigits decimal places, defaulting to 0.
|
||
|
||
If ndigits is omitted, returns an Integral, otherwise
|
||
returns a Real. Rounds half toward even.
|
||
"""
|
||
raise NotImplementedError
|
||
|
||
def __divmod__(self, other):
|
||
"""The pair (self // other, self % other).
|
||
|
||
Sometimes this can be computed faster than the pair of
|
||
operations.
|
||
"""
|
||
return (self // other, self % other)
|
||
|
||
def __rdivmod__(self, other):
|
||
"""The pair (self // other, self % other).
|
||
|
||
Sometimes this can be computed faster than the pair of
|
||
operations.
|
||
"""
|
||
return (other // self, other % self)
|
||
|
||
@abstractmethod
|
||
def __floordiv__(self, other):
|
||
"""The floor() of self/other. Integral."""
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __rfloordiv__(self, other):
|
||
"""The floor() of other/self."""
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __mod__(self, other):
|
||
"""self % other
|
||
|
||
See
|
||
http://mail.python.org/pipermail/python-3000/2006-May/001735.html
|
||
and consider using "self/other - trunc(self/other)"
|
||
instead if you're worried about round-off errors.
|
||
"""
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __rmod__(self, other):
|
||
"""other % self"""
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __lt__(self, other):
|
||
"""< on Reals defines a total ordering, except perhaps for NaN."""
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __le__(self, other):
|
||
raise NotImplementedError
|
||
|
||
# Concrete implementations of Complex abstract methods.
|
||
|
||
def __complex__(self):
|
||
return complex(float(self))
|
||
|
||
@property
|
||
def real(self):
|
||
return self
|
||
|
||
@property
|
||
def imag(self):
|
||
return 0
|
||
|
||
def conjugate(self):
|
||
"""Conjugate is a no-op for Reals."""
|
||
return self
|
||
|
||
|
||
We need to clean up Demo/classes/Rat.py and promote it into
|
||
rational.py in the standard library. Then it will implement the
|
||
Rational ABC. ::
|
||
|
||
class Rational(Real, Exact):
|
||
""".numerator and .denominator should be in lowest terms."""
|
||
|
||
@abstractproperty
|
||
def numerator(self):
|
||
raise NotImplementedError
|
||
|
||
@abstractproperty
|
||
def denominator(self):
|
||
raise NotImplementedError
|
||
|
||
# Concrete implementation of Real's conversion to float.
|
||
|
||
def __float__(self):
|
||
return self.numerator / self.denominator
|
||
|
||
|
||
And finally integers::
|
||
|
||
class Integral(Rational):
|
||
"""Integral adds a conversion to int and the bit-string operations."""
|
||
|
||
@abstractmethod
|
||
def __int__(self):
|
||
raise NotImplementedError
|
||
|
||
def __index__(self):
|
||
return int(self)
|
||
|
||
@abstractmethod
|
||
def __pow__(self, exponent, modulus):
|
||
"""self ** exponent % modulus, but maybe faster.
|
||
|
||
Implement this if you want to support the 3-argument version
|
||
of pow(). Otherwise, just implement the 2-argument version
|
||
described in Complex. Raise a TypeError if exponent < 0 or any
|
||
argument isn't Integral.
|
||
"""
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __lshift__(self, other):
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __rlshift__(self, other):
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __rshift__(self, other):
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __rrshift__(self, other):
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __and__(self, other):
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __rand__(self, other):
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __xor__(self, other):
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __rxor__(self, other):
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __or__(self, other):
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __ror__(self, other):
|
||
raise NotImplementedError
|
||
|
||
@abstractmethod
|
||
def __invert__(self):
|
||
raise NotImplementedError
|
||
|
||
# Concrete implementations of Rational and Real abstract methods.
|
||
|
||
def __float__(self):
|
||
return float(int(self))
|
||
|
||
@property
|
||
def numerator(self):
|
||
return self
|
||
|
||
@property
|
||
def denominator(self):
|
||
return 1
|
||
|
||
|
||
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 ``Integral`` 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(Number): pass
|
||
class Inexact(Number): pass
|
||
|
||
|
||
Changes to operations and __magic__ methods
|
||
-------------------------------------------
|
||
|
||
To support more precise narrowing from float to int (and more
|
||
generally, from Real to Integral), I'm proposing the following new
|
||
__magic__ methods, to be called from the corresponding library
|
||
functions. All of these return Integrals rather than Reals.
|
||
|
||
1. ``__trunc__(self)``, called from a new builtin ``trunc(x)``, which
|
||
returns the Integral closest to ``x`` between 0 and ``x``.
|
||
|
||
2. ``__floor__(self)``, called from ``math.floor(x)``, which returns
|
||
the greatest Integral ``<= x``.
|
||
|
||
3. ``__ceil__(self)``, called from ``math.ceil(x)``, which returns the
|
||
least Integral ``>= x``.
|
||
|
||
4. ``__round__(self)``, called from ``round(x)``, which returns the
|
||
Integral closest to ``x``, rounding half toward even. The
|
||
2-argument version should return a Real.
|
||
|
||
Because the ``int()`` conversion from ``float`` is equivalent to but
|
||
less explicit than ``trunc()``, let's remove it. (Or, if that breaks
|
||
too much, just add a deprecation warning.)
|
||
|
||
``complex.__{divmod,mod,floordiv,int,float}__`` also go away. It would
|
||
be nice to provide a nice error message to help confused porters, but
|
||
not appearing in ``help(complex)`` is more important.
|
||
|
||
|
||
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, a complex type
|
||
could reasonably implement hash() as follows::
|
||
|
||
def __hash__(self):
|
||
return hash(complex(self))
|
||
|
||
but should be careful of any values that fall outside of the built in
|
||
complex's range or precision.
|
||
|
||
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)
|
||
|
||
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 Integral, this means that
|
||
__add__ and __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 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 ``Complex`` (``a : A <:
|
||
Complex``), and ``b : B <: Complex``. I'll consider ``a + b``:
|
||
|
||
1. If A defines an __add__ which accepts b, all is well.
|
||
2. If A 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__. (Or A may not implement __add__ at
|
||
all.)
|
||
3. Then B's __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 Complex.
|
||
|
||
If ``A<:Complex`` and ``B<:Real`` without sharing any other knowledge,
|
||
then the appropriate shared operation is the one involving the built
|
||
in complex, and both __radd__s land there, so ``a+b == b+a``.
|
||
|
||
|
||
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, and we ran into the
|
||
issue that even if ``x`` is an instance of ``X <: MonoidUnderPlus``
|
||
and ``y`` is an instance of ``Y <: MonoidUnderPlus``, ``x + y`` may
|
||
still not make sense.
|
||
|
||
Then I gave the numbers 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 I've now
|
||
scaled back the proposal to resemble the Scheme numeric tower much
|
||
more closely.
|
||
|
||
|
||
References
|
||
==========
|
||
|
||
.. [#pep3119] Introducing Abstract Base Classes
|
||
(http://www.python.org/dev/peps/pep-3119/)
|
||
|
||
.. [#classtree] 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.
|
||
|
||
|
||
|
||
..
|
||
Local Variables:
|
||
mode: indented-text
|
||
indent-tabs-mode: nil
|
||
sentence-end-double-space: t
|
||
fill-column: 70
|
||
coding: utf-8
|
||
End:
|