The hierarchical structure of PEP 3141 numerical type -- Python official document translation [original]

Keywords: Python Google Mac

PEP 3141 -- A Type Hierarchy for Numbers

Original English: https://www.python.org/dev/peps/pep-3141
Collection date: February 27, 2020

PEP: 3141
Title: A Type Hierarchy for Numbers
Author: Jeffrey Yasskin jyasskin@google.com
Status: Final
Type: Standards Track
Created: 23-Apr-2007
Post-History: 25-Apr-2007, 16-May-2007, 02-Aug-2007

Catalog

Abstract

This proposal defines ABC (Abstract Base Class, PEP 3119 )The hierarchy of. Here, the number: > complex: > Real: > rational: > integration level is proposed. A: > b means "a is the super type of B". The establishment of this hierarchy has been Scheme value type tower Inspiration.

Reason (rational)

Functions with values as parameters should be able to determine the properties of these values, and when type based overloads are added to the language, function overloads should be implemented according to parameter types. For example, slice operations require an Integral parameter, while functions in the math module require a Real parameter.

Specification

In this paper, we define a set of abstract base classes, and give some common implementation methods. It's used here PEP 3119 But this hierarchy makes sense for any systematic solution for class definition.

The type checking process in a standard library should use these classes instead of concrete built-in types.

Numeric classes

Let's start with a Number class, so that we can blur the type of the Number first. This class is only for the convenience of overloading and does not support any operation.

    class Number(metaclass=ABCMeta): pass

Most implementation classes of complex numbers are hashable, but if they are to be absolutely reliable, they must be checked explicitly to verify that the value type hierarchy supports mutable values.

    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 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):
            """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 abstract base class of Real indicates that the value is in the Real position in the hierarchy and supports all operations of built-in float type. In addition to NaN, the Real numbers are completely ordered.

    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

Demo/classes/Rat.py should be removed and upgraded to rational.py in the standard library. In this way, Rational, an abstract base class of Rational numbers, can be implemented.

    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

Finally, integer type:

    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) & 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
        def numerator(self):
            """Integers are their own numerators."""
            return +self

        @property
        def denominator(self):
            """Integers have a denominator of 1."""
            return 1

Changes to operations and magic methods

In order to support more subtle differences between float and int (Real and Integral), here are some new magic methods for the corresponding library function calls. All of these methods return Integral instead of Real.

  1. __TRUNC (self), called by the new built-in method trunc(x), returns the integer nearest to X between 0 and X.

  2. __Floor (self), called by math.floor(x), returns the maximum integer of < = X.

  3. __Ceil (self), called by math.ceil(x), returns the maximum integer > = X.

  4. __Round (self), called by round(x), returns the nearest integer to x, and half of the rounding will depend on the data type. In version 3.0, float will be changed to half even rounding. There is also a two parameter version of round (self, ndigits) called by round(x, ndigits), which will return real numbers.

In version 2.6, math.floor, math.ceil, and round will still return floating-point numbers.

The int() transformation implemented by float is equivalent to trunc(). Normally, an int() conversion should try \\\\\\\\\\.

Complex. {divmod, mod, floordiv, int, float} also disappeared. It's perfect to provide a good error message, but more importantly, don't show up in help(complex).

Notes for type implementers

When implementing, you should make sure that equal values are indeed equal and hash them to the same value. If there are two different extension implementations of real numbers, it may be a little tricky. For example, it is reasonable to implement hash() as follows:

        def __hash__(self):
            return hash(complex(self))

But care should be taken with values that are outside the range or precision of the built-in complex.

Adding More Numeric ABCs

Of course, numeric types may also have more abstract base classes. If you do not consider the ability to add these classes, the hierarchy of numeric types will be very poor. For example, the following MyFoo can be added between Complex and Real:

    class MyFoo(Complex): ...
    MyFoo.register(Real)

Implementing the arithmetic operations

In the mixed operation, either call the implementation with known two parameter types, or convert both parameters to the closest built-in type before performing the operation, which is the arithmetic operation that should be implemented. For subtypes of integers, this means that add and radd should be defined as follows:

    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

For the subclass of Complex number, there are five different cases of mixed operation. All of the above code that does not refer to MyIntegral and OtherTypeIKnowAbout is used as a boilerplate. A will be an instance of a, and a is a subtype of Complex (A: a <: Complex). Similarly, B: B <: Complex. Then a + b will be treated as follows:

  1. If A defines an add method that accepts b, all is well.
  2. If A is downgraded to adopt template code and the result value is returned by add, even if B defines A more sensible radd, it will be ignored, so the template code should return the NotImplemented result from add. (or A may not implement add at all.
  3. Then it's B's radd. If you can accept a, everything will be fine.
  4. If B is demoted to adopt boilerplate code, because there is no other way to try, the default implementation code will be adopted.
  5. If B <: A, Python will try to call B. Radd before A. add. There is no problem with this approach, because B's approach is implemented with an understanding of a, so it can handle these instances before passing them to Complex.

If a <: Complex and B <: real no longer share other information, it is reasonable to share the related operation methods of the built-in Complex type. The radd s of both will fall into Complex, so a+b == b+a.

Rejected Alternatives

Prior to the formation of Number, the original version of this PEP defined a kind of Haskell Numeric Prelude Inspired value type hierarchy, including monoid underplus, AdditiveGroup, Ring, Field, and several other value types mentioned earlier. It was hoped that these would be useful for people who use vectors and matrices, but the NumPy community is not interested in this, and there is also a problem. Even if x is an instance of x <: monoid underplus, y is also an instance of Y <: monoid underplus, x + y may not be meaningful.

So later, Number added more branches, including values such as Gaussian Integer and Z/nZ. They may belong to Complex but do not have to support operations such as division. The community believes that this approach is too Complex for Python, so the proposal is now smaller and closer to Scheme value type tower.

The decision type

After consultation with the author, it was decided that the Decimal type should not be added to the numerical type tower at present.

References

Introduction to abstract base class (http://www.python.org/dev/peps/pep-3119/)

Possible Python 3K class tree? Wiki by Bill Janssen (http://wiki.python.org/moin/AbstractBaseClasses)

NumericPrelude: an experimental alternative to the hierarchical relationship of numerical classes (http://darcs.haskell.org/numericprelude/docs/html/index.html)

Scheme numeric type tower (https://groups.csail.mit.edu/mac/ftpdir/scheme-reports/r5rs-html/r5rs_.htmlą·¸sec50)

Acknowledgments

Thanks to Neal Norwitz for encouraging me to write this PEP at the first time. Thanks to Travis Oliphant for pointing out that the concept of Numpy user logarithm really doesn't care. Thanks to Alan Isaac for reminding me that Scheme has completed the construction of the relevant system of this article. Thanks to Guido van Rossum and many people in the mailing list for helping me improve the concept.

Copyright

This article has been published in the public domain.

Posted by eldee on Wed, 04 Mar 2020 20:45:17 -0800