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
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.
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.
class Complex(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
def real(self):
"""Retrieve the real component of this number.
This should subclass Real.
"""
raise NotImplementedError
@abstractproperty
def imag(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
def conjugate(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).
class Real(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
def real(self):
return +self
@property
def imag(self):
return 0
def conjugate(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.
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.
# (This invokes Integer.__div__().)
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):
"""__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) &