[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