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

Jeffrey Yasskin jyasskin at gmail.com
Wed Aug 22 21:36:31 CEST 2007


There are still some open issues here that need answers:

* Should __pos__ coerce the argument to be an instance of the type
it's defined on?
* Add Demo/classes/Rat.py to the stdlib?
* How many of __trunc__, __floor__, __ceil__, and __round__ should be
magic methods? For __round__, when do we want to return an Integral?
[__properfraction__ is probably subsumed by divmod(x, 1).]
* How to give the removed methods (divmod, etc. on complex) good error
messages without having them show up in help(complex)?

I'll look into this during the sprint.

On 8/2/07, Jeffrey Yasskin <jyasskin at gmail.com> wrote:
> After some more discussion, I have another version of the PEP with a
> draft, partial implementation. Let me know what you think.
>
>
>
> PEP: 3141
> Title: A Type Hierarchy for Numbers
> Version: $Revision: 56646 $
> Last-Modified: $Date: 2007-08-01 10:11:55 -0700 (Wed, 01 Aug 2007) $
> Author: Jeffrey Yasskin <jyasskin at 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. **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).
>             """
>             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:
>
>


-- 
Namasté,
Jeffrey Yasskin
http://jeffrey.yasskin.info/

"Religion is an improper response to the Divine." — "Skinny Legs and
All", by Tom Robbins


More information about the Python-3000 mailing list