[Python-3000] Updated and simplified PEP 3141: A Type Hierarchy for Numbers
Guido van Rossum
guido at python.org
Wed Aug 22 21:57:32 CEST 2007
On 8/22/07, Jeffrey Yasskin <jyasskin at gmail.com> wrote:
> 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?
Yes, I think so. That's what the built-in types do (in case the object
is an instance of a subclass). It makes sense because all other
operators do this too (unless overridden).
> * Add Demo/classes/Rat.py to the stdlib?
Yes, but it needs a makeover. At the very least I'd propose the module
name to be rational.
The code is really old.
> * How many of __trunc__, __floor__, __ceil__, and __round__ should be
> magic methods?
I'm okay with all of these.
> For __round__, when do we want to return an Integral?
When the second argument is absent only.
> [__properfraction__ is probably subsumed by divmod(x, 1).]
Probably, but see PEP 3100, which still lists __mod__ and __divmod__
as to be deleted.
> * How to give the removed methods (divmod, etc. on complex) good error
> messages without having them show up in help(complex)?
If Complex doesn't define them, they'll be TypeErrors, and that's good
enough IMO.
> 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
> _______________________________________________
> Python-3000 mailing list
> Python-3000 at python.org
> http://mail.python.org/mailman/listinfo/python-3000
> Unsubscribe: http://mail.python.org/mailman/options/python-3000/guido%40python.org
>
--
--Guido van Rossum (home page: http://www.python.org/~guido/)
More information about the Python-3000
mailing list