Python et le taoïsme

mercredi 1er avril 2020
par  Alain BUSSER , Sébastien HOARAU

À la question « un chien a-t-il la nature de Bouddha ? » le moine Joshu a répondu « mu (zen) » que qui veut dire « rien » ou « vide ». Le Tao quant à lui conseille d’« agir sans agir » :

La voie reste toujours sans agir,
mais sans elle rien ne se fait.

Nous allons donc voir, avec les bouddhistes Zen, comment le rien peut être modélisé en Python, et ensuite, nous allons rejoindre les taoïstes pour voir comment, toujours en Python, se modélise la non action. En passant on verra également ce qu’est au sens éthymologique, la fainéantise.

Modélisation du rien en Python

Le rien est-il variable ?

L’instruction suivante a pour effet de créer une variable de nom v et d’y mettre du rien :

v = None

En effet l’objet None est vide (il ne possède pas vraiment de valeur, ou plutôt on modélise cette valeur par None). En modélisant mathématiquement une variable par un couple (nom,valeur) ou une boîte contenant une valeur et portant une étiquette avec un nom, la variable de nom v que l’on a créée ci-dessus est une boîte portant l’étiquette « v » mais vide.

Où est le vide ?

En entrant

id(v)

on apprend à quel emplacement de la mémoire vive, se situe cet objet vide. Il n’y a qu’un seul emplacement car il n’y a qu’un seul vide : on dit que le rien est un singleton (patron de conception).

C’est quoi le rien ?

En entrant

type(v)

on a l’affichage

<class 'NoneType'>

qui suggère que le rien est un type.

Que peut-on faire avec rien ?

Pour connaître les méthodes de cet objet vide, on peut entrer dans la console

dir(v)

ce qui donne quelque chose comme

