Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 3141 – A Type Hierarchy for Numbers

Author:
Jeffrey Yasskin <jyasskin at google.com>
Status:
Final
Type:
Standards Track
Created:
23-Apr-2007
Python-Version:
3.0
Post-History:
25-Apr-2007, 16-May-2007, 02-Aug-2007

Table of Contents

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". The hierarchy is inspired by Scheme’s numeric tower [2].

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.

classNumber(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.

classComplex(Number):
"""Complex defines the operations that work on the builtin complex type.
 In short, those are: conversion to complex, bool(), .real, .imag,
 +, -, *, /, **, abs(), .conjugate(), ==, and !=.
 If it is given heterogeneous 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
 defreal(self):
"""Retrieve the real component of this number.
 This should subclass Real.
 """
 raise NotImplementedError
 @abstractproperty
 defimag(self):
"""Retrieve the imaginary 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):
"""Coerces self to whatever class defines the method."""
 raise NotImplementedError
 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):
"""a/b; should promote to float or complex when necessary."""
 raise NotImplementedError
 @abstractmethod
 def__rdiv__(self, other):
 raise NotImplementedError
 @abstractmethod
 def__pow__(self, exponent):
"""a**b; should promote to float or 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
 defconjugate(self):
"""(x+y*i).conjugate() returns (x-y*i)."""
 raise NotImplementedError
 @abstractmethod
 def__eq__(self, other):
 raise NotImplementedError
 # __ne__ is inherited from object and negates whatever __eq__ does.

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).

classReal(Complex):
"""To Complex, Real adds the operations that work on real numbers.
 In short, those are: conversion to float, trunc(), math.floor(),
 math.ceil(), round(), divmod(), //, %, <, <=, >, and >=.
 Real also provides defaults for some of the derived operations.
 """
 # XXX What to do about the __int__ implementation that's
 # currently present on float? Get rid of it?
 @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);
 * for any Integral j satisfying the first two conditions,
 abs(i) >= abs(j) [i.e. i has "maximal" abs among those].
 i.e. "truncate towards 0".
 """
 raise NotImplementedError
 @abstractmethod
 def__floor__(self):
"""Finds the greatest Integral <= self."""
 raise NotImplementedError
 @abstractmethod
 def__ceil__(self):
"""Finds the least Integral >= self."""
 raise NotImplementedError
 @abstractmethod
 def__round__(self, ndigits:Integral=None):
"""Rounds self to ndigits decimal places, defaulting to 0.
 If ndigits is omitted or None, returns an Integral,
 otherwise returns a Real, preferably of the same type as
 self. Types may choose which direction to round half. For
 example, float rounds half toward even.
 """
 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):
"""self % other
 See
 https://mail.python.org/pipermail/python-3000/2006-May/001735.html
 and consider using "self/other - trunc(self/other)"
 instead if you're worried about round-off errors.
 """
 raise NotImplementedError
 @abstractmethod
 def__rmod__(self, other):
"""other % self"""
 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
 # __gt__ and __ge__ are automatically done by reversing the arguments.
 # (But __le__ is not computed as the opposite of __gt__!)
 # Concrete implementations of Complex abstract methods.
 # Subclasses may override these, but don't have to.
 def__complex__(self):
 return complex(float(self))
 @property
 defreal(self):
 return +self
 @property
 defimag(self):
 return 0
 defconjugate(self):
"""Conjugate is a no-op for Reals."""
 return +self

We should clean up Demo/classes/Rat.py and promote it into rational.py in the standard library. Then it will implement the Rational ABC.

classRational(Real, Exact):
""".numerator and .denominator should be in lowest terms."""
 @abstractproperty
 defnumerator(self):
 raise NotImplementedError
 @abstractproperty
 defdenominator(self):
 raise NotImplementedError
 # Concrete implementation of Real's conversion to float.
 # (This invokes Integer.__div__().)
 def__float__(self):
 return self.numerator / self.denominator

And finally integers:

classIntegral(Rational):
"""Integral adds a conversion to int and the bit-string operations."""
 @abstractmethod
 def__int__(self):
 raise NotImplementedError
 def__index__(self):
"""__index__() exists because float has __int__()."""
 return int(self)
 def__lshift__(self, other):
 return int(self) << int(other)
 def__rlshift__(self, other):
 return int(other) << int(self)
 def__rshift__(self, other):
 return int(self) >> int(other)
 def__rrshift__(self, other):
 return int(other) >> int(self)
 def__and__(self, other):
 return int(self) & int(other)
 def__rand__(self, other):
 return int(other) & int(self)
 def__xor__(self, other):
 return int(self) ^ int(other)
 def__rxor__(self, other):
 return int(other) ^ int(self)
 def__or__(self, other):
 return int(self) | int(other)
 def__ror__(self, other):
 return int(other) | int(self)
 def__invert__(self):
 return ~int(self)
 # Concrete implementations of Rational and Real abstract methods.
 def__float__(self):
"""float(self) == float(int(self))"""
 return float(int(self))
 @property
 defnumerator(self):
"""Integers are their own numerators."""
 return +self
 @property
 defdenominator(self):
"""Integers have a denominator of 1."""
 return 1

Changes to operations and __magic__ methods

To support more precise narrowing from float to int (and more generally, from Real to Integral), we propose 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), which returns the Integral closest to x, rounding half as the type chooses. float will change in 3.0 to round half toward even. There is also a 2-argument version, __round__(self, ndigits), called from round(x, ndigits), which should return a Real.

In 2.6, math.floor, math.ceil, and round will continue to return floats.

The int() conversion implemented by float is equivalent to trunc(). In general, the int() conversion should try __int__() first and if it is not found, try __trunc__().

complex.__{divmod,mod,floordiv,int,float}__ also go away. It would be nice to provide a nice error message to help confused porters, but not appearing in help(complex) is more important.

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:

classMyFoo(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:

classMyIntegral(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 [1] including MonoidUnderPlus, AdditiveGroup, Ring, and Field, and mentioned several other possible algebraic types before getting to the numbers. We 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 we 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.

The Decimal Type

After consultation with its authors it has been decided that the Decimal type should not at this time be made part of the numeric tower.

References

Acknowledgements

Thanks to Neal 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.


Source: https://github.com/python/peps/blob/main/peps/pep-3141.rst

Last modified: 2025年01月30日 01:21:51 GMT

AltStyle によって変換されたページ (->オリジナル) /