[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