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.