PEP: 3141 Title: A Type Hierarchy for Numbers Version: $Revision$ Last-Modified: $Date$ Author: Jeffrey Yasskin 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. **Open issue:** Should __pos__ coerce the argument to be an instance of the type it's defined on? Why do the builtins do this? :: 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): return self 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 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): raise NotImplementedError @abstractmethod def __rmod__(self, other): 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 There is no built-in rational type, but it's straightforward to write, so we provide an ABC for it. **Open issue**: Add Demo/classes/Rat.py to the stdlib? :: 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)``, with returns the Integral closest to ``x``, rounding half toward even. **Open issue:** We could support the 2-argument version, but then we'd only return an Integral if the second argument were ``<= 0``. 5. ``__properfraction__(self)``, called from a new function, ``math.properfraction(x)``, which resembles C's ``modf()``: returns a pair ``(n:Integral, r:Real)`` where ``x == n + r``, both ``n`` and ``r`` have the same sign as ``x``, and ``abs(r) < 1``. **Open issue:** Oh, we already have ``math.modf``. What name do we want for this? Should we use divmod(x, 1) instead? 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}__`` should also go away. These should continue to raise ``TypeError`` to help confused porters, but should not appear in ``help(complex)`` to avoid confusing more people. **Open issue:** This is difficult to do with the ``PyNumberMethods`` struct. What's the best way to accomplish it? 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: