Python : liberez les expressions membres !

Amateur de programmation et en particulier fan du langage python, je trouve dommage que ce langage (ou d'autres) ne permette pas d'écrire des "expressions membres". Je vais essayer de vous expliquer en quoi cela consisterait.

Les expressions membres comblent un vide

Si l'on examine un code source quelconque (ici en python), une bonne partie des lignes consiste à  évaluer une expression, et à placer le résultat dans une variable :

n = sqrt(x**2 + y**2 + z**2)

Lorsque dans un programme apparait plusieurs fois la même expression, le programmeur a le réflexe d'introduire une fonction pour "factoriser du code".

Ainsi le code suivant :

na = sqrt(xa**2 + ya**2 + za**2)
nb = sqrt(xb**2 + yb**2 + zb**2)

sera remplacé par :

def norme(x, y, z):
  return sqrt(x**2 + y**2 + z**2)

na = norme(xa, ya, za)
nb = norme(xb, yb, zb)

Dans le cas où ce calcul répétitif concerne plus particulièrement les données membres d'un object, on introduit alors une fonction membre. Par exemple :

class Vecteur:
  ...
  def norme(self):
     return sqrt(self.x**2 + self.y**2 + self.z**2)
...
na = a.norme()
nb = b.norme()
nc = c.norme()

Une expression peut donc :
- être répétitive ou non (2 possibilités)
- concerner ou non les données membres d'un objet
(2 possibilités)
2 possibilités x 2 possibilités = 4 possibilités. Pourtant dans le cas ci-dessus je n'ai traité que 3 cas !... Il manque un cas de figure... Faisons un tableau :

ne concerne pas les données membres d'un objet concerne les données membres d'un objet
expression non répétitive n = sqrt(x**2 + y**2 + z**2) ?
expression répétitive
(introduction d'une fonction)
def norme(x, y, z):
  return x**2 + y**2 + z**2

na = norme(xa, ya, za)
nb = norme(xb, yb, zb)
nc = norme(xc, yc, zc)
class Vecteur:
  ...
  def norme(self):
     return sqrt(self.x**2 + self.y**2 + self.z**2)

na = a.norme()
nb = b.norme()
nc = c.norme()

Il manque donc le cas où une expression non répétitive concerne plus particulièrement les données membres d'un objet...

Par exemple, quelle syntaxe pourrions nous utiliser pour, à partir de l'expression v1 + v2  exprimer la norme ?
On pourrait parler d'une "expression membre", c'est à dire une expression qui concerne les données membres de l'objet. Reprenons le point qui indique la notion de membre :
(v1+v2).
Il nous faut alors rajouter une expressions concernant les données membre. Prenons comme convention que "self" représente l'objet (pour que tout cela reste bien "pythonique"), et plaçons l'expression entre parenthèses :
(v1+v2).(sqrt(self.x**2 + self.y**2 + self.z**2))
Nous avons donc notre élément manquant, et on peut donc achever le tableau :


ne concerne pas les données membres d'un objet concerne les données membres d'un objet
expression non répétitive expression
n = sqrt(x**2 + y**2 + z**2)
expression membre
nab = (b-a).(sqrt(self.x**2 + self.y**2 + self.z**2))
expression répétitive
(introduction d'une fonction)
fonction
def norme(x, y, z):

  return x**2 + y**2 + z**2

na = norme(xa, ya, za)
nb = norme(xb, yb, zb)
nc = norme(xc, yc, zc)
fonction membre
class Vecteur:
  ...
  def norme(self):
     return sqrt(self.x**2 + self.y**2 + self.z**2)

na = a.norme()
nb = b.norme()
nc = c.norme()

Pour résumer :
- La notion de fonction est associée à la factorisation de code
- La notion de membre est associé au fait que l'on travaille sur un objet particulier
Ces deux concepts sont indépendants (orthogonaux), il est naturel que la syntaxe permette d'exprimer ces deux concepts indépendamment.

Lisibilité : une écriture postfixée

On vient de voir que les expressions membres comblent un vide dans la syntaxe. Si on prend l'habitude de les utiliser, on ne tardera pas à voir régulièrement des situations où elles allègent le code.

Mais un autre avantage important des expressions membre, c'est qu'il s'agit d'une écriture postfixée, c'est à dire qui se lit toujours de gauche à droite. Lorsque l'on a recours à des expressions compliquées, la lisibilitée devient critique. Prenons par exemple une expression telle que :

string2html(''.join(a.strip().split('/n')[:-1])).lower()

Il faut s'y reprendre à plusieurs fois pour savoir dans quel ordre se font les opérations. Cela provient notament du fait que l'on a affaire à un mélange d'écriture de gauche à droite et de droite à gauche.
Utilisons les couleurs de l'arc-en-ciel pour voir à quel étape de calcul appartiennent chacun des symboles :
etape 1  etape 2  etape 3  etape 4  etape 5  étape 6 étape 7

Pour notre expression originale, cela donne

string2html(''.join(a.strip().split('/n')[:-1])).lower()

On peut alors utiliser des expressions membres pour faire apparaître des fonctions postfixées

a.strip().split('/n')[:-1].(''.join(self)).(string2html(self)).lower()

Si l'on remarque bien, cela rajoute quelques parenthèses, mais finalement on ne pert pas en lisibilité, loin de là : le niveau d'imbrication de dépasse pas 2, mais surtout on lit naturellement les étapes toujours dans le même sens (de gauche à droite).

Bien sûr, les couleurs ici aident beaucoup à lire mais si les expressions membres rentrent dans les moeurs, on sera habitué à décoder du premier coup d'oeil une suite d'opération de la forme:
expr1.( ..expr2.. ).( ..expr3.. ).( ..expr4.. ).( ..expr5.. ) etc...

Economies de variables et de mémoire

Reprenons une instruction telle que :
nab = (b-a).(sqrt(self.x**2 + self.y**2 + self.z**2))

L'expression membre remplace l'introduction de variables locales que nécessiterait la méthode classique :
temp = b-a
nab = sqrt(temp.x**2 + temp.y**2 + temp.z**2)

De même, si l'on est habitué à lire les expressions membres, on n'hésitera pas à écrire une instruction telle que :
print a.strip().split('/n')[:-1].(''.join(self)).(string2html(self)).lower()

Au lieu de :
temp1 = a.strip().split('/n')[:-1]
temp2 = ''.join(temp1)
print string2html(
temp2).lower()

La philosophie des expressions membres pourrait être "pourquoi introduire un symbole de variable pour le jeter juste après ?"
Ainsi, on se débarrasse des variables locales introduites pour la simple lisibilité. Celles-ci étaient d'ailleurs à double tranchant : elles rendaient le code plus lisible localement, mais au prix de l'introduction de nouveaux symboles dans le code, ce qui alourdit celui-ci globalement. Plus le nombre de variables est important, plus il est difficile mentalement de faire la distinction entre ces variables "éphèmères" (utilisées uniquement sur 2 lignes consécutives) et celles qui devront être utilisées plus tard dans le code.
De plus, on élimine le risque d'utiliser par mégarde un nom de variable locale déjà utilisé pour autre chose et à portée du code courant.

Par ailleurs, lorsque l'on utilise des variables intermédiaires
temp1, temp2, etc. , celles-ci sont gardées en mémoire jusqu'à ce qu'elles soient hors de portée.
Au contraire, dans le cas d'une expression membre, le compteur de référence peut détruire la valeur intermédiaire immédiatement, puisqu'elle n'est plus référencée.

Cas des "list comprehension"

La possibilité d'écrire des "list comprehension" pour remplacer les "boucles for" classiques est un des points forts de python. Par exemple :
nombresimpairs = [ 2*i + 1 for i in range(49) ]
On effectue ainsi une "mutation de liste" en remplaçant un élément i par l'élément 2*i + 1 dans la nouvelle liste.

La contrainte est donc que l'élément de la nouvelle liste puisse s'exprimer en une simple expression de l'élément de la liste de départ.

Dans le cas contraire, on est obligé d'introduire une variable intermédiaire et de remplacer la belle "list comprehension" par une vilaine boucle for, comme le montre l'exemple suivant :

ecarts = []
for v in vecteurs:
  temp = v - v0

 
ecarts.append( sqrt(temp.x**2 + temp.y**2 + temp.z**2) )

Les expressions membres pourraient être alors bien utile pour retrouver notre "list comprehension" :

ecarts = [ (v - v0
).(sqrt(self.x**2 + self.y**2 + self.z**2)) for v in vecteurs ]

Ne pas masquer une variable self

Si une expression membre est utilisée dans la définition d'une fonction membre d'une classe A, la variable "self" de l'expression membre masquera la variable "self" de l'objet de classe A.
Pour cela, on peut imaginer que la syntaxe permette d'utiliser un nom autre que "self" pour l'expression membre, par exemple :
(v1+v2).(v: v.x**2 + v.y**2 + v.z**2 )
Cette syntaxe est donc plus souple, mais pour alléger le cas général on considère que "self:"  est sous-entendu au début des expressions membres ne précisant pas un nom de variable.