comprehension-lists – Sam & Max http://sametmax.com Du code, du cul Thu, 05 Sep 2019 08:22:03 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Introduction au currying http://sametmax.com/introduction-au-currying/ http://sametmax.com/introduction-au-currying/#comments Fri, 12 Dec 2014 19:37:00 +0000 http://sametmax.com/?p=12693 Le currying (ou Curryfication pour les frencofans) est le nom donné à une technique de programmation qui consiste à créer une fonction à partir d’une autre fonction et d’une liste partielle de paramètres destinés à celle-ci. On retrouve massivement cette technique en programmation fonctionnelle puisqu’elle permet de créer une fonction pure à partir d’une autre fonction pure. C’est une forme de réutilisabilité de code.

La forme la plus simple de currying est de réécrire une fonction appelant l’autre. Par exemple, soit une fonction pour multiplier tous les éléments d’un itérable :

def multiply(iterable, number):
    """ Multiplie tous les éléments d'un itérable par un nombre.

        Exemple :

            >>> list(multiply([1, 2, 3], 2))
            [2, 4, 6]
    """
    return (x * number for x in iterable)

On peut ensuite créer une fonction qui multipliera par 2 tous les éléments d’un itérable :

def doubled(iterable):
    """ Multiplie tous les éléments d'un itérable par un 2.

        Exemple :

            >>> list(doubled([1, 2, 3]))
            [2, 4, 6]
    """
    return multiply(iterable, 2)

C’est une forme de currying. On créé une fonction qui fait ce que fait une autre fonction, mais avec des arguments par défaut.

Python possède une fonction pour faire ça automatiquement avec n’importe quelle fonction :

>>> from functools import partial 
>>> tripled = partial(multiply, number=3) # on curryfie ici
>>> list(tripled([1, 2, 3])) # nouvelle fonction avec un seul argument
[3, 6, 9]

Cela marche car, je vous le rappelle, les fonctions sont des objets en Python. On peut mettre une fonction (je ne parle pas de son résultat) dans une variable, passer une fonction en paramètre ou retourner une fonction dans une autre fonction. Les fonctions sont manipulables.

Il n’est pas rare d’utiliser les fonctions anonymes comme outils curryfication. En Python, on ferait ça avec une lambda :

>>> tripled = lambda x: multiple(x, 3) 
>>> list(tripled([1, 2, 3]))
[3, 6, 9]

Certains outils, comme Ramda en Javascript, vont plus loin, et exposent des fonctions qui se curryfient automatiquement.

Pour ce faire, il faut inverser l’ordre qu’on mettrait intuitivement aux arguments dans la déclaration d’une fonction :

# au lieu de multiply(iterable, number), on a :
def multiply(number, iterable=None):
    # Si on a pas d'itérable passé, on curryfie
    if iterable is None:
        return partial(multiply, number=number)
    return (x * number for x in iterable)

Ainsi :

>>> list(multiply(2, [1, 2, 3])) # pas de currying
[2, 4, 6]
>>> quintuple = multiply(5) # currying automatique
>>> list(quintuple([1, 2, 3]))
[5, 10, 15]

L’intérêt de ce style, c’est qu’on peut composer des traitements à partir de plusieurs sous traitements, presque déclarativement :

def remove(filter, iterable=None):
    """ Retire tous les éléments d'un itérable correspondant au filtre.

        Exemple :

            >>> list(remove(lambda x: x >= 4, [1, 2, 3, 4, 5]))
            [1, 2, 3]
    """
    if iterable is None:
        return partial(remove, filter)

    return (x for x in iterable if not filter(x))

>>> smalls = remove(lambda x: x >= 4)
>>> list(smalls(tripled([0, 1, 2, 3, 4]))) # le traitement est auto descriptif
[0, 3]

Néanmoins, il faut savoir que ce style n’est pas pythonique. En effet, en Python on préférera généralement utiliser des suites suite de générateurs. Soit par intention, soit via yield.

Notre exemple serait alors :

>>> tripled = (x * 3 for x in [0, 1, 2, 3, 4])
>>> smalls = (x for x in tripled if x <= 4)
>>> list(smalls)
[0, 3]

De plus, cette technique suppose qu’on ne profitera pas de certaines fonctionnalités, comme les paramètres par défaut des fonctions Python.

C’est toutefois une bonne chose à connaître. C’est occasionnellement utile en Python et peut produire des solutions très élégantes. C’est également une bonne chose à comprendre pour aborder d’autres langages plus fonctionnels qui les utilisent bien plus comme le Javascript, le Lisp, ou carrément le Haskell.

]]>
http://sametmax.com/introduction-au-currying/feed/ 3 12693
5 choses à apprendre en priorité en Python http://sametmax.com/5-choses-a-apprendre-en-priorite-en-python/ http://sametmax.com/5-choses-a-apprendre-en-priorite-en-python/#comments Sun, 22 Dec 2013 08:57:17 +0000 http://sametmax.com/?p=8376 Quand on apprend un nouveau langage de programmation, on apprend d’abord les bases. Et pour la plupart des langages, elles sont communes : déclarer une variable, faire des conditions et des boucles, faire des fonctions, importer un code d’un autre fichier, etc.

Ce qui va différencier le moment où vous savez programmer dans CE langage, ce sont des notions qui lui sont spécifiques et que vous commencez à maitriser.

Voici 5 notions spécifiques au langage qu’il faut apprendre en priorité si vous voulez pouvoir dire “je code en Python” :

Pip

Pip est la moyen le plus utilisé d’installer une bibliothèque externe dans l’environnement Python. Dès qu’on veut faire un projet sérieux, on en a besoin. Tellement qu’il va en fait être inclus par défaut dans Python 3.4.

Lire l’article sur pip.

Virtualenv

Virtualenv permet d’isoler plusieurs installations de Python. A partir du moment où l’on travaille sur plusieurs projets en même temps, il devient vite indispensable. Mais personnellement, je l’utilise même quand je n’ai qu’un projet installé sur une machine car il me permet de le séparer du setup Python du système et d’utiliser des hooks.

Un outil qui a été ajouté dans la lib standard en Python 3.3. J’apprécie que le pragmatisme de l’évolution de Python qui intègre petit à petit les projets qui se sont révélés les outils de facto dans la communauté.

Lire l’article sur virtualenv.

Les listes en intention

J’ai envie de dire l’itération en générale, mais c’est un très vaste sujet, et il est couvert en grande partie par les 3 derniers points.

La liste en intention, ou liste en compréhension, est une manière de boucler sur un itérable (souvent une liste), avec optionellement un filtre, afin de produire une nouvelle liste. En une ligne.

C’est stylistiquement la marque de fabrique de Python (même si c’est piqué à Haskell). C’est également ce qui le rend aussi expressif. On peut presque coder tout un programme en déclaratif avec des enchainements de listes en intention.

C’est beau, propre, efficace et court. IN-DIS-PEN-SA-BLE.

Lire l’article sur les listes en intention.

L’unpacking

L’unpacking est une autre fonctionalité typiquement pythonienne qui permet de prendre un itérable (souvent un tuple), et de mettre ses éléments dans des variables d’une traite.

Cela permet d’augmenter drastiquement la lisibilité des programmes.

Lire les articles sur l’unpacking.

Les générateurs

Les générateurs permettent non seulement un énorme gain en performance, mais en plus ils autorisent le traitement itératif de flux de données dont on ne connait pas la taille en avance, voire de taille infinie. Si vous utilisez des expressions génératrices, vous pourrez le faire en déclaratif. Si vous utilisez yield, vous pourrez cacher un algorithme complet derrière une simple boucle for.

Lire l’article sur yield.

Le reste ?

Tout le reste, c’est du détail. Les décorateurs, la POO, l’opérateur with, les métaclasses, les astuces magiques pour faire ceci ou cela. C’est bien, mais ça peut attendre. Ce sont ces 5 notions, qui, bien utilisées, feront d’un programmeur un dev Python.

]]>
http://sametmax.com/5-choses-a-apprendre-en-priorite-en-python/feed/ 9 8376
C’est pour des trucs comme ça que j’adore Python… http://sametmax.com/cest-pour-des-trucs-comme-ca-que-jadore-python/ http://sametmax.com/cest-pour-des-trucs-comme-ca-que-jadore-python/#comments Tue, 26 Nov 2013 06:43:31 +0000 http://sametmax.com/?p=8098 La manipulation de structures de données est fantastique dans ce langage.

J’avais un problème du genre :

“soit des personnes dans une file ayant un rang supposé…””

persons = [
    ('Nadine', 3),
    ('Tom', 2),
    ('Christophe', 7),
    ('Cloclo', 1),
    ('Lily', 5),
    ('Georges', 7),
    ('Paul', 6),
    ('Bérénice', 4),
    ('Ursula', 10),
    ('Lea', 15)
]

“…trouver, parmi les 5 premières, celles qui sont effectivement à leur place”

Vous connaissez mon amour immodéré pour les listes en intention en Python :

>>> [name for i, (name, rank) in enumerate(persons[:5]) if (i + 1) == rank]
[u'Tom', u'Lily']

Snif. j’en pleurerais presque.

]]>
http://sametmax.com/cest-pour-des-trucs-comme-ca-que-jadore-python/feed/ 13 8098
map(), filter() et reduce () ? http://sametmax.com/map-filter-et-reduce/ http://sametmax.com/map-filter-et-reduce/#comments Thu, 14 Nov 2013 09:44:58 +0000 http://sametmax.com/?p=7791 map(), filter() et reduce() sont des fonctions de traitement d'itérables typiques de la programmation fonctionnelle, qui ont été marquées comme à retirer des builtins pour Python 3. Finalement, seule reduce() sera déplacée dans le module functools pour Python 3.]]> map(), filter() et reduce() sont des fonctions de traitement d’itérables typiques de la programmation fonctionnelle, qui ont été marquées comme à retirer des builtins pour Python 3. Finalement, seule reduce() sera déplacée dans le module functools pour Python 3.

Les opérations que font ces fonctions sont typiquement quelque chose que l’ont peut faire sans elles, et nous allons les passer en revue pour voir dans quels cas elles sont pertinentes, dans quel cas une alternative est meilleure. L’alternative étant, dans 90% des cas, une liste en intention.

filter()

filter() prend une fonction en paramètre, souvent une lambda, comme ses deux soeurs puisqu’on est dans le paradigme fonctionnel. Elle doit renvoyer True si on garde un élément, et False sinon.

L’usage typique est celui-ci :

ages = range(30)
majeurs = filter(lambda x: x > 18, ages)
print(majeurs)
## [19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]

Typiquement, ce code peut être remplacé par une liste en intention dans le pur style Python :

majeurs = [a for a in ages if a > 18]

Le code est plus court, et fait usage d’une des fonctionalités les plus importantes du langage. On peut en plus ajouter une transformation à a facilement si on le désire, au lieu de devoir coller un map() derrière.

filter() est vraiment la moins utile des 3, et sera une question de style, surtout pour les nostalgiques de Lisp. Je répète souvent que quand on code avec un autre langage, on doit essayer de se tenir au style du nouveau et pas faire un mix avec ses anciennes habitudes. Quand je code en Java, je fais des getter et setter, même si j’ai horreur de ça.

Il existe quand même UNE astuce pour laquelle filter est utile : garder uniquement les éléments vrais d’une liste.

Typiquement :

l = [1, 0, None, [], True]
print filter(bool, l)
[1, True]

Ca marche si la fonction pre-existe et qu’on a pas à faire une lambda, mais c’est vraiment le seul usage potable. Un peu plus court qu’une liste en intention.

map()

Si filter() est l’équivalent de la partie de droite d’une liste en intention, map() est l’équivalent de la partie de gauche. La fonction passée retourne un résultat qui permet de transformer la liste.

Typiquement :

memes = ["It's over 9000 !", "All your base are belong to us."]
print(map(unicode.upper, memes))

Ce qui peut se traduire par :

print(s.upper() for s in memes)

map() est un peu plus utile, dans le sens où sa syntaxe peut être plus concise dans certains cas, comme le casting de types. Par exemple si je reçois une heure sous forme de string :

h, m, s = map(int, '8:19:22'.split(':'))

sera plus court et plus concis, et plus clair que :

h, m, s = (int(i) for i in '8:19:22'.split(':'))

Mais bon, la différence n’est pas non plus incroyable au point d’en faire une fonctionnalitéé clé. Je l’utilise de temps à autre par soucis de brièveté, mais vraiment c’est tout.

reduce()

reduce() est plus tordu. La fonction doit prendre deux paramètres en entrée, et retourner une valeur. Au premier appel, les deux premiers éléments de l’itérable sont passés en paramètres. Ensuite, le résultat de cet appel et l’élément suivant sont passés en paramètre, et ainsi de suite.

Vous n’avez rien pigé ? C’est normal. reduce() est parfaitement cryptique. Voici ce que ça donne en pratique :

def afficher(a, b):
    print("Entrée :", a, b)
    print("Sortie :", a + b)
    return a + b

res = reduce(afficher, range(10))
print("Résultat final", res)

## Entrée : 0 1
## Sortie : 1
## Entrée : 1 2
## Sortie : 3
## Entrée : 3 3
## Sortie : 6
## Entrée : 6 4
## Sortie : 10
## Entrée : 10 5
## Sortie : 15
## Entrée : 15 6
## Sortie : 21
## Entrée : 21 7
## Sortie : 28
## Entrée : 28 8
## Sortie : 36
## Entrée : 36 9
## Sortie : 45
## Résultat final 45

Vous allez me dire, à quoi ça sert ? Et bien par exemple à appliquer des opérateurs commutatifs, ici nous l’avons fait avec +, nous avons fait la somme de tous les éléments retournés par range(10). La preuve :

print(sum(range(10)))
## 45

Il n’y a pas, en Python, de fonction équivalent à sum() pour la multiplication. Donc on ferait :

print(reduce(lambda a, b: a * b, range(1, 11)))
## 3628800

Ce qui multiplie tous les éléments entre eux. Comme l’ordre dans lequel les éléments sont multipliés n’a pas d’important (d’où le ‘commutatif’), ça fonctionne.

reduce() peut prendre un troisième paramètre, initial, qui sera la valeur passée en premier au premier appel de la fonction. Cela permet de travailler sur des calculs en cascade qui ne fonctionneraient sinon pas. Revenons à notre exemple de temps :

temps = map(int, '8:19:22'.split(':'))
print(reduce(lambda a, b: a * 60 + b, temps, 0))
## 29962

Ce qui peut se traduire par :

h, m, s = map(int, '8:19:22'.split(':'))
print(h * 3600 + m * 60 + s)
## 29962

Bien sûr, cette conversion ne fonctionnerait pas si le calcul était sur un itérable plus long. Mais une version itérative est facile à faire :

