9.1. numbers — Classes de base abstraites numériques

Code source : Lib/numbers.py


Le module numbers (PEP 3141) définit une hiérarchie de classes de base abstraites numériques qui définissent progressivement plus d’opérations. Aucun des types définis dans ce module ne peut être instancié.

class numbers.Number

La base de la hiérarchie numérique. Si vous voulez juste vérifier qu’un argument x est un nombre, peu importe le type, utilisez isinstance(x, Number).

9.1.1. La tour numérique

class numbers.Complex

Les sous-classes de ce type décrivent des nombres complexes et incluent les opérations qui fonctionnent sur le type natif complex. Ce sont : les conversions vers complex et bool, real, imag, +, -, *, /, abs(), conjugate(), == et !=. Toutes sauf - et != sont abstraites.

real

Abstrait. Récupère la partie réelle de ce nombre.

imag

Abstrait. Retrouve la partie imaginaire de ce nombre.

abstractmethod conjugate()

Abstrait. Renvoie le complexe conjugué. Par exemple, (1+3j).conjugate() == (1-3j).

class numbers.Real

Real ajoute les opérations qui fonctionnent sur les nombres réels à Complex.

En bref, celles-ci sont : une conversion vers float, math.trunc(), round(), math.floor(), math.ceil(), divmod(), //, %, <, <=, > et >=.

Real fournit également des valeurs par défaut pour complex(), real, imag et conjugate().

class numbers.Rational

Dérive Real et ajoute les propriétés numerator et denominator qui doivent être les plus petits possible. Avec celles-ci, il fournit une valeur par défaut pour float().

numerator

Abstrait.

denominator

Abstrait.

class numbers.Integral

Dérive Rational et ajoute une conversion en int. Fournit des valeurs par défaut pour float(), numerator et denominator. Ajoute des méthodes abstraites pour ** et les opérations au niveau des bits: <<, >>, &, ^, |, ~.

9.1.2. Notes pour implémenter des types

Les développeurs doivent veiller à ce que des nombres égaux soient bien égaux lors de comparaisons et à ce qu’ils soient hachés aux mêmes valeurs. Cela peut être subtil s’il y a deux dérivations différentes des nombres réels. Par exemple, fractions.Fraction implémente hash() comme suit :

def __hash__(self):
    if self.denominator == 1:
        # Get integers right.
        return hash(self.numerator)
    # Expensive check, but definitely correct.
    if self == float(self):
        return hash(float(self))
    else:
        # Use tuple's hash to avoid a high collision rate on
        # simple fractions.
        return hash((self.numerator, self.denominator))

9.1.2.1. Ajouter plus d’ABC numériques

Il est bien entendu possible de créer davantage d’ABC pour les nombres et cette hiérarchie serait médiocre si elle excluait la possibilité d’en ajouter. Vous pouvez ajouter MyFoo entre Complex et Real ainsi :

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

9.1.2.2. Implémentation des opérations arithmétiques

Nous voulons implémenter les opérations arithmétiques de sorte que les opérations en mode mixte appellent une implémentation dont l’auteur connaît les types des deux arguments, ou convertissent chacun dans le type natif le plus proche et effectuent l’opération sur ces types. Pour les sous-types de Integral, cela signifie que __add__() et __radd__() devraient être définis comme suit :

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

Il existe 5 cas différents pour une opération de type mixte sur des sous-classes de Complex. Nous nous référerons à tout le code ci-dessus qui ne se réfère pas à MyIntegral et OtherTypeIKnowAbout comme « expression générique ». a est une instance de A, qui est un sous-type de Complex (a : A <: Complex) et b : B <: Complex. Considérons a + b:

  1. Si A définit une __add__() qui accepte b, tout va bien.

  2. Si A fait appel au code générique et que celui-ci renvoie une valeur de __add__(), nous manquons la possibilité que B définisse une __radd__() plus intelligent, donc le code générique devrait retourner NotImplemented dans __add__() (ou alors A ne doit pas implémenter __add__() du tout.)

  3. Alors __radd__() de B a une chance. si elle accepte a, tout va bien.

  4. Si elle fait appel au code générique, il n’y a plus de méthode possible à essayer, c’est donc ici que l’implémentation par défaut intervient.

  5. Si B < : A`, Python essaie B.__radd__ avant A.__add__. C’est valable parce qu’elle est implémentée avec la connaissance de A, donc elle peut gérer ces instances avant de déléguer à Complex.

Si A <: Complex et B <: Real sans autre information, alors l’opération commune appropriée est celle impliquant complex et les deux __radd__() atterrissent à cet endroit, donc a+b == b+a.

Comme la plupart des opérations sur un type donné seront très similaires, il peut être utile de définir une fonction accessoire qui génère les instances résultantes et inverses d’un opérateur donné. Par exemple, fractions.Fraction utilise :

def _operator_fallbacks(monomorphic_operator, fallback_operator):
    def forward(a, b):
        if isinstance(b, (int, Fraction)):
            return monomorphic_operator(a, b)
        elif isinstance(b, float):
            return fallback_operator(float(a), b)
        elif isinstance(b, complex):
            return fallback_operator(complex(a), b)
        else:
            return NotImplemented
    forward.__name__ = '__' + fallback_operator.__name__ + '__'
    forward.__doc__ = monomorphic_operator.__doc__

    def reverse(b, a):
        if isinstance(a, Rational):
            # Includes ints.
            return monomorphic_operator(a, b)
        elif isinstance(a, numbers.Real):
            return fallback_operator(float(a), float(b))
        elif isinstance(a, numbers.Complex):
            return fallback_operator(complex(a), complex(b))
        else:
            return NotImplemented
    reverse.__name__ = '__r' + fallback_operator.__name__ + '__'
    reverse.__doc__ = monomorphic_operator.__doc__

    return forward, reverse

def _add(a, b):
    """a + b"""
    return Fraction(a.numerator * b.denominator +
                    b.numerator * a.denominator,
                    a.denominator * b.denominator)

__add__, __radd__ = _operator_fallbacks(_add, operator.add)

# ...