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 <[EMAIL PROTECTED]> 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 <[EMAIL PROTECTED]> > 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@python.org http://mail.python.org/mailman/listinfo/python-3000 Unsubscribe: http://mail.python.org/mailman/options/python-3000/archive%40mail-archive.com