Opérateurs

L’objectif ici est de voir comment on peut appliquer les opérateurs, habituels agissant sur les types prédéfinis, pour réaliser des opérations avec de nouveaux types d’objets.
Par exemple, comment donner un sens à l’expression z1+z2
où z1
et z2
sont des instances du type Complex
défini précédemment.
Le principe pour étendre la sémantique des opérateurs à de nouveaux types passe par la définition de méthodes particulières.
1. Le principe général
2. Opérandes de même type
On s’intéresse ici aux opérateurs binaires s’appliquant à 2 objets de même type, par exemple l’addition de 2 complexes. Dans ce cas, il suffit de créer les méthodes adéquates dans la classe concernée.
2.1. Opérateurs arithmétiques binaires
Les méthodes implémentant ces opérateurs sont :
__add__
, __sub__
, __mul__
, etc.
Elles prennent en argument un complexe. L’opération est réalisée entre le complexe courant et le complexe passé en argument. La méthode retourne un nouveau complexe, résultat de l’opération. L’objet courant est inchangé.
Exemple, définir les 2 méthodes ci-dessous :
def __add__(self,z):
return Complex(self.x+z.x, self.y+z.y)
def __mul__(self,z):
return Complex(self.x*z.x-self.y*z.y, self.x*z.y+self.y*z.x)
va permettre d’écrire :
z1= Complex(3,4)
z2= Complex(3,5)
print(z1+z2) # print(z1.__add__(z2))
print(z1*z2) # print(z1.__mul__(z2))
2.2. Opérateurs arithmétiques unaires
L’opposé est un exemple d’opérateur arithmétique unaire.
Il est implémenté par la méthode __neg__
.
Elle ne prend pas d’argument. L’opération est réalisée à partir du complexe courant. La méthode renvoie un nouveau complexe, résultat de l’opération. L’objet courant est inchangé.
Donc définir la méthode :
def __neg__(self):
return Complex(-self.x,-self.y)
va permettre d’écrire :
z= Complex(1,2)
print(-z) # print(z.__neg__())
Les équivalences habituelles n’existent pas. Par exemple, ce n’est pas parce qu’on sait faire une addition et calculer l’opposé que l’on peut soustraire de 2 complexes. |
2.3. Opérateurs relationnels
Ce sont des opérateurs binaires.
Les méthodes implémentant ces opérateurs sont :
__lt__
, __le__
, __eq__
, __ne__
, __gt__
et __ge__
.
Elles prennent en argument un complexe. L’opération est réalisée entre le complexe courant et le complexe passé en argument. La méthode retourne un booléen, résultat de l’opération.
Donc définir la méthode :
def __eq__(self,z):
return self.x==z.x and self.y==z.y
permet d’écrire :
z1= Complex(3,4)
z2= Complex(3,5)
if z1==z2: # if z1.__eq__(z2):
..
Par défaut, __ne__ renvoie la négation de __eq__ ,
et n’a donc pas a être personnalisé dans la plupart des cas.
|
L’exemple complet donne :
class Complex:
...
def __add__(self,z):
return Complex(self.x+z.x, self.y+z.y)
def __mul__(self,z):
return Complex(self.x*z.x-self.y*z.y, self.x*z.y+self.y*z.x)
def __neg__(self):
return Complex(-self.x,-self.y)
def __eq__(self,z):
return self.x==z.x and self.y==z.y
if __name__=="__main__":
z1= Complex(3,4)
z2= Complex(3,5)
z3= Complex(3,4)
print(z1+z2)
print(z1*z2)
print(-z1)
print(z1==z2) # False
print(z1!=z2) # True
print(z1<z2) # TypeError: '<' not supported between instances of 'Complex' and 'Complex'
print(z1==z3,z1 is z3) # True False
2.4. Opérateurs de modification sur place
Ce sont les opérateurs +=
, *=
, etc.
Sur les types prédéfinis, xα=y
signifie x=xαy
.
Par exemple, x+=y
signifie x=x+y
,
x/=y
signifie x=x/y
, etc.
Cette relation implicite n’existe pas pour les types créés par le programmeur.
Même si l’on a défini z1+z2
entre 2 complexes, cela n’autorise pas l’écriture de z1+=z2
.
Il faut donc définir ces opérateurs explicitement si l’on veut en disposer.
Les méthodes à utiliser sont :
__iadd__
, __isub__
, __imul__
, etc.
Exemple :
class Complex:
...
def __iadd__(self,z):
self.x += z.x
self.y += z.y
return self
def __imul__(self,z):
self.x = self.x*z.x-self.y*z.y
self.y = self.x*z.y+self.y*z.x
return self
if __name__=="__main__":
z1 = Complex(3,4)
z2 = Complex(1,5)
z1 += z2
z1 *= z2
Ces méthodes doivent retourner self .
|
3. Opérandes de types différents
Il s’agit par exemple de définir des opérations entre 1 complexe et 1 réel (dans cet ordre).
Pour cela, on teste le type du paramètre (par exemple, avec isinstance
),
et on fait le calcul approprié.
Exemple :
class Complex:
# ...
def __add__(self,z):
if isinstance(z,Complex):
return Complex(self.x+z.x, self.y+z.y)
elif isinstance(z,int) or isinstance(z,float):
return Complex(self.x+z, self.y)
else:
raise RuntimeError("opération non implémentée")
if __name__=="__main__":
z= Complex(1,1)
print(z+z) # 2+2i
print(z+1) # 2+i (1)
print(1+z) # TypeError: unsupported operand type(s) for +: 'int' and 'Complex' (2)
1 | pas de surprise, z+1 est interprêté comme z.__add__(1) . |
2 | par contre, erreur pour 1+z , qui est interprêté comme 1.__add__(z) , ce qui n’a pas de sens. |
4. Premier opérande non personnalisable
Parfois il n’est pas possible de modifier la classe du premier opérande.
Par exemple, pour implémenter l’addition entre un numpy.float16
et un complexe,
il faut pourvoir le faire sans nécessité d’étendre la classe numpy.float16
.
Parfois ce premier opérande n’est même pas une instance de classe
(s’il est de type int
, float
, etc.).
Si aucune méthode n’est trouvée dans le premier opérande pour réaliser l’opération souhaitée, alors une méthode alternative est recherchée dans le second.
Ces méthodes alternatives sont
__radd__
, __rsub__
, __rmul__
, etc.
Exemple, voici comment on peut définir l’addition et la multiplication entre un scalaire et un complexe (dans cet ordre) :
class Complex:
...
def __radd__(self,x):
return Complex(self.x+x, self.y)
def __rmul__(self,x):
return Complex(self.x*x, self.y*x)
if __name__=="__main__":
z = Complex(3,4)
print(1+z)
print(2+z)