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

Python Enhancement Proposals

PEP 3141 – Eine Typenhierarchie für Zahlen

Autor:
Jeffrey Yasskin <jyasskin at google.com>
Status:
Final
Typ:
Standards Track
Erstellt:
23. April 2007
Python-Version:
3.0
Post-History:
25. April 2007, 16. Mai 2007, 02. August 2007

Inhaltsverzeichnis

Zusammenfassung

Dieser Vorschlag definiert eine Hierarchie von Abstract Base Classes (ABCs) (PEP 3119) zur Darstellung von zahlenähnlichen Klassen. Er schlägt eine Hierarchie von Number :> Complex :> Real :> Rational :> Integral vor, wobei A :> B bedeutet "A ist ein Obertyp von B". Die Hierarchie ist inspiriert von Schemes numerischem Turm [2].

Begründung

Funktionen, die Zahlen als Argumente nehmen, sollten in der Lage sein, die Eigenschaften dieser Zahlen zu bestimmen, und wenn und wann Typerabhängiges Overloading in die Sprache aufgenommen wird, sollten sie basierend auf den Typen der Argumente überladbar sein. Zum Beispiel erfordert Slicing, dass seine Argumente Integrals sind, und die Funktionen im math Modul erfordern, dass ihre Argumente Real sind.

Spezifikation

Dieses PEP spezifiziert eine Reihe von Abstract Base Classes und schlägt eine allgemeine Strategie für die Implementierung einiger Methoden vor. Es verwendet Terminologie aus PEP 3119, aber die Hierarchie soll für jede systematische Methode zur Definition von Klassensets aussagekräftig sein.

Die Typüberprüfungen in der Standardbibliothek sollten diese Klassen anstelle der konkreten Built-ins verwenden.

Numerische Klassen

Wir beginnen mit einer Number-Klasse, um es den Leuten zu erleichtern, vage zu sein, welche Art von Zahl sie erwarten. Diese Klasse hilft nur beim Overloading; sie bietet keine Operationen.

class Number(metaclass=ABCMeta): pass

Die meisten Implementierungen von komplexen Zahlen werden hashbar sein, aber wenn Sie sich darauf verlassen müssen, müssen Sie dies explizit überprüfen: veränderliche Zahlen werden von dieser Hierarchie unterstützt.

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.

Die Real ABC zeigt an, dass der Wert auf der reellen Achse liegt und die Operationen des float Built-ins unterstützt. Reelle Zahlen sind total geordnet, mit Ausnahme von NaNs (die dieses PEP im Grunde ignoriert).

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

Wir sollten Demo/classes/Rat.py bereinigen und es in rational.py in die Standardbibliothek aufnehmen. Dann wird es die Rational ABC implementieren.

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

Und schließlich Ganzzahlen

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

Änderungen an Operationen und __magic__ Methoden

Um eine präzisere Verengung von float auf int (und allgemeiner von Real auf Integral) zu unterstützen, schlagen wir die folgenden neuen __magic__ Methoden vor, die von den entsprechenden Bibliotheksfunktionen aufgerufen werden. Alle diese geben Integrale anstelle von Reellen zurück.

  1. __trunc__(self), aufgerufen von einem neuen Built-in trunc(x), das das Integral zurückgibt, das x am nächsten liegt, zwischen 0 und x.
  2. __floor__(self), aufgerufen von math.floor(x), das das größte Integral zurückgibt, das <= x ist.
  3. __ceil__(self), aufgerufen von math.ceil(x), das das kleinste Integral zurückgibt, das >= x ist.
  4. __round__(self), aufgerufen von round(x), das das Integral zurückgibt, das x am nächsten liegt, wobei die Hälfte so gerundet wird, wie es der Typ wählt. float wird sich in 3.0 ändern, um die Hälfte zu geraden Zahlen zu runden. Es gibt auch eine 2-Argument-Version, __round__(self, ndigits), aufgerufen von round(x, ndigits), die ein Real zurückgeben sollte.

In 2.6 werden math.floor, math.ceil und round weiterhin Floats zurückgeben.

Die von float implementierte int()-Konvertierung ist äquivalent zu trunc(). Im Allgemeinen sollte die int()-Konvertierung zuerst __int__() versuchen, und wenn diese nicht gefunden wird, __trunc__() versuchen.

complex.__{divmod,mod,floordiv,int,float}__ verschwinden ebenfalls. Es wäre wünschenswert, eine nette Fehlermeldung bereitzustellen, um verwirrte Portierer zu unterstützen, aber das Nicht-Auftauchen in help(complex) ist wichtiger.

Hinweise für Typenimplementierer

Implementierer sollten vorsichtig sein, gleiche Zahlen gleich zu machen und sie zu denselben Werten zu hashen. Dies kann subtil sein, wenn es zwei verschiedene Erweiterungen der reellen Zahlen gibt. Zum Beispiel könnte ein komplexer Typ hash() vernünftigerweise wie folgt implementieren

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

aber sollte vorsichtig sein mit Werten, die außerhalb des Bereichs oder der Präzision des integrierten komplexen Typs liegen.

Hinzufügen weiterer numerischer ABCs

Es gibt natürlich mehr mögliche ABCs für Zahlen, und dies wäre eine schlechte Hierarchie, wenn sie die Möglichkeit ausschließen würde, diese hinzuzufügen. Sie können MyFoo zwischen Complex und Real hinzufügen mit

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

Implementierung der arithmetischen Operationen

Wir möchten die arithmetischen Operationen so implementieren, dass gemischte Operationen entweder eine Implementierung aufrufen, deren Autor die Typen beider Argumente kannte, oder beide in den nächstgelegenen integrierten Typ konvertieren und die Operation dort ausführen. Für Untertypen von Integral bedeutet dies, dass __add__ und __radd__ wie folgt definiert werden sollten

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

Es gibt 5 verschiedene Fälle für eine gemischte Typoperation auf Unterklassen von Complex. Ich werde mich auf den obigen Code beziehen, der sich nicht auf MyIntegral und OtherTypeIKnowAbout bezieht, als "Boilerplate". a ist eine Instanz von A, die ein Untertyp von Complex ist (a : A <: Complex), und b : B <: Complex. Ich werde a + b betrachten

  1. Wenn A ein __add__ definiert, das b akzeptiert, ist alles in Ordnung.
  2. Wenn A auf den Boilerplate-Code zurückfällt und einen Wert von __add__ zurückgeben würde, würden wir die Möglichkeit verpassen, dass B ein intelligenteres __radd__ definiert, daher sollte der Boilerplate-Code NotImplemented von __add__ zurückgeben. (Oder A implementiert __add__ gar nicht.)
  3. Dann bekommt B's __radd__ eine Chance. Wenn es a akzeptiert, ist alles in Ordnung.
  4. Wenn es auf den Boilerplate-Code zurückfällt, gibt es keine weiteren möglichen Methoden mehr, also ist dies der Ort, an dem die Standardimplementierung leben sollte.
  5. Wenn B <: A, versucht Python B.__radd__ vor A.__add__. Das ist in Ordnung, da es mit Wissen über A implementiert wurde, sodass es diese Instanzen handhaben kann, bevor es an Complex delegiert.

Wenn A<:Complex und B<:Real sind, ohne weiteres Wissen zu teilen, dann ist die entsprechende gemeinsame Operation diejenige, die den integrierten komplexen Typ involviert, und beide __radd__s landen dort, also a+b == b+a.

Abgelehnte Alternativen

Die erste Version dieses PEP definierte eine algebraische Hierarchie, inspiriert von einem Haskell Numeric Prelude [1], einschließlich MonoidUnderPlus, AdditiveGroup, Ring und Field, und erwähnte mehrere andere mögliche algebraische Typen, bevor sie zu den Zahlen kam. Wir hatten erwartet, dass dies für Leute nützlich sein würde, die Vektoren und Matrizen verwenden, aber die NumPy-Community war wirklich nicht interessiert, und wir stießen auf das Problem, dass selbst wenn x eine Instanz von X <: MonoidUnderPlus und y eine Instanz von Y <: MonoidUnderPlus ist, x + y möglicherweise immer noch keinen Sinn ergibt.

Dann gaben wir den Zahlen eine viel stärker verzweigte Struktur, um Dinge wie die Gaußschen ganzen Zahlen und Z/nZ einzuschließen, die komplex sein könnten, aber nicht unbedingt Dinge wie Division unterstützen würden. Die Community entschied, dass dies zu viel Komplexität für Python wäre, also habe ich den Vorschlag nun zurückgeschraubt, um dem numerischen Turm von Scheme viel näher zu kommen.

Der Decimal-Typ

Nach Rücksprache mit seinen Autoren wurde entschieden, dass der Decimal-Typ zu diesem Zeitpunkt nicht Teil des numerischen Turms werden sollte.

Referenzen

Danksagungen

Dank an Neal Norwitz, der mich überhaupt erst ermutigt hat, dieses PEP zu schreiben, an Travis Oliphant, der darauf hingewiesen hat, dass sich die NumPy-Leute nicht wirklich für die algebraischen Konzepte interessierten, an Alan Isaac, der mich daran erinnerte, dass Scheme dies bereits getan hatte, und an Guido van Rossum und viele andere auf der Mailingliste für die Verfeinerung des Konzepts.


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

Zuletzt geändert: 2025-01-30 01:21:51 GMT