res = 0
for i in map(int, '8:19:22'.split(':')):
    res = res * 60 + i
print(res)
## 29962

Maintenant, autant les deux dernières versions sont faciles à comprendre, autant la première prend quelques secondes. Et c’est la raison pour laquelle reduce() a été retirée des builtins, pour encourager l’usage des alternatives. En effet, cette fonction donne toujours un résultat très peu lisible. Je cite et approuve Guido là dessus:

C’est en fait celle que je déteste le plus, car, à part pour quelques exemples impliquant + ou *, presque chaque fois que je vois un appel à reduce() avec une fonction non-triviale passée en argument, j’ai besoin de prendre un crayon et un papier pour faire le diagramme de ce qui est effectivement entrée dans la fonction avant que je comprenne ce qu’est supposé faire reduce(). Donc à mes yeux, l’application de reduce() est plutôt limitée à des opérateurs associatifs, et dans d’autres cas il est mieux d’écrire une boucle d’accumulation explicitement.

Graissage maison.

Bref, reduce() est dur à lire, et une boucle ne l’est pas. Écrivez 3 lignes de plus, ça ne va pas vous tuer. Relire votre one-liner dans un mois par contre…

Cette fonction a été beaucoup utilisée avec les opérateurs or et and pour savoir si tous les éléments étaient vrais au moins un élément vrai dans une liste :

tout_est_vrai = [1, 1, 1, 1]
certains_sont_vrais = [1, 0, 1, 0]
tout_est_faux = [0, 0, 0, 0]

# Retourne True si tout est vrai
print(bool(reduce(lambda a, b: a and b, tout_est_vrai)))
## True
print(bool(reduce(lambda a, b: a and b, certains_sont_vrais)))
## False
print(bool(reduce(lambda a, b: a and b, tout_est_faux)))
## False
 
# Retourne True si au moins un élément est vrai
print(bool(reduce(lambda a, b: a or b, tout_est_vrai)))
## True
print(bool(reduce(lambda a, b: a or b, certains_sont_vrais)))
## True
print(bool(reduce(lambda a, b: a or b, tout_est_faux)))
## False

Mais aujourd’hui, c’est parfaitement inutile puisque nous avons les fonctions built-in all() et any(), qui font ça en plus court et plus rapide :

# Retourne True si tout est vrai
print(all(tout_est_vrai))
## True
print(all(certains_sont_vrais))
## False
print(all(tout_est_faux))
## False
 
# Retourne True si au moins un élément est vrai
print(any(tout_est_vrai))
## True
print(any(certains_sont_vrais))
## True
print(any(tout_est_faux))
## False

Petite astuce finale

Souvenez-vous également que les fonctions Python peuvent être déclarées n’importe où à la volée, même dans une autre fonction, une classe, une méthode, un context manager, etc. Or une fonction peut retourner un générateur grâce à yield, ce qui vous permet de déclarer des gros bouts de logique, et de les plugger dans votre process itérative a posteriori :

def traitement_complexe(iterable):
    for x in iterable:
        if x not in (1, 3, 7) and x % 2 != 0:
            if x + x < 13 :
                yield x
            else: 
                yield x - 2

print("-".join(map(str, traitement_complexe(range(20)))))
## 5-7-9-11-13-15-17
]]>
http://sametmax.com/map-filter-et-reduce/feed/ 22 7791
Les listes en intension VS map() en Python http://sametmax.com/les-listes-en-intentions-vs-map-en-python/ http://sametmax.com/les-listes-en-intentions-vs-map-en-python/#comments Thu, 04 Oct 2012 13:26:27 +0000 http://sametmax.com/?p=2452 map() et sont souvent plus à l'aise avec elle qu'avec les listes en intention en Python. Les deux font pourtant la même chose, tant et si bien que Python 3 voit map() retiré de ses built-in.]]> sLes adeptes de la programmation fonctionnelle connaissent bien le principe de la fonction map() et sont souvent plus à l’aise avec elle qu’avec les listes en intension en Python.

Les deux font pourtant la même chose, tant et si bien que Python 3 voit map() retiré de ses built-in. Ouuuups. Dérapage un-con-trollé de Sam.

C’est plutôt une nouvelle chose car dans un language orienté objet comme Python, il y a une subtile différence entre map() et les comprehension lists. Quand on utilise une méthode plutôt qu’une fonction, map() tue le duck tuping:

>>> l = [u'      Le choix dans la date     ', '     Les nouilles cuisent au jus de canne     ']
>>> map(str.strip, l)
Traceback (most recent call last):
  File "", line 1, in 
    map(str.strip, l)
TypeError: descriptor 'strip' requires a 'str' object but received a 'unicode'

>>> map(unicode.strip, l)
Traceback (most recent call last):
  File "", line 1, in 
    map(unicode.strip, l)
TypeError: descriptor 'strip' requires a 'unicode' object but received a 'str'

>>> [x.strip() for x in l]
[u'Le choix dans la date', 'Les nouilles cuisent au jus de canne']

Notez que je vous recomande d’éviter à tout prix de mélanger des chaînes encodées, et des chaînes unicode, de toute façon. J’utilise ceci uniquement parce que c’est un cas simple à comprendre.

Mais ici, le problème peut être résumé ainsi: nous avons une collection hétérogène d’objets (qui pourraient être de n’importe quels autres types, il y a des cas beaucoup plus légitimes), et nous appliquons la méthode de l’objet. Qui ne correspond pas forcément à son type.

Avoir des types différents avec la même interface est courant en Python pour profiter du duck typing, donc ce genre de chose n’est pas un edge case.

La solution est bien entendu de wrapper l’appel dans une fonction anonyme:

>>> map(lambda x: x.strip(), l)
[u'Le choix dans la date', 'Les nouilles cuisent au jus de canne']

Mais à ce stade, [x.strip() for x in l] est beaucoup plus lisible et rapide. Sans compter qu’il peut être transformé en générateur juste en changeant les [] en ().

]]>
http://sametmax.com/les-listes-en-intentions-vs-map-en-python/feed/ 4 2452
Astuces Python en vrac (bis) http://sametmax.com/astuces-python-en-vrac-bis/ http://sametmax.com/astuces-python-en-vrac-bis/#comments Sun, 19 Aug 2012 16:00:37 +0000 http://sametmax.com/?p=1794 quand on a pas d'inspiration.]]> Le genre d’article qu’on fait quand on a pas d’inspiration.

Denester une list

On peut le faire avec des listes en intention imbriquées. Ou on peut utiliser cette astuce:

>>> lst = [(1,2,3), (4, 5,6)]
>>> sum(lst, ())
(1, 2, 3, 4, 5, 6)

C’est pas très performant, mais c’est très court.

deepcopy de structures de données complexes

Si vous avez des listes des dictionnaires et de tuples, faire des copies, ne peut se faire avec le signe égal, car on copie juste la référence. Le module copy permet de faire une vrai copie de la liste, mais il existe une alternative toute bête:

>>> import pickle
>>> deepcopy = lambda x: pickle.loads(pickle.dumps(x))
>>> deepcopy([[1, 2, 3], [1, 2, 3]])
[[1, 2, 3], [1, 2, 3]]

Initialiser rapidement une matrice

>>> make_matrix = lambda cols, rows: [x[:] for x in [[0] * cols] * rows]
>>> make_matrix(4, 5)
[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]

Ici toute l’astuce est l’utilisation de l’opérateur splat (*) qui multiplie aussi les listes. Mais pour que cela soit un succès, on initialise la première liste avec 0 qui est immutable, et on copie chaque liste obtenue avec x[:] pour ne pas multiplier les références. On obtient ainsi de vraies copies, et non des duplications de références.

Évidement si vous cherchez la performance, utilisez plutôt NumPy.

Plusieurs print sur une seule ligne

Print insère toujours un saut de ligne, et pour écrire sur la même ligne avec plusieurs appels, on doit faire sys.stdout.write. Sauf si on utilise une virgule:

print "bib",
print "bip"

Va afficher bip bip.

Tout est comparable

Il y a pas que les chiffres qui sont comparables entre eux, tous les objets le sont. Ca n’a pas toujours du sens:

>>> True > False
True
>>> True < False
False

Mais c'est utile dans certains cas, la comparaison des mots se fait par ordre alphabétique:

>>> "a" > "b"
False
>>> "abricot" < "zoophile"
True
]]>
http://sametmax.com/astuces-python-en-vrac-bis/feed/ 10 1794
Python love: les listes en intension (partie 2) http://sametmax.com/python-love-les-listes-en-intention-partie-2/ http://sametmax.com/python-love-les-listes-en-intention-partie-2/#comments Wed, 15 Feb 2012 19:30:14 +0000 http://sametmax.com/?p=154 première partie, nous avons vu les bases des listes en intention. Mais elles ont encore beaucoup de choses à offrir. Même si vous les utilisez depuis quelques temps, lisez la suite, vous pourriez bien apprendre quelque chose.]]> Cet article a été mis à jour et contient maintenant du code Python en version 3

En première partie, nous avons vu les bases des listes en intension. Mais elles ont encore beaucoup de choses à offrir. Même si vous les utilisez depuis quelques temps, lisez la suite, vous pourriez bien apprendre quelque chose.

Les expressions génératrices

Il y a plus encore !

Quand vous faites ceci:

>>> nombres = [sum(range(nombre)) for nombre in range(0, 10, 2)]
>>> for nombre in nombres:
...     print(nombre)
...
0
1
6
15
28

La liste nombres est créée en mémoire.

Si la liste est petite, ce n’est pas un problème. Mais si la liste est grande, par exemple si c’est un fichier complet, cela peut devenir très consommateur de RAM.

Il existe un moyen de pallier à cela:

>>> nombres = (sum(range(nombre)) for nombre in range(0, 10, 2))
>>> for nombre in nombres:
...     print(nombre)
...
0
1
6
15
28

Vous ne voyez pas la différence ? Regardez de plus près la première ligne: les [] ont été remplacés par des parenthèses. Tout le reste de la syntaxe est la même.

Ce petit détail change absolument tout:

>>> [sum(range(nombre)) for nombre in range(0, 10, 2)]
[0, 1, 6, 15, 28]
>>> (sum(range(nombre)) for nombre in range(0, 10, 2))
 at 0x7f07bac94be0>

[] crée une liste.
() crée un générateur.

Un générateur ressemble beaucoup à une liste : c’est un itérable, donc on peut l’utiliser de la même manière dans une boucle for. La différence principale est que le générateur ne peut être lu qu’une seule fois. Si vous bouclez dessus une seconde fois, il sera vide:

>>> nombres = (sum(range(nombre)) for nombre in range(0, 10, 2))
>>> for nombre in nombres:
...     print(nombre)
...
0
1
6
15
28
>>> for nombre in nombres: # ceci n'affiche rien !
...     print(nombre)
...

La raison à cela est que le générateur ne contient pas toutes les valeurs de la liste. Il les génère.

Il calcule chaque valeur une à une, à la volée, quand la boucle for lui demande. Il calcule la première valeur, puis l’oublie, puis la deuxième, puis l’oublie, etc. Jusqu’à la dernière.

Cela signifie qu’on ne peut pas demander à un générateur un élément à un index en particulier:

>>> liste = [sum(range(nombre)) for nombre in range(0, 10, 2)]
>>> liste[0] # donne moi l'élément à l'index 0
0
>>> generateur = (sum(range(nombre)) for nombre in range(0, 10, 2))
>>> generateur[0]
Traceback (most recent call last):
File "", line 1, in 
TypeError: 'generator' object is not subscriptable

On utilise les générateurs partout où l’on a besoin d’une liste, mais qu’on ne souhaite pas stocker toute la liste en mémoire, et qu’on pense lire la liste une seule fois.

Il y a bien plus à dire sur les générateurs, qui est une notion très puissante et importante en Python. Si vous avez envie d’en savoir plus, par exemple si le mot clé yield vous intrigue, il y a un article dédié.

Il n’y a pas que les listes dans la vie

Python a une idée bien plus large de l’itération que les listes. En fait, tout objet “itérable” peut être utilisé dans une boucle for:

>>> une_liste = [1, 2, 3]
>>> for item in une_liste:
...     print(item)
...
1
2
3
>>> un_tuple = (1, 2, 3)
>>> for item in un_tuple:
...     print(item)
...
1
2
3
>>> un_dictionnaire = {'moi': 'tarzan', 'toi': 'jane'}
>>> for key in un_dictionnaire:
...     print(key)
...
moi
toi
>>> une_chaine_de_caracteres = "yabadabado"
>>> for lettre in une_chaine_de_caracteres:
...     print(lettre)
...
y
a
b
a
d
a
b
a
d
o
>>> un_fichier = open('fichier_de_test.txt')
>>> for line in un_fichier:
...     print(line)
...
b'ceci'
b'est'
b'un'
b'test'

Être itérable est un concept à part entière en Python.

Et il se trouve que les listes en intension acceptent n’importe quel itérable, pas juste les listes.

Tout comme les expressions génératrices.

Et plein de fonctions acceptent n’importe quel itérable. tuple(), liste(), join(), sum(), etc. Du coup on peut faire un tas de combos à faire se pâmer un fan de Tekken:

>>> [str(sum(range(int(nombre)))) for nombre in "123456789"]
['0', '1', '3', '6', '10', '15', '21', '28', '36']
>>> ', '.join([str(sum(range(int(nombre)))) for nombre in "123456789"])
'0, 1, 3, 6, 10, 15, 21, 28, 36'
>>> ', '.join(str(sum(range(int(nombre)))) for nombre in "123456789")
'0, 1, 3, 6, 10, 15, 21, 28, 36'

Notez sur la dernière ligne que l’on peut carrément supprimer les [], ne pas rajouter de parenthèses, et ça marche toujours. L’expression retourne un générateur, passé en paramètre à join().

On peut aussi chainer tout ça, les unes à la suite des autres et créer un gigantesque système de pipes à coup de générateurs, pluggable sur n’importe quel itérable.

Par exemple, vous voulez filtrer le contenu d’un fichier ?

>>> f = open('fichier_de_test.txt', encoding="ascii")
>>> lignes_non_vides = (line for line in f if line.strip())
>>> mot = "ni"
>>> lignes_qui_contiennent_un_mot = (l for l in lignes_non_vides if mot in l)
>>> lignes_qui_ne_finissent_pas_par_un_point = (l for l in lignes_qui_contiennent_un_mot if not l.endswith('.'))
>>> lignes_numerotees = enumerate(lignes_qui_ne_finissent_pas_par_un_point)

Afficher toutes les lignes non vides, du fichier fichier_de_test.txt, qui contiennent “ni” et numérotées:

>>> for numero, line in lignes_numerotees:
...     print("%s - %s" % (numero, line))
...
1 - Ni Dieu, ni maître
2 - Les chevaliers qui disent "ni"
3 - Con nichon ahhhhhhhh. Ca veut dire bonjour en japonais