['__bool__', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

Somme toute, il y a pas mal de choses que l’on peut faire avec le vide !

Peut-on voir le vide ?

Et bien cela dépend. Le vide se pare avant de sortir. Il peut être invisible ou au contraire apparaître clairement aux yeux de tous. Dans la console, entrer v n’a pas le même effet qu’entrer print(v). Dans le premier cas, est invoquée la méthode __repr__ (qui renvoie la chaîne vide et nous est donc invisible) alors que dans le second cas, c’est la méthode __str__ qui est appliquée, et celle-ci renvoie la chaîne de caractères

'None'

Quelle est la taille du vide ?

Puisqu’on a vu que le vide avait un type, qu’on pouvait même le voir, on est en droit de se poser la question : a-t-il une taille ?

v.__sizeof__() Nous renseigne efficacement :

16

Mais alors, en Python, peut-on être un moins que rien ? Quelques tests nous incitent à penser que non :

  1. x = 0
  2. x.__sizeof__()

Télécharger

nous donne :

24

  1. s = ''
  2. s.__sizeof__()

Télécharger

nous donne :

49

Le vide est-il égal à lui-même ?

Il n’y a qu’à demander à Python :

v == v

La réponse True est éclairante !

La logique du vide

  1. v == True
  2. False

Télécharger

  1. v == False
  2. False

Télécharger

Oh ! Le vide n’est ni vrai ni faux. Pourtant :

  1. if v:
  2.    print('Le vide est vrai')
  3. else:
  4.    print('Le vide est faux')

Télécharger

Nous affiche :
Le vide est faux

En réalité, l’expression

bool(v)

s’évalue à False

Le vide s’apparente au faux.

Finalement c’est quoi le rien ?

En écrivant help(v) on obtient cette description du rien :

  1. Help on NoneType object:
  2.  
  3. class NoneType(object)
  4.  |  Methods defined here:
  5.  |  
  6.  |  __bool__(self, /)
  7.  |      self != 0
  8.  |  
  9.  |  __new__(*args, **kwargs) from builtins.type
  10.  |      Create and return a new object.  See help(type) for accurate signature.
  11.  |  
  12.  |  __repr__(self, /)
  13.  |      Return repr(self).
  14. (END)

Télécharger

Ceci confirme qu’on peut convertir du rien en proposition logique (on obtient False comme réponse à la question self != 0 ce qui signifie que le vide n’est pas différent de zéro - est-il pour autant égal à 0 ?- non car v==0 donne aussi l’affichage False), qu’on peut créer le vide (mais une seule fois) et qu’il possède une représentation qui est la représentation du vide.

Le vide n’est pas non nul.
Le vide n’est pas nul non plus.

Renvoyer du rien ou ne rien renvoyer ?

Le rien en Haskell

En programmation fonctionnelle, et en particulier en Haskell, l’équivalent du None de Python s’appelle le type unité. Il se note par des parenthèses vides. Pour savoir (avec ghci qui est une console interactive en Haskell) de quel type est le rien, on entre

:t ()

ce qui donne l’affichage suivant :

() :: ()

ce qui signifie que le rien est du type du rien. On s’en doutait un peu quand même.

En Haskell, le rien (appelé unité) sert à définir des fonctions impures comme cette fonction afficher :

let afficher texte = print texte

qui a pour but d’afficher du texte, et non de renvoyer une valeur. Pour connaître son type on demande à Haskell

:t afficher

et on obtient

afficher :: Show a => a -> IO ()

Ce qui est avant le => signifie que si a est un type compatible avec Show (quelque chose qu’on peut montrer) alors la fonction afficher est de type a -> IO () c’est-à-dire le type d’une fonction qui, à a, associe ce qu’on obtient en enveloppant le rien (les parenthèses vides) dans une monade IO (comme « input-output » ; c’est la monade qui contient print).

C’est là l’utilité en Haskell d’avoir du rien : c’est ce rien que renverra une fonction comme afficher qui sert à faire quelque chose (afficher son argument) et non à renvoyer quoi que ce soit : à la place cette « fonction » renvoie du rien.

Ne soyez pas procéduriers, c’est impur

On peut (un peu arbitrairement) classer les fonctions de Python en trois catégories :

  • Les fonctions pures qui ne servent qu’à renvoyer quelque chose (elles ne font pas d’effet de bord) ;
  • les fonctions impures qui renvoyent quelque chose mais, en plus, modifient leur environnement (exemple la méthode pop qui fait sauter le bouchon du saké avant de renvoyer le bouchon) ;
  • les procédures qui ne font que des effets de bord.

Les spécialistes de la programmation fonctionnelle n’aiment pas utiliser le mot « procédure » mais celui-ci est peut-être plus naturel en mathématiques où une fonction a vocation à être composée avec une autre fonction, pas à modifier l’environnement.

Or en Python comme dans les langages fonctionnels il n’y a pas de possibilité de distinguer les fonctions des procédures, on propose en général de remplacer le mot « procédure » par l’expression « fonction ne renvoyant rien ».

Comment Python peut-il ne rien renvoyer ?

2 petits riens

Cette fonction ne renvoie rien :

  1. def rien1():
  2.     return

Télécharger

On verra plus bas que même le mot-clé return est facultatif. Mais ici c’est une instruction (et non une fonction) intimant à la machine l’ordre de quitter le corps de la fonction. Cette fonction ne renvoie donc rien, comme on peut le constater en entrant dans la console rien1() et en constatant que rien n’est affiché (puisque rien n’est renvoyé et que la console devait afficher ce que renvoie la fonction, c’était prévisible).

On obtient exactement la même chose (c’est-à-dire rien) en entrant dans la console rien2() après avoir défini la fonction suivante :

  1. def rien2():
  2.     return None

Télécharger

Ne rien renvoyer ou renvoyer du rien, en Python, c’est pareil.

Les apparences sont-elles trompeuses ?

On ne sait jamais, il se pourrait qu’en cachette les deux fonctions rien1 et rien2 ci-dessus ne fassent pas exactement la même chose dans les arcanes de la machine. Pour en savoir plus on peut utiliser le module de désassemblage de bytecode fourni avec Python. En anglais « désassemblage » se traduit par disassemble et tant le module que la fonction se nomment dis.

On écrit donc

from dis import *

avant les définitions des fonctions rien1 et rien2 puis, après les définitions, dis(rien1) ou dis(rien2) permet de savoir comment chacune est exécutée pas à pas par la machine virtuelle Python.

Les deux fonctions donnent exactement le même bytecode :

 4           0 LOAD_CONST               0 (None)
             3 RETURN_VALUE

Il n’y a, côté assembleur, que deux lignes :

  • La ligne 4 consiste à charger la valeur qui sera renvoyée par la fonction. Il s’agit d’une constante de taille 0 octet et de valeur None.
  • La ligne suivante (3 car il y a eu changement dans la taille de la pile) est le retour de la valeur en question : tout ce qui concernait l’exécution de la fonction est oublié (dépilé) et la constante None est renvoyée à ce moment-là.

Ceci signifie qu’en réalité, renvoyer du rien revient effectivement à ne rien renvoyer, ou plutôt l’inverse : Python insère automatiquement un None derrière return, s’il n’y a rien. Autrement dit, s’il voit un return sans rien derrière, il considère que ce rien est None (ce qu’on ne peut guère lui reprocher) et renvoie None, comme s’il avait vu return None.

Faire du rien

3 fois rien

Et si on essayait de ne même pas mettre de ligne avec return ?

Pas si facile : si on essaye d’entrer

  1. def rien3():
  2.    

Télécharger

on a un message d’erreur sur l’indentation non respectée après le double-point (une ligne vide n’est pas une ligne indentée).

Pour éviter ce désagrément on peut, ou bien mettre quelque chose d’inutile genre a=a ou bien faire

  1. def rien3():
  2.     pass

Télécharger

Cette nouvelle fonction donne exactement le même bytecode que les deux précédentes comme on peut le vérifier en la soumettant à la fonction dis de désassemblage.

L’instruction la plus taoïste de Python est assurément ce pass qui ne fait rien. La présence d’une telle instruction ne faisant rien n’est pas spécifique à Python, par exemple la plupart des assembleurs possèdent une instruction NOP qui a le même effet. Rappelons qu’une instruction est un ordre donné à la machine lorsqu’on veut qu’elle fasse quelque chose (affecter une variable, afficher un résultat, etc). Mais ici on veut que la machine ne fasse rien. C’est néanmoins une instruction.

L’eau est faible
De ce qui est fort
rien ne la passe
rien ne prend sa place

(Lao Tseu)

Sage, passage et repassage

Quitte à ne rien faire, pourquoi pas le (pas) faire plusieurs fois ?

La fonction suivante accepte un entier n en entrée, puis répète n fois l’action (ou plutôt la non-action) de ne rien faire :

  1. from time import *
  2.  
  3. def repassage(n):
  4.     t = time()
  5.     for _ in range(n):
  6.         pass
  7.     return time()-t

Télécharger

Noter que si on appelle la fonction avec un argument nul, elle ira encore plus loin que ne rien faire puisqu’elle fera rien, zéro fois. Y a-t-il une langue au monde qui possède un verbe pour cette non-action (ne rien faire, mais zéro fois) ?

En fait la fonction repassage n’est pas sage : Elle chronomètre le temps pris à ne rien faire. Et il semble intéressant d’évaluer ce temps, notamment pour de grandes valeurs de n. Ce script le permet :

  1. from matplotlib.pyplot import * # ligne à placer au tout début du script
  2.  
  3. zen = [repassage(n) for n in range(1000)]
  4. plot(range(1000),zen,'b.')
  5. show()

Télécharger

Le résultat est que mine de rien ça prend quand même un peu de temps, de ne rien faire (même si n est égal à zéro : le temps de l’appel à la fonction) :

On voit que la droite ne passe pas tout-à-fait par l’origine : Ne rien faire zéro fois prend très peu de temps mais un peu de temps quand même. Par contre la paresse itérée est plus chronophage que la paresse occasionnelle.

Le sage ne fait que passer dans le Tao

Mais à quoi peut donc servir cette instruction qui ne fait rien ? C’est une histoire de gabarit. Par exemple, lorsqu’on veut créer son propre objet, on a intérêt à donner la liste des méthodes avant de voir comment on programme les méthodes. On commencera donc typiquement à écrire quelque chose comme

  1. class MonObjet():
  2.     def méthode1(self):
  3.         pass
  4.     def méthode2(self):
  5.         pass

Télécharger

Ensuite on teste avec un script du genre

  1. o = MonObjet()
  2. o.méthode1()
  3. o.méthode2()

Télécharger

S’il n’y a aucun message d’erreur, et dans ce cas seulement, on commence à réfléchir à ce qu’on va mettre à la place des pass.

On a vu une autre application de cette instruction dans cet article : Les méthodes des variables de Sofus ne sont pas accessibles depuis le menu des fonctions de la calculatrice, et il est donc pratique de créer des fonctions globales ayant le même nom, mais ne faisant rien. Elles ne font donc que passer à la fin de ce script.

Selon Dana Scott et Christopher Strachey, une instruction est la requête d’un effet de bord. Or l’instruction nulle n’a pas d’effet de bord. Doit-on alors la considérer comme une instruction, ou pas ? ou pass ?


Commentaires

Logo de Dhénin
mercredi 26 août 2020 à 01h55 - par  Dhénin

Chang Tseu dit :

« Traiter les choses en chose plutôt que d’être traité en chose par les choses. » 

https://dumas.ccsd.cnrs.fr/dumas-01... p 34