modifié le

Opérateurs

operateurs

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+z2z1 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

1.1. Opérateur binaire

Une opération binaire   xαy   est interprétée comme   x.mα(y), où mα est la méthode implémentant l’opérateur α. Par exemple, la méthode implémentant l’opérateur + étant __add__(), alors z1+z2 est interprété comme z1.__add__(z2).

1.2. Opérateur unaire

Une opération unaire   αx   est interprétée comme   x.mα() , où mα est la méthode implémentant l’opérateur α. Par exemple, la méthode implémentant l’opérateur unaire - étant __neg__(), -z est interprété comme z.__neg__().

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)