À aucun moment l’intégralité du fichier n’est stocké en mémoire. Toutes les lignes sont traitées une par une.

Il y a encore énormément à dire sur les itérables mais je vais plutôt vous laisser digérer ce gros morceau.

Bonus 1: Les dictionnaires et les sets en intension

C’est le même principe pour les dictionnaires, on doit juste entourer de {} et séparer clé et valeur par :.

>>> {str(i): i for i in range(10)}
{'1': 1, '0': 0, '3': 3, '2': 2, '5': 5, '4': 4, '7': 7, '6': 6, '9': 9, '8': 8}

Idem pour les sets. Pour rappel, les sets sont des itérables non ordonnés, sans doublons :

>>> {1, 2, 3, 4, 4, 4, 4}
set([1, 2, 3, 4])

Ne confondez pas avec un dico : il n’y a que des valeurs, pas de clés.

On peut faire des sets en intension, la syntaxe ressemble à celle des dicos, mais sans le : :

>>> {i*i for i in range(10)}
set([0, 1, 4, 81, 64, 9, 16, 49, 25, 36])

Bonus 2: nested comprehension lists

Les listes en intension peuvent contenir des listes en intension de 2 manières.

La première est classique, et permet de créer des listes de listes (ou de générateurs):

>>> [[i*i for i in range(x)] for x in range(5)]
[[], [0], [0, 1], [0, 1, 4], [0, 1, 4, 9]]

La deuxième est beaucoup moins intuitive, permet de combiner plusieurs boucles. On peut par exemple aplatir une séquence de séquences. L’inverse en quelque sorte.

Ainsi, sous avez ceci:

[[], [0], [0, 1], [0, 1, 4], [0, 1, 4, 9]]

Vous voulez obtenir cela:

[0, 0, 1, 0, 1, 4, 0, 1, 4, 9]

Hop:

>>> liste_de_listes = [[], [0], [0, 1], [0, 1, 4], [0, 1, 4, 9]]
>>> [element_de_sousliste for sousliste in liste_de_listes for element_de_sousliste in sousliste]
[0, 0, 1, 0, 1, 4, 0, 1, 4, 9]

On peut aussi l’utiliser pour combiner des éléments de deux sources différentes.

Récupérer toutes les combinaisons de couleurs et formes :

>>> colors = ['red', 'green', 'yellow']
>>> forms = ['circle', 'square', 'triangle']
>>> [{'color': c, 'form': f} for c in colors for f in forms]
[{'color': 'red', 'form': 'circle'},
{'color': 'red', 'form': 'square'},
{'color': 'red', 'form': 'triangle'},
{'color': 'green', 'form': 'circle'},
{'color': 'green', 'form': 'square'},
{'color': 'green', 'form': 'triangle'},
{'color': 'yellow', 'form': 'circle'},
{'color': 'yellow', 'form': 'square'},
{'color': 'yellow', 'form': 'triangle'}]

La syntaxe n’est pas du tout intuitive, et pour se souvenir de l’ordre des choses, voici une astuce visuelle. Formez ainsi la liste dans votre esprit. Ceci:

[element_de_sousliste for sousliste in liste_de_listes for element_de_sousliste in sousliste]

Est en fait:

[element_de_sousliste # <= truc à mettre dans la nouvelle liste
    for sousliste in liste_de_listes # <= ordre normal d'une double boucle for
        for element_de_sousliste in sousliste]

Car une double boucle for serait ainsi faite:

nouvelle_liste = []
for sousliste in liste_de_listes:
    for element_de_sousliste in sousliste:
        nouvelle_liste.append(element_de_sousliste)

C'est bon, vous pouvez débranchez votre cerveau et retourner sur Youporn.

]]>
http://sametmax.com/python-love-les-listes-en-intention-partie-2/feed/ 25 154
Python love: les listes en intension (partie 1) http://sametmax.com/python-love-les-listes-en-intention-partie/ http://sametmax.com/python-love-les-listes-en-intention-partie/#comments Tue, 14 Feb 2012 23:26:08 +0000 http://sametmax.com/?p=142 Cet article a été mis à jour et contient maintenant du code Python en version 3

Dans le top 10 des raisons d’aimer Python se hisse aisément les listes en intension (avec un “s”), ou “comprehension lists” pour les gens branchés. Rappel du concept, et un petit tour complet de ce qu’on peut en faire. Les connaisseurs attendront le second article qui aborde des notions avancées, et contiendra quelques bonus.

Disclaimer: pour comprendre ce petit gros article, il faut être à l’aise avec la boucle for et les listes.

La boucle for

En Python, on itère beaucoup, c’est à dire qu’on applique très souvent un traitement à tous les éléments d’une séquence, un par un. Et pour ça il y a la boucle for:

>>> sequence = ["a", "b", "c"]
>>> for element in sequence:
...     print(element)
...
a
b
c

Et très souvent, on fait une nouvelle liste avec les éléments de la première liste, mais modifiés:

