python-peps/pep-3141.txt

407 lines
14 KiB
Plaintext
Raw Normal View History

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
2007-07-31 15:19:57 -04:00
Post-History: 25-Apr-2007, 16-May-2007, xx-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
2007-07-31 15:19:57 -04:00
``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
2007-07-31 15:19:57 -04:00
be ``Integrals``, and the functions in the ``math`` module require
their arguments to be ``Real``.
Specification
=============
2007-07-31 15:19:57 -04:00
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.
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
2007-07-31 15:19:57 -04:00
overloading; it doesn't provide any operations.
class Number(metaclass=MetaABC): pass
Some types (primarily ``float``) define "Not a Number" (NaN) values
that return false for any comparison, including equality with
2007-07-31 15:19:57 -04:00
themselves, and are maintained through operations. That is, ``nan + x
-> nan`` and ``nan == nan -> False`` 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 UndefinedNumber(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
...
2007-07-31 15:19:57 -04:00
# Should we demand a conversion to float?
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 !=.
2007-07-31 15:19:57 -04:00
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):
2007-07-31 15:19:57 -04:00
"""Return a builtin complex instance."""
@abstractmethod
@property
def real(self):
2007-07-31 15:19:57 -04:00
"""Retrieve the real component of this number, which should subclass Real."""
raise NotImplementedError
@abstractmethod
@property
def imag(self):
2007-07-31 15:19:57 -04:00
"""Retrieve the real component of this number, which should subclass Real."""
raise NotImplementedError
2007-07-31 15:19:57 -04:00
@abstractmethod
def __add__(self, other):
raise NotImplementedError
@abstractmethod
def __sub__(self, other):
raise NotImplementedError
@abstractmethod
def __neg__(self):
2007-07-31 15:19:57 -04:00
raise NotImplementedError
@abstractmethod
def __mul__(self, other):
raise NotImplementedError
@abstractmethod
def __div__(self, other):
raise NotImplementedError
2007-07-31 15:19:57 -04:00
@abstractmethod
def __abs__(self):
2007-07-31 15:19:57 -04:00
"""Returns the Real distance from 0."""
raise NotImplementedError
2007-07-31 15:19:57 -04:00
@abstractmethod
def conjugate(self):
2007-07-31 15:19:57 -04:00
"""(x+y*i).conjugate() returns (x-y*i)."""
raise NotImplementedError
2007-07-31 15:19:57 -04:00
@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
2007-07-31 15:19:57 -04:00
totally ordered. (NaNs were handled above).::
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 >=.
2007-07-31 15:19:57 -04:00
Real also provides defaults for the derived operations.
"""
@abstractmethod
def __float__(self):
"""Any Real can be converted to a native float object."""
2007-07-31 15:19:57 -04:00
raise NotImplementedError
@abstractmethod
def __trunc__(self):
2007-07-31 15:19:57 -04:00
"""Returns an Integral of the same sign as self whose abs is <= self's abs."""
raise NotImplementedError
2007-07-31 15:19:57 -04:00
def __divmod__(self, other):
"""The pair (self // other, self % other)."""
return (self // other, self % other)
@abstractmethod
def __floordiv__(self, other):
"""The floor() of self/other."""
raise NotImplementedError
@abstractmethod
def __mod__(self, other):
"""."""
raise NotImplementedError
2007-07-31 15:19:57 -04:00
@abstractmethod
def __lt__(self, other):
2007-07-31 15:19:57 -04:00
raise NotImplementedError
def __le__(self, other):
# Assume that if other is Real, it defines an ordering
# consistent with this class, or returns NotImplemented.
if isinstance(other, Real):
2007-07-31 15:19:57 -04:00
return not (other < self)
# 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
There is no built-in rational type, but it's straightforward to write,
2007-07-31 15:19:57 -04:00
so we provide an ABC for it. *Open issue*: Add Demo/classes/Rat.py to
the stdlib?::
class Rational(Real, Exact):
2007-07-31 15:19:57 -04:00
""".numerator and .denominator should be in lowest terms."""
@abstractmethod
@property
def numerator(self):
raise NotImplementedError
@abstractmethod
@property
def denominator(self):
raise NotImplementedError
2007-07-31 15:19:57 -04:00
# Concrete implementation of Real's conversion to float.
def __float__(self):
return self.numerator / self.denominator
2007-07-31 15:19:57 -04:00
And finally integers::
class Integral(Rational):
"""Integral adds a conversion to int and the bit-string operations."""
@abstractmethod
def __int__(self):
raise NotImplementedError
@abstractmethod
def __lshift__(self, other):
raise NotImplementedError
@abstractmethod
def __rshift__(self, other):
raise NotImplementedError
@abstractmethod
def __and__(self, other):
raise NotImplementedError
@abstractmethod
def __xor__(self, other):
raise NotImplementedError
@abstractmethod
def __or__(self, other):
raise NotImplementedError
# Concrete implementations of Rational and Real abstract methods.
def __float__(self):
return float(int(self))
@property
2007-07-31 15:19:57 -04:00
def numerator(self):
return self
@property
2007-07-31 15:19:57 -04:00
def denominator(self):
return 1
2007-07-31 15:19:57 -04:00
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
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):
2007-07-31 15:19:57 -04:00
return hash(complex(self))
2007-07-31 15:19:57 -04:00
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)
2007-07-31 15:19:57 -04:00
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
2007-07-31 15:19:57 -04:00
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/)
2007-07-31 15:19:57 -04:00
.. [#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
2007-07-31 15:19:57 -04:00
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: