XXVII. Exemples▲
XXVII-1. Suite n-bonacci▲
La suite N‑bonacci est une construction mathématique basée sur la suite de Fibonacci mais avec un nombre N d’éléments de départ ; et où chaque terme sera alors la somme des N termes qui le précèdent.
Par exemple la suite 4‑bonacci partira de 4 chiffres (ex U0=1, U1=1, U2=1, U3=1) et chaque terme suivant sera alors la somme des 4 termes précédents (U4=4, U5=7, U6=13, etc).
Si on prend la suite N‑bonacci, comme référence, alors la suite de Fibonacci est simplement une suite N‑bonacci de rang 2 (une 2‑bonacci).
Petit exemple d’une suite de N‑bonacci où la base de départ est donnée par l’appelant.
XXVII-1-a. En récursif▲
XXVII-1-b. En itératif▲
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
#!/usr/bin/env python3
# coding: utf-8
def n_bonacci(base, n=0):
if n < len(base): return base[n]
for i in range(n - len(base) + 1):
base=base[1:] + (sum(base),)
return base[-1]
# n_bonacci()
if __name__ == "__main__":
for i in range(11):
print(i, n_bonacci((1, 2, 3, 4), i))
# if
XXVII-1-c. Avec une pile▲
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
#!/usr/bin/env python3
# coding: utf-8
def n_bonacci(base, n=0):
res=0
pile=[n,]
while pile:
v=pile.pop()
if v >= len(base):
pile.extend(range(v-1, v-len(base)-1, -1))
else:
res+=base[v]
# while
return res
# n_bonacci()
if __name__ == "__main__":
for i in range(11):
print(i, n_bonacci((1, 2, 3, 4), i))
# if
XXVII-1-d. Sous forme de générateur▲
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
#!/usr/bin/env python3
# coding: utf-8
def n_bonacci(base, n=0):
for i in range(n):
if i < len(base):
yield base[i]
continue
# if
base=base[1:] + (sum(base),)
yield base[-1]
# for
# n_bonacci()
if __name__ == "__main__":
for (i, f) in enumerate(n_bonacci((1, 2, 3, 4), 11)):
print(i, f)
# if
XXVII-2. Tracer le scope local▲
Création d’un objet spécifique avec affichage dans les méthodes __init__() et __del__() pour montrer le mécanisme de création et suppression automatique des variables locales dans une fonction.
2.
3.
4.
5.
6.
# class cTrace()
def __init__(self, n):
print("init:", n)
self.__n=n
def __del__(self): print("del:", self.__n)
# class cTrace
XXVII-2-a. Quand l’objet est stocké dans une variable de la fonction▲
2.
3.
4.
5.
6.
7.
8.
def fct(n):
print("in fct:", n)
var=cTrace(n)
print("out fct:", n)
print(1) # Message témoin
fct("xxx") # Appel de la fonction
print(2) # Message témoin
Résultat :
2.
3.
4.
5.
6.
1
in fct: xxx
init: xxx
out fct: xxx
del: xxx
2
Explications : le premier message « in fct » montre la fonction qui démarre avec son paramètre reçu. Ensuite, elle instancie un objet cTrace qui est récupéré par la variable var. Le message « init » est donc affiché à l'instanciation de l'objet. Puis la fonction se termine et affiche le message « out fct » avant de sortir. Lors de cette sortie, ses variables internes sont alors toutes supprimées ce qui détruit l'objet par ricochet et le message « del » de l'objet est alors affiché à sa destruction. Et tout ceci se passe entre les deux messages témoins.
XXVII-2-b. Quand l’objet est créé à la volée sans être récupéré par une variable▲
2.
3.
4.
5.
6.
7.
8.
def fct(n):
print("in fct:", n)
cTrace(n)
print("out fct:", n)
print(1) # Message témoin
fct("xxx") # Appel de la fonction
print(2) # Message témoin
Résultat :
2.
3.
4.
5.
6.
1
in fct: xxx
init: xxx
del: xxx
out fct: xxx
2
Explications : la différence se fait ici lors de l'instanciation de l'objet. Cette instance n'étant pas récupérée dans une variable, l'objet est alors immédiatement détruit juste après avoir été créé et ce, avant que la fonction ne se termine.
XXVII-3. Tracer le fonctionnement des paramètres par défaut▲
Création d’un objet spécifique avec affichage dans les méthodes __init__() et __del__() pour montrer le mécanisme de création des valeurs par défaut des paramètres d’une fonction.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
#!/usr/bin/env python3
# coding: utf-8
class cTrace:
def __init__(self): print("init: ", self)
def __del__(self): print("del: ", self)
# class cTrace
# Première définition de fonction avec un paramètre ayant l’objet comme valeur par défaut
def fct(n=cTrace()): print("fct:", n)
# Seconde définition de fonction qui va écraser la première
def fct(n=cTrace()): print("fct:", n)
print(1) # Message témoin
fct("essai") # Appel avec argument concret (donc pas d’argument par défaut)
print(2) # Message témoin
fct() # Appel avec argument par défaut
print(3) # Message témoin
… et au résultat :
Explications : le premier message « init » provient de l’objet cTrace() créé lorsque la fonction est définie. On a déjà là la démonstration que l’argument par défaut est créé à la définition de la fonction et non à son appel. Ensuite, le second message « init » provient d’un second objet cTrace() créé lorsque la fonction est de nouveau définie. À ce moment‑là, la première fonction disparait et avec elle les objets qu’elle a créés, d’où le premier message « del ». Viennent ensuite, le message témoin « 1 » puis le message en provenance de la fonction, laquelle affiche le paramètre reçu. Puis s’affichent le second message témoin « 2 » puis de nouveau le message en provenance de la fonction, laquelle affiche cette fois le paramètre par défaut (objet cTrace créé auparavant). Arrive enfin le troisième message témoin « 3 ». Puis le programme s’arrête détruisant alors la fonction fct ce qui détruit par ricochet son argument par défaut.
XXVII-4. Tracer le fonctionnement des fonctions incluses▲
Création d’un objet spécifique avec affichage dans les méthodes __init__() et __del__() pour montrer le mécanisme de création des fonctions incluses mis en œuvre lors de l’appel (et non de l’écriture) de la fonction parente.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
#!/usr/bin/env python3
# coding: utf-8
class cTrace:
def __init__(self): print("init: ", self)
def __del__(self): print("del: ", self)
# class cTrace
# Définition de la fonction parente
def fct():
# Fonction incluse avec un paramètre ayant l’objet comme valeur par défaut
def xxx(t=cTrace()): pass
print("fct")
# fct()
print(1) # Message témoin
fct() # Appel fonction
print(2) # Message témoin
fct() # Appel fonction
print(3) # Message témoin
…et au résultat :
Explications : affichage tout d’abord du message témoin « 1 » puis d’un premier « init » provenant de l’objet cTrace() créé lorsque l’appel de la fonction fct() ce qui entraine la création de la fonction xxx() (qui, elle, n’a pas besoin d’être appelée). Puis exécution du premier « del » lorsque la fonction xxx() est détruite et avec elle ses objets. Puis affichage du message témoin « 2 » puis appel de nouveau à la fonction fct() entrainant la création de la fonction xxx() et de son objet en tant que paramètre par défaut, puis nouvel appel de la fonction fct() entrainant la destruction de son contenu et donc de xxx() et de ses objets par ricochet puis enfin, affichage du message témoin « 3 ».
XXVII-5. Fonction « cmp_to_key() »▲
Reproduction de la fonction cmp_to_key() du module « functools » permettant de passer une fonction comme critère de tri à sorted().
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
# La fonction test va trier alphabétiquement chaque chaîne inversée ("abc" vu comme "cba")
def compare(x, y):
if x[::-1] < y[::-1]: return -1
if x[::-1] > y[::-1]: return 1
return 0
# La fonction cmp_to_key qui convertit une fonction en clef
# Pour cela, elle crée un objet interne contenant tous les opérateurs relationnels
# Chaque opérateur utilisera la fonction à convertir pour faire ses évaluations
# La fonction cmp_to_key n’a plus alors qu’à renvoyer l’objet
# De là, toute comparaison de deux éléments par sort() passera alors par l’objet
def my_cmp_to_key(fct):
class __cFct:
def __init__(self, k): self.__k=k
def __eq__(self, other): return fct(self.__k, other.__k) == 0
def __ne__(self, other): return fct(self.__k, other.__k) != 0
def __lt__(self, other): return fct(self.__k, other.__k) < 0
def __le__(self, other): return fct(self.__k, other.__k) <= 0
def __gt__(self, other): return fct(self.__k, other.__k) > 0
def __ge__(self, other): return fct(self.__k, other.__k) >= 0
return __cFct
>>> auteurs=("Hugo", "Racine", "Balzac")
>>> for x in sorted(auteurs, key=my_cmp_to_key(compare), reverse=True): print(x)
...
Hugo
Racine
Balzac
>>> from functools import cmp_to_key
>>> for x in sorted(auteurs, key=cmp_to_key(compare), reverse=True): print(x)
...
Hugo
Racine
Balzac
XXVII-6. Petit benchmark sur les différentes façon de copier une liste▲
Il a été vu lors du chapitre sur la copie des objets mutables, qu’une liste pouvait être copiée soit par l’opérateur Python slice ([:]), soit par la création d’une nouvelle liste à travers le mot‑clef list(), soit par la méthode copy() de l’objet liste lui‑même. Y a‑t‑il une différence entre ces trois façon de faire ?
Voici un petit exemple de benchmark des trois méthodes…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
#!/usr/bin/env python3
# coding: utf-8
import random
import timeit
from functools import partial
# Initialisation random
random.seed()
# Les fonctions à tester
fct={
"slice" : lambda l: l[:],
"copy" : lambda l: l.copy(),
"list" : lambda l: list(l),
}
# Les données à traiter
data=list(range(10_000))
# Le nombre de répétitions (les moyennes se feront sur cette valeur)
repeat=20
# Appel des fonctions dans un ordre aléatoire et affichage du chrono
print("start=%d, repeat=%d" % (len(data), repeat))
for (k, v) in random.sample(fct.items(), len(fct)):
t=timeit.Timer(partial(v, data)).repeat(repeat=repeat, number=500_000)
print("%s: min=%f, max=%f, avg=%f" % (k, min(t), max(t), sum(t)/len(t)))
# for
Et (au bout d’environ 12 minutes) un résultat…
À ce qu’il semble, c’est le cast list() qui serait le plus prometteur…
XXVII-7. Petit benchmark sur le append() face aux listes en compréhension▲
Il a été vu lors du chapitre sur les listes en comprehension, qu’en dehors d’une écriture plus concise, son avantage principal était d’éviter la méthode list.append() présentée comme globalement très consommatrice.
Voici un petit exemple de benchmark permettant de comparer les deux écritures…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
#!/usr/bin/env python3
# coding: utf-8
import random
import timeit
from functools import partial
# Initialisation random
random.seed()
# La fonction qui crée la liste par append()
def fct_append(l):
res=list()
for x in l: res.append(x)
# fct_append()
# La fonction qui passe par les listes en compréhension
def fct_comprehension(l): return list(x for x in l)
# Les fonctions à tester (on évitera la lambda pour être le plus impartial possible)
fct={
"append" : fct_append,
"comprehension" : fct_comprehension,
}
# Les données à traiter
data=tuple(range(50_000_000))
# Le nombre de répétitions (les moyennes se feront sur cette valeur)
repeat=10
# Appel des fonctions dans un ordre aléatoire et affichage du chrono
print("start=%d, repeat=%d" % (len(data), repeat))
for (k, v) in random.sample(fct.items(), len(fct)):
t=timeit.Timer(partial(v, data)).repeat(repeat=repeat, number=20)
print("%s: min=%f, max=%f, avg=%f" % (k, min(t), max(t), sum(t)/len(t)))
# for
Et (au bout d’environ 10 minutes) un résultat…
Là en revanche, les écarts sont clairs et avérés. Près de 18 % plus rapide avec l’écriture en compréhension (ou près de 22 % plus lent avec la méthode list.append())…
XXVII-8. Benchmark sur le map() face aux listes en compréhension▲
Il a été vu lors du chapitre sur les compléments des tuples, listes, ensembles et dictionnaires, qu’il existait une fonction map() permettant de faire traiter tout un itérable par une fonction, la fonction étant alors appliquée sur chaque élément de l’itérable.
Guido Van Rossum, le concepteur de Python, a dit qu’il n’aimait pas la fonction map(), car on peut facilement la remplacer par une liste en compréhension plus rapide et plus lisible. Et beaucoup de sites sur Internet ont repris ces deux arguments.
Sur la lisibilité d’une liste en compréhension face à la fonction map(), cela reste uniquement une affaire de goûts dont on ne peut pas discuter. En revanche, on peut parfaitement discuter de la rapidité.
Toutefois, n’oublions pas qu’on peut faire traiter l’itérable (on dira « liste » pour simplifier) par une fonction lambda ou bien par une fonction déjà existante. De plus, il ne faut pas oublier que la fonction map() peut traiter plusieurs listes en parallèle…
XXVII-8-a. Liste unique▲
Voici un petit exemple de benchmark permettant de comparer les deux écritures quand on traite une liste unique…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
#!/usr/bin/env python3
# coding: utf-8
import random
import timeit
from functools import partial
# Initialisation random
random.seed()
# Une fonction somme
def somme(x): return x
# Les données à traiter
data=list(range(10_000))
# Les fonctions à tester
fct={
"with_lambda" : {
"map" : lambda l: list(map(lambda x: x, l)),
"comprehension" : lambda l: [x for x in l],
},
"with function" : {
"map" : lambda l: list(map(somme, l)),
"comprehension" : lambda l: [somme(x) for x in l],
},
}
# Le nombre de répétitions (les moyennes se feront sur cette valeur)
repeat=20
# Appel des groupes
print("start=%d, repeat=%d" % (len(data), repeat))
for (k, v) in fct.items():
# Appel des fonctions du groupe dans un ordre aléatoire et affichage du chrono
for (kk, vv) in random.sample(v.items(), len(v)):
t=timeit.Timer(partial(vv, data)).repeat(repeat=20, number=10_000)
print("%s (%s): min=%f, max=%f, avg=%f" % (kk, k, min(t), max(t), sum(t)/len(t)))
# for
# for
Et (au bout d’environ 4 minutes) un résultat…
XXVII-8-b. Deux listes▲
Voici un petit exemple de benchmark permettant de comparer les deux écritures quand on traite deux listes en parallèle. Mais dans le cas de la compréhension, il faudra associer les listes au travers de la fonction zip()…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
#!/usr/bin/env python3
# coding: utf-8
import random
import timeit
from functools import partial
# Initialisation random
random.seed()
# Une fonction somme
def somme(x, y): return x+y
# Les données à traiter
data=list(range(10_000))
# Les fonctions à tester
fct={
"with_lambda" : {
"map" : lambda l: list(map(lambda x, y: x+y, l, l)),
"comprehension" : lambda l: [x+y for (x, y) in zip(l, l)],
},
"with function" : {
"map" : lambda l: list(map(somme, l, l)),
"comprehension" : lambda l: [somme(x, y) for (x, y) in zip(l, l)],
},
}
# Le nombre de répétitions (les moyennes se feront sur cette valeur)
repeat=20
# Appel des groupes
print("start=%d, repeat=%d" % (len(data), repeat))
for (k, v) in fct.items():
# Appel des fonctions du groupe dans un ordre aléatoire et affichage du chrono
for (kk, vv) in random.sample(v.items(), len(v)):
t=timeit.Timer(partial(vv, data)).repeat(repeat=20, number=10_000)
print("%s (%s): min=%f, max=%f, avg=%f" % (kk, k, min(t), max(t), sum(t)/len(t)))
# for
# for
Et (au bout d’environ 8 minutes) un résultat…
XXVII-8-c. Trois listes▲
Voici un petit exemple de benchmark permettant de comparer les deux écritures quand on traite trois listes en parallèle (avec bien entendu toujours cette association au travers de la fonction zip())"…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
#!/usr/bin/env python3
# coding: utf-8
import random
import timeit
from functools import partial
# Initialisation random
random.seed()
# Une fonction somme
def somme(x, y, z): return x+y+z
# Les données à traiter
data=list(range(10_000))
# Les fonctions à tester
fct={
"with_lambda" : {
"map" : lambda l: list(map(lambda x, y, z: x+y+z, l, l, l)),
"comprehension" : lambda l: [x+y+z for (x, y, z) in zip(l, l, l)],
},
"with function" : {
"map" : lambda l: list(map(somme, l, l, l)),
"comprehension" : lambda l: [somme(x, y, z) for (x, y, z) in zip(l, l, l)],
},
}
# Le nombre de répétitions (les moyennes se feront sur cette valeur)
repeat=20
# Appel des groupes
print("start=%d, repeat=%d" % (len(data), repeat))
for (k, v) in fct.items():
# Appel des fonctions du groupe dans un ordre aléatoire et affichage du chrono
for (kk, vv) in random.sample(v.items(), len(v)):
t=timeit.Timer(partial(vv, data)).repeat(repeat=20, number=10_000)
print("%s (%s): min=%f, max=%f, avg=%f" % (kk, k, min(t), max(t), sum(t)/len(t)))
# for
# for
Et (au bout d’environ 12 minutes) un résultat…
XXVII-8-d. Conclusion▲
Il semble que l’utilisation de la fonction map() sera plus rapide d’environ 40 % face à la liste en compréhension quand le traitement se fait via une fonction déjà existante ; en opposition au traitement fait par une lambda dans lequel là la liste en compréhension sera plus rapide d’environ 70 %. Peut‑être (juste une hypothèse) parce que dans la fonction map(), la lambda est redéfinie à chaque itération…
Cependant, quand le traitement s’intensifie pour devoir opérer sur plusieurs listes, l’avantage de la liste en compréhension dans le cadre de la lambda diminue assez drastiquement (il passe à environ 30 % pour deux listes et à environ 20 % pour trois listes) tandis que l’avantage du map() utilisant une fonction déjà existante diminue moins (il passe à environ 30 % pour deux listes et reste à cette valeur pour trois listes). Probablement cette perte drastique de performances dans l’écriture en compréhension est‑elle due à l’introduction de la fonction zip() nécessaire à la jointure des listes.
Toutefois, n’oublions pas aussi l’avantage de la fonction map() en cas de listes de taille inégale. Dans ce cas elle s’adapte automatiquement à la liste la plus longue en complétant les listes plus courtes par autant de None que nécessaire. L’adaptation de ce fonctionnement dans une écriture en compréhension sera plus ardue et nuira certainement à la lisibilité de l’ensemble sans même parler des baisses de performances que cela pourra entraîner.