>>> sequence = ["a", "b", "c"]
>>> new_sequence = []
>>> for element in sequence:
...     new_sequence.append(element.upper())
...
>>> print(new_sequence)
['A', 'B', 'C']

Les listes en intension: la base

Cette opération – prendre une séquence, modifier les éléments un par un, et faire une autre liste avec – est très commune. Et comme pour à peu près tout ce qui est opération courante, Python possède une manière élégante de le faire plus vite:

>>> sequence = ["a", "b", "c"]
>>> new_sequence = []
>>> for element in sequence:
...     new_sequence.append(element.upper())
...
>>> print(new_sequence)
['A', 'B', 'C']

Devient:

>>> sequence = ["a", "b", "c"]
>>> new_sequence = [element.upper() for element in sequence]
>>> print(new_sequence)
['A', 'B', 'C']

Il n’y a aucun mystère, ce code fait exactement la même chose, mais:

>>> new_sequence = []
>>> for element in sequence:
...     new_sequence.append(element.upper())

Est réduit à:

>>> new_sequence = [element.upper() for element in sequence]

Ne cherchez pas un truc compliqué, c’est juste une question de syntaxe, ça fait la même chose, mais écrit différemment : à droite, la boucle, à gauche, ce qu’on veut mettre dans la liste finale.

Et c’est surtout beaucoup plus court.

Là où ça devient franchement sympa, c’est que l’on peut assigner le résultat d’une liste en intension directement à la variable originale:

>>> sequence = ["a", "b", "c"]
>>> new_sequence = [element.upper() for element in sequence]
>>> print(new_sequence)
['A', 'B', 'C']

Devient alors:

>>> sequence = ["a", "b", "c"]
>>> sequence = [element.upper() for element in sequence]
>>> print(sequence)
['A', 'B', 'C']

Et vous avez du coup un moyen très propre de transformer toute une liste.

Listes en intension avancées

On peut faire bien plus avec les listes en intension. Python est un langage dynamiquement typé, donc on peut transformer carrément le type de liste.

>>> sequence = [1, 2, 3]
>>> print([str(nombre) for nombre in sequence])
['1', '2', '3']

On peut aussi faire des opérations un peu plus complexes:

>>> sequence = [1, 2, 3]
>>> print(['a' * nombre for nombre in sequence])
['a', 'aa', 'aaa']

Et même construire des sequences imbriquées à la volée:

>>> sequence = [1, 2, 3]
>>> print(list(range(5))) # petit rappel de l'usage de la fonction range
[0, 1, 2, 3, 4]
>>> sequence = [(nombre, list(range(nombre))) for nombre in sequence]
>>> print(sequence)
[(1, [0]), (2, [0, 1]), (3, [0, 1, 2])]
>>> print(sequence[-1])
(3, [0, 1, 2])
>>> print(sequence[-1][0])
3
>>> print(sequence[-1][1])
[0, 1, 2]

La syntaxe [expression for element in sequence] autorise n’importe quelle expression, du coup on peut créer des listes très élaborées, en utilisant tous les opérateurs mathématiques, logiques, etc, et toutes les fonctions que l’on veut.

Filtrer avec les listes en intension

Une autre opération courante consiste à filtrer la liste plutôt que de la transformer:

>>> nombres = range(10)
>>> nombres_pairs = []
>>> for nombre in nombres:
...     if nombre % 2 == 0: # garder uniquement les nombres pairs
...         nombres_pairs.append(nombre)
...
>>> print(nombres)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> print(nombres_pairs)
[0, 2, 4, 6, 8]

Évidement Python a également une syntaxe plus courte pour cela. Il suffit de rajouter la condition à la fin:

>>> nombres = range(10)
>>> print([nombre for nombre in nombres if nombre % 2 == 0])
[0, 2, 4, 6, 8]

Toutes les expressions habituellement utilisables pour tester une condition sont également disponibles.

Bien sûr, rien ne vous empêche de filtrer ET de transformer la liste en même temps. En clair, un nouvel arrivant à Python fera ça:

>>> nombres = range(10)
>>> sommes = []
>>> for nombre in nombres:
...     if nombre % 2 == 0:
...         somme = 0
...         for i in range(nombre):
...             somme += i
...         sommes.append(somme)
...
>>> print(sommes)
[0, 1, 6, 15, 28]

Un codeur qui trouve ses marques fera ça:

>>> sommes = []
>>> for nombre in range(10):
...     if nombre % 2 == 0:
...         sommes.append(sum(range(nombre)))
...
>>> print(sommes)
[0, 1, 6, 15, 28]

Un pythoniste affranchi ira droit au but:

>>> print([sum(range(nombre)) for nombre in range(10) if nombre % 2 == 0])

Bon, en vérité il fera plutôt:

>>> [sum(range(nombre)) for nombre in range(0, 10, 2)]

Mais c’était pour l’exemple :-)

Les listes en intension ont encore plus à offrir, donc une fois que vous êtes bien familiarisés avec la notion, lisez le second article.

]]>
http://sametmax.com/python-love-les-listes-en-intention-partie/feed/ 28 142