Opérateurs

Il est maintenant temps de nous intéresser aux opérateurs du langage Python (+, -, *, etc.). En effet, un code respectant la philosophie du langage se doit de les utiliser à bon escient.

Ils sont une manière claire de représenter des opérations élémentaires (addition, concaténation, …) entre deux objets. a + b est en effet plus lisible qu’un add(a, b) ou encore a.add(b).

Ce chapitre a pour but de vous présenter les mécanismes mis en jeu par ces différents opérateurs, et la manière de les implémenter.

Des méthodes un peu spéciales

Nous avons vu précédemment la méthode __init__, permettant d’initialiser les attributs d’un objet. On appelle cette méthode une méthode spéciale, il y en a encore beaucoup d’autres en Python. Elles sont reconnaissables par leur nom débutant et finissant par deux underscores.

Vous vous êtes peut-être déjà demandé d’où provenait le résultat affiché sur la console quand on entre simplement le nom d’un objet.

1
2
3
>>> john = User(1, 'john', '12345')
>>> john
<__main__.User object at 0x7fefd77fae10>

Il s’agit en fait de la représentation d’un objet, calculée à partir de sa méthode spéciale __repr__.

1
2
>>> john.__repr__()
'<__main__.User object at 0x7fefd77fae10>'

À noter qu’une méthode spéciale n’est presque jamais directement appelée en Python, on lui préférera dans le cas présent la fonction builtin repr.

1
2
>>> repr(john)
'<__main__.User object at 0x7fefd77fae10>'

Il nous suffit alors de redéfinir cette méthode __repr__ pour bénéficier de notre propre représentation.

1
2
3
4
5
class User:
    ...

    def __repr__(self):
        return '<User: {}, {}>'.format(self.id, self.name)
1
2
>>> User(1, 'john', '12345')
<User: 1, john>

Une autre opération courante est la conversion de notre objet en chaîne de caractères afin d’être affiché via print par exemple. Par défaut, la conversion en chaîne correspond à la représentation de l’objet, mais elle peut être surchargée par la méthode __str__.

1
2
3
4
5
6
7
8
class User:
    ...

    def __repr__(self):
        return '<User: {}, {}>'.format(self.id, self.name)

    def __str__(self):
        return '{}-{}'.format(self.id, self.name)
1
2
3
4
5
6
7
8
9
>>> john = User(1, 'john', 12345)
>>> john
<User: 1, john>
>>> repr(john)
'<User: 1, john>'
>>> str(john)
'1-john'
>>> print(john)
1-john

Doux opérateurs

Les opérateurs sont un autre type de méthodes spéciales que nous découvrirons dans cette section.

En effet, les opérateurs ne sont rien d’autres en Python que des fonctions, qui s’appliquent sur leurs opérandes. On peut s’en rendre compte à l’aide du module operator, qui répertorie les fonctions associées à chaque opérateur.

1
2
3
4
5
>>> import operator
>>> operator.add(5, 6)
11
>>> operator.mul(2, 3)
6

Ainsi, chacun des opérateurs correspondra à une méthode de l’opérande de gauche, qui recevra en paramètre l’opérande de droite.

Opérateurs arithmétiques

L’addition, par exemple, est définie par la méthode __add__.

1
2
3
4
5
6
>>> class A:
...     def __add__(self, other):
...         return other # on considère self comme 0
...
>>> A() + 5
5

Assez simple, n’est-il pas ? Mais nous n’avons pas tout à fait terminé. Si la méthode est appelée sur l’opérande de gauche, que se passe-t-il quand notre objet se trouve à droite ?

1
2
3
4
>>> 5 + A()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'A'

Nous ne supportons pas cette opération. En effet, l’expression fait appel à la méthode int.__add__ qui ne connaît pas les objets de type A. Heureusement, ce cas a été prévu et il existe une fonction inverse, __radd__, appelée si la première opération n’était pas supportée.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> class A:
...     def __add__(self, other):
...         return other
...     def __radd__(self, other):
...         return other
...
>>> A() + 5
5
>>> 5 + A()
5

Il faut bien noter que A.__radd__ ne sera appelée que si int.__add__ a échoué.

