Python et la notation infix

le 14 Janvier 2010 par Julien Palard
Bonjour ami ! Bienvenue à l'innauguration du blog Developpeurs d'eeple.fr ! Je vais faire ça vite car je sais que vous êtes la pour lire de la ligne de code :
============================================8<===============================================
Voilà, ça, c'est fait.
Sujet du jour : La notation infix dans Python. Pour ceux qui se demandent "Pourquoi l'infix" ? Je répond "Parce que la notation prefix, c'est pas toujours lisible !"
Mais pour les incoditionnels du prefix, lisez donc ça :
sum(select(where(take_while(fib(), lambda x: x < 1000000) lambda x: x % 2), lambda x: x * x))

 

Rien de compliqué pourtant... Bon, coder sur une ligne, c'est mal, alors tentons d'indenter, avec un peu d'espoir :

 

sum(
select(
where(
take_while(
fib(),
lambda x: x < 1000000)
lambda x: x % 2)
lambda x: x * x))
 

 

Toujours aussi sale, et plus j'en rajouterai, moin ca sera lisible...
Comparons la même en infix :
fib().take_while(lambda x: x < 1000000).where(lambda x: x % 2).select(lambda x: x * x).sum()

 

Et sa version indentée :
fib()
.take_while(lambda x: x < 1000000)
.where(lambda x: x % 2)
.select(lambda x: x * x)
.sum()
 

Class, hum ?
Bon maintenant, c'est bien beau, mais comment s'y prend-on en Python ? Et bien, on ne peut pas .... mais ne partez pas  ! Bon c'est vrai qu'on peut pas, enfin, pas sans faire de grosses dégeulasseries ... mais apres une petite nuit de méditation, et les pistes d'un ami, voilà sur quoi on tombe :
fib() | take_while(lambda x: x < 1000000)
| where(lambda x: x % 2)
| select(lambda x: x * x)
| sum()

 

Pour les habitués du shell, la notion du pipe est la même ici, on pipe des données d'une fonction à l'autre. Mais en shell me direz vous, on est pas obligés d'attendre la fin de l'éxécution d'un programme pour que le pipe transfère le données au suivant ... Bon bah, si ils le font, on va le faire ! Il faudra que notre code soit compatible avec les iterators de Python.
Ce qu'il nous faudrait pour commencer, c'est un | qui fonctionne, un pipe, en python, ça existe, c'est un or, on pourrait donc essayer de s'amuser avec la surcharge d'operateur :
class Stdout:
def __ror__(self, value):
print value
 
class Double:
def __ror__(self, value):
return value * 2
 
stdout = Stdout()
double = Double()
 
42 | stdout
# 42
"foo" | stdout
# foo
42 | double | stdout
# 84

 

C'est un début ...

Bon jouons avec les Generateurs, maintenant...

 

def count():
"Creeons un rapide generateur de test, retournant 0 1 2 et 3"
yield 0
yield 1
yield 2
yield 3
 
class Double:
def __ror__(self, values):
for i in values:
yield i * 2
 
class Stdout:
def __ror__(self, values):
for i in values:
print i
 
double = Double()
stdout = Stdout()
 
count() | double | stdout
# 0 2 4 6

 

Tout semble fonctionner aussi avec des générateurs, mais, ce n'est pas très souple, si on pouvait factoriser ça un peu et créer une classe qui nous génèrerai des pipes, ce serait mieux, quelque chose dans ce style 

 

count() | select(lambda x: x * 2) | stdout

 

Mais cela nécessite de pouvoir passer des paramètres à un pipeable, passer un paramètre tel que je l'ai écrit ici, c'est l'appeler, donc il doit être applable, et l'appel, tel que je l'ai écrit ici, se passera au moment de l'interprétation de la ligne, et non lors du déroulement du pipe, donc, select ici n'est en fait pas un pipe, mais une fonction qui return un pipe. Attention, ça pique :

 

def count():
yield 0
yield 1
yield 2
yield 3
 
class Pipe:
def __init__(self, function):
self.function = function
def __ror__(self, other):
return self.function(other)
 
class FuncPipe:
def __init__(self, function):
self.function = function
def __call__(self, *value):
return Pipe(lambda x: self.function(x, *value))
 
select = FuncPipe(lambda left, pred: (pred(x) for x in left))
where = FuncPipe(lambda left, pred: (x for x in left if pred(x)))
 
for i in count() | where(lambda x: x % 2 == 0) | select(lambda x: x * 2):
print i
# 0 4

 

Et voilà, tout semble fonctionner, il ne nous reste plus qu'a écrire quelques Pipes et quelques FuncPipes de base, et de tester !

 

import itertools
from random import randint
 
def rand(min, max):
while True:
print "Generating a random number ..."
yield randint(min, max)
 
class Pipe:
def __init__(self, function):
self.function = function
def __ror__(self, other):
return self.function(other)
 
class FuncPipe:
def __init__(self, function):
self.function = function
def __call__(self, *value):
return Pipe(lambda x: self.function(x, *value))
 
def _head(left, qte):
for x in left:
if qte > 0:
yield x
else:
return
qte -= 1
 
def _average(left):
total = 0.0
qte = 0
for x in left:
total += x
qte += 1
return total / qte
 
average = Pipe(_average)
head = FuncPipe(_head)
 
print "Average of 1000 random integers between 0 and 100 :"
ten_random_integers = rand(0, 100) | head(10)
print "Point 1"
print ten_random_integers | average
 

 

Quizz ! Que va afficher le code précédent ?

1) Rien, il part en boucle infinie...

2) Des "Generating a random number" à l'infinie

3) 10 "Generating a random number" puis "Point 1" puis le résultat

4) "Point 1" puis 10 "Generating a random number" puis le résultat ?

 

... ?

 

 

... ?

 

 

C'est bien le dernier qui s'affiche, et ça confirme bien que tout reste bien lazy évalué, la ligne :

 

ten_random_integers = rand(0, 100) | head(10)

 

Ne fait que décrire un générateur associé à son comportement, rand(0, 100) est un générateur infini, mais aucune valeur n'est itérée.

 

Bon bah on a tout, c'est parti ! On peut s'amuser !
Par exemple, résolvons la 2nd énigme du Euler Project :
"Find the sum of all the even-valued terms in Fibonacci which do not exceed four million." :
euler2 = fib() | where(lambda x: x % 2 == 0) | take_while(lambda x: x < 4000000) | sum
assert euler2 == 4613732
 

 

Et voilà, comme tout histoire qui se finit bien, vous finissez avec le code source : http://github.com/downloads/JulienPalard/Pype/pype.py contenant tout ce qu'il faut pour s'amuser, une bonne batterie de pipes de base, et un main de test, enjoy :-)