Les autres opérateurs arithmétques binaires auront un comportement similaire, voici une liste des méthodes à implémenter pour chacun d’eux :

  • Addition/Concaténation (a + b) — __add__, __radd__
  • Soustraction/Différence (a - b) — __sub__, __rsub__
  • Multiplication (a * b) — __mul__, __rmul__
  • Division (a / b) — __truediv__, __rtruediv__
  • Division entière (a // b) — __floordiv__, __rfloordiv__
  • Modulo/Formattage (a % b) — __mod__, __rmod__
  • Exponentiation (a ** b) — __pow__, __rpow__

On remarque aussi que chacun de ces opérateurs arithmétiques possède une version simplifiée pour l’assignation (a += b) qui correspond à la méthode __iadd__. Par défaut, les méthodes __add__/__radd__ sont appelées, mais définir __iadd__ permet d’avoir un comportement différent dans le cas d’un opérateur d’assignation, par exemple sur les listes :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> l = [1, 2, 3]
>>> l2 = l
>>> l2 = l2 + [4]
>>> l2
[1, 2, 3, 4]
>>> l
[1, 2, 3]
>>> l2 = l
>>> l2 += [4]
>>> l2
[1, 2, 3, 4]
>>> l
[1, 2, 3, 4]

Opérateurs arithmétiques unaires

Voici pour les opérateurs binaires, voyons maintenant les opérateurs unaires, qui ne prennent donc pas d’autre paramètre que self.

  • Opposé (-a) — __neg__
  • Positif (+a) - __pos__
  • Valeur abosule (abs(a)) — __abs__

Opérateurs de comparaison

De la même manière que pour les opérateurs arithmétiques, nous avons une méthode spéciale par opérateur de comparaison. Ces opérateurs s’appliqueront sur l’opérande gauche en recevant le droit en paramètre. Ils devront retourner un booléen.

Contrairement aux opérateurs arithmétiques, il n’est pas nécessaire d’avoir deux versions pour chaque opérateur puisque Python saura directement quelle opération inverse tester si la première a échoué (a == b est équivalent à b == a, a < b à b > a, etc.).

  • Égalité (a == b) — __eq__
  • Différence (a != b) — __neq__
  • Stricte infériorité (a < b) — __lt__
  • Infériorité (a <= b) — __le__
  • Stricte supériorité (a > b) — __gt__
  • Supériorité (a >= b) — __ge__

On notera aussi que beaucoup de ces opérateurs peuvent s’inférer les uns les autres. Par exemple, il suffit de savoir calculer a == b et a < b pour définir toutes les autres opérations. Ainsi, Python dispose d’un décorateur, total_ordering du module functools, pour automatiquement générer les opérations manquantes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
>>> from functools import total_ordering
>>> @total_ordering
... class Inferior:
...     def __eq__(self, other):
...         return False
...     def __lt__(self, other):
...         return True
...
>>> i = Inferior()
>>> i == 5
False
>>> i > 5
False
>>> i < 5
True
>>> i <= 5
True
>>> i != 5
True

Autres opérateurs

Nous avons ici étudié les principaux opérateurs du langage. Ces listes ne sont pas exhaustives et présentent juste la méthodologie à suivre.

Pour une liste complète, je vous invite à consulter la documentation du module operator : https://docs.python.org/3/library/operator.html.

TP : Arithmétique simple

Oublions temporairement nos utilisateurs et notre forum, et intéressons-nous à l’évaluation mathématique.

Imaginons que nous voulions représenter une expression mathématique, qui pourrait contenir des termes variables (par exemple, 2 * (-x + 1)).

Il va nous falloir utiliser un type pour représenter cette variable x, appelé Var, et un second pour l’expression non évaluée, Expr. Les Var étant un type particulier d’expressions.

Nous aurons deux autres types d’expressions : les opérations arithmétiques unaires (+, -) et binaires (+, -, *, /, //, %, **). Vous pouvez vous appuyer un même type pour ces deux types d’opérations.

L’expression précédente s’évaluerait par exemple à :

1
BinOp(operator.mul, 2, BinOp(operator.add, UnOp(operator.neg, Var('x')), 1))

Nous ajouterons à notre type Expr une méthode compute(**values), qui permettra de calculer l’expression suivant une valeur donnée, de façon à ce que Var('x').compute(x=5) retourne 5.

Enfin, nous pourrons ajouter une méthode __repr__ pour obtenir une représentation lisible de notre expression.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
import operator

def compute(expr, **values):
    if not isinstance(expr, Expr):
        return expr
    return expr.compute(**values)

class Expr:
    def compute(self, **values):
        raise NotImplementedError

    def __pos__(self):
        return UnOp(operator.pos, self, '+')

    def __neg__(self):
        return UnOp(operator.neg, self, '-')

    def __add__(self, rhs):
        return BinOp(operator.add, self, rhs, '+')

    def __radd__(self, lhs):
        return BinOp(operator.add, lhs, self, '+')

    def __sub__(self, rhs):
        return BinOp(operator.sub, self, rhs, '-')

    def __rsub__(self, lhs):
        return BinOp(operator.sub, lhs, self, '-')

    def __mul__(self, rhs):
        return BinOp(operator.mul, self, rhs, '*')

    def __rmul__(self, lhs):
        return BinOp(operator.mul, lhs, self, '*')

    def __truediv__(self, rhs):
        return BinOp(operator.truediv, self, rhs, '/')

    def __rtruediv__(self, lhs):
        return BinOp(operator.truediv, lhs, self, '/')

    def __floordiv__(self, rhs):
        return BinOp(operator.floordiv, self, rhs, '//')

    def __rfloordiv__(self, lhs):
        return BinOp(operator.floordiv, lhs, self, '//')

    def __mod__(self, rhs):
        return BinOp(operator.mod, self, rhs, '*')

    def __rmod__(self, lhs):
        return BinOp(operator.mod, lhs, self, '*')

class Var(Expr):
    def __init__(self, name):
        self.name = name

    def compute(self, **values):
        if self.name in values:
            return values[self.name]
        return self

    def __repr__(self):
        return self.name

class Op(Expr):
    def __init__(self, op, *args):
        self.op = op
        self.args = args

    def compute(self, **values):
        args = [compute(arg, **values) for arg in self.args]
        return self.op(*args)

class UnOp(Op):
    def __init__(self, op, expr, symbol=None):
        super().__init__(op, expr)
        self.symbol = symbol

    def __repr__(self):
        if self.symbol is None:
            return super().__repr__()
        return '{}{!r}'.format(self.symbol, self.args[0])

class BinOp(Op):
    def __init__(self, op, expr1, expr2, symbol=None):
        super().__init__(op, expr1, expr2)
        self.symbol = symbol

    def __repr__(self):
        if self.symbol is None:
            return super().__repr__()
        return '({!r} {} {!r})'.format(self.args[0], self.symbol, self.args[1])

if __name__ == '__main__':
    x = Var('x')
    expr = 2 * (-x + 1)
    print(expr)
    print(compute(expr, x=1))

    y = Var('y')
    expr += y
    print(compute(expr, x=0, y=10))

Les opérateurs sont une notion importante en Python, mais ils sont loin d’être la seule. Le chapitre suivant vous présentera d’autres concepts avancés du Python, qu’il est important de connaître, pour être en mesure de les utiliser quand cela s’avère nécessaire.