Un tools d’itertour, ou l’inverse


Ah, ça faisait longtemps que je ne vous avais pas fait un petit article bien long. Ca manquait d’opportunité de partager de la musique. Allez hop !

Python est un langage avec un “for” penchant pour l’itération si je puis dire. Et ce n’est que justice qu’il ait donc un module dédié pour les opérations d’itération : itertools.

itertools est un peu impénétrable pour quelqu’un qui n’a pas l’habitude, alors je vais vous faire un petit tour du propriétaire.

Principes de base

Itertools a pour but de manipuler les itérables, et si vous voulez en savoir plus sur le concept d’itérabilité, je vous renvoie à l’article sur les trucmuchables. Pour faire court, les itérables sont tout ce sur quoi on peut appliquer une boucle for.

Le problème, c’est que tous les itérables ne se comportent pas de la même façon. En effet, certains ont une taille définie:

>>> len("abcd")
4
>>> len(range(3))
3

D’autres non:

>>> len(open('/etc/fstab'))
Traceback (most recent call last):
  File "<ipython-input-7-baed595d06b3>", line 1, in <module>
    len(open('/etc/fstab'))
TypeError: object of type '_io.TextIOWrapper' has no len()

Certains, se terminent:

>>> a = list(open('/etc/fstab'))
>>>

D’autres sont infinis:

>>> def infinite_generator():
...     while True:
...         yield 1
...
>>> list(infinite_generator) # bloque la VM jusqu'à crasher sur un MemoryError

Du coup, si vous voulez faire des opérations qui marchent sur tous les itérables, il faut écrire des algos spéciaux dit “paresseux” (en anglais, “lazy”), c’est à dire qui font le travail minimum. Ils se lancent à la dernière minute et ne lisent rien de plus que nécessaire.

Et c’est ce que contient itertools. Les fonctions de ce module marchent sur les fichiers, les générateurs, les sockets réseaux comme les listes, les tuples et les chaînes de caractères.

chain

Combine plusieurs itérables en un seul:

>>> from itertools import chain
>>> generator = chain("abc", range(3), [True, False, None])
>>> generator
<itertools.chain object at 0x7f2ea903d550>
>>> list(generator)
['a', 'b', 'c', 0, 1, 2, True, False, None]

Très utile pour boucler sur des itérables hétérogènes comme si c’était une seule et même structure. Chaîner des fichiers, des générateurs, etc.

islice

Comme la syntaxe de slicing [::]:

>>> carres_de_nombres_pairs = [x * x  for x in range(100) if x % 2 == 0]
>>> carres_de_nombres_pairs[3:11]
[36, 64, 100, 144, 196, 256, 324, 400]

Mais marche sur tous les itérables, même sur les générateurs:

>>> carres_de_nombres_pairs = (x * x  for x in range(100) if x % 2 == 0)
>>> carres_de_nombres_pairs
<generator object <genexpr> at 0x7f2eab171a98>
>>> from itertools import islice
>>> generator = islice(carres_de_nombres_pairs, 3, 11)
>>> generator
<itertools.islice object at 0x7f2eab1a26d8>
>>> list(generator)
[36, 64, 100, 144, 196, 256, 324, 400]

cycle

Boucler de manière infinie sur un itérable. Quand on arrive à la fin, on revient au début.

>>> generator = cycle('abc')
>>> generator
<itertools.cycle object at 0x7f2eab59a588>
>>> next(generator)
'a'
>>> next(generator)
'b'
>>> next(generator)
'c'
>>> next(generator)
'a'

C’est très pratique, mais assez dangereux donc faites des tests avant de l’utiliser. En effet si l’itérable génère les données à la volée, elles sont stockées dans un buffer, donc ça charge tout en mémoire. De plus, c’est une boucle infinie. Ne castez pas ça avec un tuple ou une liste :)

repeat

Prend n’importe quel élément, et le retourne encore et encore.

>>> generator = repeat("plop") # répète infiniment
>>> next(generator)
'plop'
>>> next(generator)
'plop'
>>> next(generator)
'plop'
>>> generator = repeat("plop", 2) # répète 2 fois
>>> for x in generator:
...     print(x)
...
plop
plop

On ne peut pas passer un callable, mais iter() le fait déjà. La signature normale de cette dernière prend un iterable, mais si on lui passe deux paramètres, un callable et un sentinel, on obtient un générateur qui va appeler le callable jusqu’à ce qu’il retourne une valeur égale au sentinel.

Exemple, vous voulez lire un fichier 50 caractères à la fois. On passe une fonction qui retourne les 50 caractères suivants (le callable) et une chaîne de caractères vide (le sentinel):

>>> f = open('/etc/fstab')
>>>
>>> def read_50_chars():
...     return f.read(50)
...
>>> generator = iter(read_50_chars, '') # lit 50 caractères à la fois jusqu'à tomber sur ''
>>> generator
<callable_iterator object at 0x7f2eab5abef0>
>>> next(generator)
'# /etc/fstab: static file system information.\n#\n# '
>>> next(generator)
"Use 'blkid' to print the universally unique identi"
>>> next(generator)
'fier for a\n# device; this may be used with UUID= a'

iter() est un built-in, pas dans itertools, mais ça aurait été con de pas en parler.

zip_longest

Comme zip(), mais au lieu de s’arrêter quand le premier itérable est fini:

>>> list(zip('abc', range(5)))
[('a', 0), ('b', 1), ('c', 2)]

On remplit les valeurs manquantes:

>>> list(zip_longest('abc', range(5)))
[('a', 0), ('b', 1), ('c', 2), (None, 3), (None, 4)]
>>> list(zip_longest('abc', range(5), fillvalue="Tada !"))
[('a', 0), ('b', 1), ('c', 2), ('Tada !', 3), ('Tada !', 4)]

starmap

Comme map(), mais applique l’opérateur splat sur les arguments avant.

Si map() ressemble (en mieux) à:

def map_en_moins_bien(callable, iterable):
    for x in iterable:
       yield callable(x)

starmap() fait:

def map_en_moins_bien(callable, iterable):
    for x in iterable:
       yield callable(*x)

Exemple:

>>> nombres = [(1, 2), (3, 4), (5, 6)]
>>> def ajouter(a, b):
...     return a + b
...
>>> from itertools import starmap
>>> list(starmap(ajouter, nombres))
[3, 7, 11]

Ces fonctions sont moins utilisées à cause des listes en intension. Mais il y a des trucs rigolos comme:

>>> list(starmap(print, zip(range(3), map(int, "123"))))
0 1
1 2
2 3
[None, None, None]

Ouais, vous allez pas utiliser ça tous les jours :)

filterfalse

Le contraire de filter(), garde les résultats négatifs au lieu des résultats positifs:

>>> list(filter(est_pair, range(10)))
[0, 2, 4, 6, 8]
>>> from itertools import filterfalse
>>> list(filterfalse(est_pair, range(10)))
[1, 3, 5, 7, 9]

Comme précédemment, aujourd’hui on utilise peu ces fonctions car on a les listes en intension. Mais certaines astuces sont sympas comme:

>>> list(filter(bool, [True, False, 1, 0, 'foo', '']))
[True, 1, 'foo']
>>> list(filterfalse(bool, [True, False, 1, 0, 'foo', '']))
[False, 0, '']

groupby

Ah, groupby(), la fonction la moins facile à comprendre. D’abord, personne ne lit la doc, et la doc dit clairement qu’il faut trier les éléments de l’itérable AVANT d’appliquer groupby, sinon les résultats sont incohérents.

Ensuite, groupby() peut prendre un callback pour un usage avancé, et toutes les fonctions qui prennent un callback sont plus dures à comprendre. Mais pour que ça ait du sens, il faut que le tri préalable s’applique sur le même callback. Personne ne pige jamais ça.

Enfin, groupby() ne renvoie pas les éléments directement, mais des objets “groupers”, qui sont eux mêmes des générateurs. Tout est bien fait pour embrouiller le chaland, surtout pour une fonction qu’on utilise pas souvent.

Pour comprendre groupby(), le mieux est de commencer par l’entrée et la sortie. Vous avez un truc comme ça:

>>> noms = ['Zebulon', 'Léo', 'Alice', 'Bob', 'Anaïs', 'Adam', 'Bernardo', 'Io']

Et vous voulez les grouper par un critère, un peu comme un GROUP BY en SQL. Ca peut être n’importe quoi:

  • La valeur de la première lettre.
  • La position de la première voyelle.
  • Deux catégories: ceux qui contiennent des caractères non ASCII et les autres.

Prenons un exemple simple, et groupons les par le nombre de lettres qu’ils contiennent. Ca donnerait quelque chose comme ça:

[(2, ('Io',)),
 (3, ('Léo', 'Bob')),
 (4, ('Adam',)),
 (5, ('Alice', 'Anaïs')),
 (7, ('Zebulon',)),
 (8, ('Bernardo',))]

Pour obtenir ce résultat avec groupby(), il faut d’abord trier l’itérable (ici notre liste) par nombre de lettres:

>>> noms.sort(key=len)
>>> noms
['Io', 'Léo', 'Bob', 'Adam', 'Alice', 'Anaïs', 'Zebulon', 'Bernardo']

Si vous ne vous souvenez plus comment fonctionne le tri en Python, particulièrement le paramètre key, allez vite lire l’article dédié du lien précédent. En effet le mécanisme est le même pour groupby() donc il faut comprendre ce fonctionnement de toute manière.

Ensuite on applique groupby(), avec la même fonction key:

>>> noms = groupby(noms, key=len)
>>> noms
<itertools.groupby object at 0x7f19c4461778>

A ce stade, on a un générateur qu’on peut parcourir, mais il ne contient pas le résultat tel que je vous l’ai montré. A la place, il yield des paires (group, grouper):

>>> next(noms)
(2, <itertools._grouper object at 0x7f19c48c0080>)

Le premier élément, le groupe, est ce que votre fonction key retourne, c’est à dire ce sur quoi vous vouliez grouper. Ici la taille du mot.

En second, un objet grouper, qui est un générateur yieldant les mots qui correspondent à ce groupe.

Donc pour vraiment voir le contenu de tout ça, il faut se taper une double boucle imbriquée:

>>> from itertools import groupby
>>> for groupe, groupeur in groupby(noms, key=len):
...     print(groupe, ":")
...     for mot in groupeur:
...         print('-', mot)
2 :
- Io
3 :
- Léo
- Bob
4 :
- Adam
5 :
- Alice
- Anaïs
7 :
- Zebulon
8 :
- Bernardo

La raison de ce manque d’intuitivité, c’est que théoriquement groupby() peut travailler de manière paresseuse et donc les groupeurs sont des générateurs qui traitent l’arrivée des données au fur et à mesure. Mais c’est rare d’avoir des données lazy qui arrivent déjà pré-ordonnées. Ensuite ça suppose que vous voulez lire ces données au fur et à mesure, mais généralement le jeu de données groupées n’est pas suffisamment grand pour que ça soit un vrai problème et on caste directement le groupe en tuple ou en liste.

Dans la pratique donc, groupby() fait parti de ces fonctions un peu overkill, car un usage typique ne bénéficiera pas du côté paresseux. En tout cas ça ne m’est jamais arrivé.

dropwhile

dropwhile() prend un callable et un itérable. Le callable est appelé sur chaque élément, et tant qu’il retourne quelque chose de vrai, l’élément est ignoré. C’est un peu tordu car ça demande d’écrire sa fonction de filtre à l’inverse de ce qu’on veut.

Par exemple, si vous voulez commencer à lire un fichier à partir de la première ligne qui commence par un commentaire, il faut que votre fonction retourne True… si la ligne ligne ne commence PAS par un commentaire.

>>> def ne_commence_pas_par_un_commentaire(element):
...     return not element.startswith('#')
...
>>> generator = dropwhile(ne_commence_pas_par_un_commentaire, open('/etc/fstab'))
>>> generator
<itertools.dropwhile object at 0x7f19c450d688>
>>> next(generator)
'# /etc/fstab: static file system information.\n'

dropwhile veut dire “ignorer tant que”. Donc ici on ignore tant que la ligne ne commence pas par un commentaire.

takewhile

takewhile() est le contraire de dropwhile(), on prend les éléments tant qu’une condition est vraie, et on s’arrête dès que la condition n’est plus vraie. On les utilise souvent en combinaison d’ailleurs, pour slicer les itérables en utilisant des conditions plutôt que des indexes.

Comme pour dropwhile, il faut penser à l’envers. Vous voulez vous arrêter quand un nombre dépasse 10000 ? Alors il faut faire une fonction qui retourne True tant que le nombre ne dépasse PAS 10000. Jusqu’ici j’ai utilisé des fonctions traditionnelles, mais bien entendu, une lambda marche aussi:

>>> nombres = (x * x for x in range(1000000000000))
>>> from itertools import takewhile
>>> generator = takewhile(lambda x: x < 10000, nombres)
>>> next(generator)
0
>>> next(generator)
1
>>> next(generator)
4
>>> list(generator)[-1]
9801

count

count() est une relique du passé, qui fait ce que fait range() maintenant, mais en moins bien. Vous pouvez l’ignorer.

tee

tee() est une fonction très intéressante qui duplique votre itérable en autant de clones que vous le souhaitez. Pour ce faire, tee() garde en mémoire les derniers éléments générés, ce qui fait que si vous lisez les clones les uns après les autres, ça n’a aucun intérêt. Autant caster en liste et lire la liste plusieurs fois.

Non, l’interêt de tee() est si vous lisez les clones en parallèle. Car là, tee() ne garde en mémoire que les éléments qui n’ont pas été lus par le dernier clone utilisé.

Par exemple, ceci est inutile:

>>> from itertools import tee
>>> clone1, clone2, clone3 = tee((x for x in range(100)), 3)
>>> list(clone1)[:3]
[0, 1, 2]
>>> list(clone2)[:3]
[0, 1, 2]

Par contre ceci économise pas mal de mémoire:

>>> clone1, clone2, clone3 = tee((x for x in range(100)), 3)
>>> next(clone1)
0
>>> next(clone2)
0
>>> next(clone3)
0
>>> next(clone1)
1
>>> next(clone2)
1
>>> next(clone3) # la valeur 0 n'est plus en mémoire après ça
1

compress

compress() est une curiosité qui va surtout parler aux data-scientists et matheux, et d’une manière générale aux adeptes de numpy.

Cette fonction prend deux itérables, un avec des valeurs à filtrer, et un avec des valeurs qui indiquent s’il faut filtrer l’élément ou non:

>>> list(compress(['toto', 'tata', 'titi'], [True, False, True]))
['toto', 'titi']

Je n’ai pas trouvé de use case pour cette fonction dans ma vie de tous les jours. Si quelqu’un est inspiré en commentaire, je mettrai l’article à jour.

accumulate

accumulate() est un pur produit de la programmation fonctionnelle. C’est un peu comme un map(), mais au lieu de passer chaque élément au callable, on lui passe l’élément, et le résultat du dernier appel.

Par exemple, vous voulez multiplier tout une liste : [1, 2, 3, 4]

Vous allez faire:

(((1 * 2) * 3) * 4)

C’est typiquement pour ce genre de chose qu’on utiliserait la fonction reduce():

>>> reduce(lambda x, y: x * y, [1, 2, 3, 4])
24

accumulate() fait cela, mais en plus vous yield tous les résultats intermédiaires:

>>> list(accumulate([1, 2, 3, 4], lambda x, y: x * y))
[1, 2, 6, 24]

La signature est inversée par rapport à reduce(), c’est ballot. Dans d’autres langages vous trouverez ce genre de fonction sous le nom fold() ou scan(), mais c’est le même principe.

product

product() prend des itérables, et vous balance toutes les combinaisons des éléments de ces itérables. On s’en sert surtout pour éviter les boucles for imbriquées. Par exemple au lieu de faire:

>>> lettres = "abcd"
>>> chiffres = range(3)
>>> for lettre in lettres:
...     for chiffre in chiffres:
...         print(lettre, chiffre)
...
a 0
a 1
a 2
b 0
b 1
b 2
c 0
c 1
c 2
d 0
d 1
d 2

On va faire:

>>> from itertools import product
>>> for lettre, chiffre in product(lettres, chiffres):
...     print(lettre, chiffre)
...
a 0
a 1
a 2
b 0
b 1
b 2
c 0
c 1
c 2
d 0
d 1
d 2

combinations

Génère tous les combinaisons possibles des éléments d’un itérable:

>>> from itertools import combinations
>>> list(combinations('abcd', 2))
[('a', 'b'), ('a', 'c'), ('a', 'd'), ('b', 'c'), ('b', 'd'), ('c', 'd')]
>>> list(combinations('abcd', 3))
[('a', 'b', 'c'), ('a', 'b', 'd'), ('a', 'c', 'd'), ('b', 'c', 'd')]

combinations_with_replacement

Pareil, mais autorise les doublons:

>>> from itertools import combinations_with_replacement
>>> list(combinations_with_replacement('abcd', 3))
[('a', 'a', 'a'), ('a', 'a', 'b'), ('a', 'a', 'c'), ('a', 'a', 'd'), ('a', 'b', 'b'), ('a', 'b', 'c'), ('a', 'b', 'd'), ('a', 'c', 'c'), ('a', 'c', 'd'), ('a', 'd', 'd'), ('b', 'b', 'b'), ('b', 'b', 'c'), ('b', 'b', 'd'), ('b', 'c', 'c'), ('b', 'c', 'd'), ('b', 'd', 'd'), ('c', 'c', 'c'), ('c', 'c', 'd'), ('c', 'd', 'd'), ('d', 'd', 'd')]

permutations

Comme combinations, mais en générant aussi les mêmes combinaisons dans un ordre différent:

>>> list(permutations('abc', 3))
[('a', 'b', 'c'), ('a', 'c', 'b'), ('b', 'a', 'c'), ('b', 'c', 'a'), ('c', 'a', 'b'), ('c', 'b', 'a')]

21 thoughts on “Un tools d’itertour, ou l’inverse

  • hiramash

    Bonjour Sam, bonjour Max,

    Question venant de mon expérience personnelle : Comment faites-vous pour déboguer / concevoir un enchevêtrement d’itérateurs, sans avoir à mettre au point d’abord les calculs sur des listes, donc des générateurs « déroulés » / consommés, puis en changeant les crochets en parenthèses ?

    La solution serait peut-être théorique, en utilisant des outils visuels type « dataflow », et encore…

    Également, comment mêler les primitives numpy, avec de la programmation fonctionnelle ? Je pense notamment à « numpy.ndarray.strides »

    La question peut être bottée en touche, mais depuis la version 3.5, ça sent l’intégration de numpy dans la librairie standard…

  • Sam Post author

    Comment faites-vous pour déboguer / concevoir un enchevêtrement d’itérateurs, sans avoir à mettre au point d’abord les calculs sur des listes, donc des générateurs « déroulés » / consommés, puis en changeant les crochets en parenthèses ?

    Personnellement je créé des générateurs avec yield plutôt que des listes en intension dès que le calcul est complexe.

    Également, comment mêler les primitives numpy, avec de la programmation fonctionnelle ? Je pense notamment à « numpy.ndarray.strides »

    On ne peut pas. numpy est fondamentalement incompatible avec les autres outils autour de l’itération. C’est un ilot à part. Soit on en a besoin et on reste sur numpy, soit on en a pas besoin et en profite des autres outils. Mais il faut faire un choix malheureusement: économiser la mémoire (lazy Python data structures) ou économiser le CPU (c preloaded data structures).

    La question peut être bottée en touche, mais depuis la version 3.5, ça sent l’intégration de numpy dans la librairie standard…

    J’en doute franchement. Numpy fait plusieurs Mo, et je vois mal Guido accepter de doubler la taille de Python, aussi formidable numpy soit-il. J’aurais aimé ceci dit, ça ouvrirai la porte à des trucs de fou, comme la création de lib pure Python pour la crypto, le traitement d’image, etc.

  • pistache

    Comment faites-vous pour déboguer / concevoir un enchevêtrement d’itérateurs, sans avoir à mettre au point d’abord les calculs sur des listes, donc des générateurs « déroulés » / consommés, puis en changeant les crochets en parenthèses ?

    Un bon débuggeur pas-a-pas permet de naviguer dans ces enchevêtrements, pour les débugger sans avoir besoin de les transformer en listes.

    J’utilise aussi quelque fois une expression qui me permet d’enregistrer les valeurs, c’est un changement de débuggage plus rapide à faire. Exemple :

    gen = (a*2 for a in source())

    Si je peux pouvoir enregistrer les valeurs générés par gen, je peux faire:

    DBG_LIST = []

    DBG = lambda value: DBG_LIST.append(value) or value

    gen = (DBG(a*2) for a in source())

    Les générateurs ont beau n’autoriser que des expressions, on peut quand même jouer un peu avec Python pour enregistrer la valeur (list.append(value) or return_value

    On ne peut pas. numpy est fondamentalement incompatible avec les autres outils autour de l’itération. C’est un ilot à part. Soit on en a besoin et on reste sur numpy, soit on en a pas besoin et en profite des autres outils. Mais il faut faire un choix malheureusement: économiser la mémoire (lazy Python data structures) ou économiser le CPU (c preloaded data structures).

    C’est vrai que NumPy est un peu un autre monde, mais il n’est pas complètement incompatible avec l’itération et les outils qui sont autours : Il est tout à fait possible d’itérer sur des arrays/views NumPy, et donc d’utiliser les fonctions décrites dans ce module pour lire des arrays NumPy.

    La grosse difficulté est de créer des arrays à NumPy à partir d’itérables, car cela nécessite souvent au moins deux allocations successives: celle de la liste Python créée à partir de l’itérable, et celle de l’array NumPy créée à partir de la liste. C’est normal, les arrays NumPy sont de taille définie, les itérables ne le sont pas. La seule manière d’éviter les deux allocations, c’est de pouvoir toujours connaître le nombre d’élements que retournera l’itérable, et préallouer l’array NumPy.

    La question peut être bottée en touche, mais depuis la version 3.5, ça sent l’intégration de numpy dans la librairie standard…

    AMHA, les deux projets ont plutot intérêt à rester indépendant pour préserver leur libertés respectives, tout en restant proches par intérêt mutuel.

    Et puis aussi, Python est Python avant d’être CPython, et Numpy actuellement ne supporte pleinement que CPython.

  • Sam Post author

    Il est tout à fait possible d’itérer sur des arrays/views NumPy, et donc d’utiliser les fonctions décrites dans ce module pour lire des arrays NumPy.

    Oui mais:

    • numpy charge tout en mémoire, donc le côté lazy passe à la poubelle
    • la convertion de la structure C vers un objet Python pour chaque élément tue les performances, et ont à donc “le plus mauvais des deux mondes”

    MHA, les deux projets ont plutot intérêt à rester indépendant pour préserver leur libertés respectives, tout en restant proches par intérêt mutuel.

    Oui surtout que les cores devs sont très à l’écoute de la communauté numpy, jusqu’à ajouter un opérateur rien que pour eux.

  • Brice

    Pour compress, j’imagine que ça doit pouvoir servir pour itérer sur des options actives, par exemple enregistrées en tant que bits (ou pas) :

    opts = 0b100110

    opts_list = [‘opt0’, ‘opt1’, ‘opt2’, ‘opt3’, ‘opt4’, ‘opt5′]

    for opt in compress(opts_list, [x==’1’ for x in “{0:06b}”.format(opts)]):

    … print(opt)

    opt0

    opt3

    opt4

    Mais bon, y’a probablement 15 moyens de faire ça sans compress, et 50 de sauvegarder des options binaires.

  • Heretron

    Petite typo : “qui est un générateur yieldant les mots qui correspondent à ce groupe”

  • artragis

    Bonjour,

    il n’y aurait pas une petite typo sur le filter/filterfalse?

    list(filter(est_pair, range(10))) # ne garde que ce qui est PAIR

    [1, 3, 5, 7, 9]

    from itertools import filterfalse

    list(filterfalse(est_pair, range(10))) # ne garde que ce qui est IMPAIR

    [0, 2, 4, 6, 8]

    J’ai fait le test en local :

    est_pair = lambda a: a % 2 == 0

    filter(est_pair, range(15))

    [0, 2, 4, 6, 8, 10, 12, 14]

  • olivier

    Merci pour l’article!

    Il manquerait pas un mot ici ?

    “A ce stade, on a un générateur qu’on peut parcourir, mais voyez-vous il ne contient pas le résultat tel que je vous l’ai montré. A la place, il yield des paires (group, grouper):”

  • Fred

    Grosse typo: La raison de ce manque d’intuitivité, c’est que théoriquement groupby() peut travailler de manière paresseuse (de “paresse” et non de “paraître”).

    Super article. Le plus dur, pour moi, c’est de me dire “putain mais là je me suis fait chier à écrire un algo alors que j’avais dropwhile qui aurait pu me le faire immédiatement” et de me dire ensuite “alors si je veux remplacer toutes les fois où j’ai mis un if not truc: continue par un dropwhile combien de temps ça va me prendre”…

    Et euh sinon je viens de killer ma vm après avoir tenté le list(infinite_generator()). Ben oui, moi quand on me dit “ça va crasher” ben c’est plus fort que moi: faut que je teste… ;)

  • octarin

    Très bon article, ça permet de redécouvrir la bibliothèque standard de python et d’éviter de réinventer la roue dans nombre de cas.

    Néanmoins, j’aurais des réserves à propos de ta remarque sur count.

    Par quel autre moyen simple peux-tu retourner un générateur qui renvoie sans s’arrêter une suite de nombres ? Range impose en effet un point d’arrêt, ce que ne fait pas count. Or parfois il peut être utile de ne pas mettre de limites dans une telle énumération (du moment qu’on sait ce qu’on fait bien entendu…), comme par exemple pour se dispenser d’un while explicite ce qui parfois facilite la compréhension.

    count n’est donc pas entièrement à jeter :)

  • Sam Post author

    En pratique, on ne veut pas une taille infinie sur un count, mais le limiter à la taille maximale adéquate pour le système. Il vaut mieux faire:

    >>> import sys
    >>> range(sys.maxsize)
    range(0, 9223372036854775807)
    
  • kontre

    Le truc avec numpy c’est qu’il évolue beaucoup plus vite que python, l’intégrer dans python ralentirait énormément son développement.

    C’est aussi pour ça que request est pas inclus dans python par exemple, alors qu’il y aurait un intérêt énorme.

    Sinon pour l’itération sur les tableaux numpy on garde le côté lazy sur les tableaux à plusieurs dimensions: l’itération se fait sur la première dimension, et renvoit des vues sur le tableau initial avec les dimensions restantes, donc ça prend que dalle de mémoire. Mais de manière générale la philosophie est différente, et il sera bien plus efficace de vectoriser que d’itérer (quand c’est possible).

    Jouer avec numpy.ndarray.strides c’est très bas level quand même, l’intérêt de numpy c’est justement de ne pas s’emmerder avec !

    • hiramash

      Bonsoir kontre,

      Réponse très instructive sur numpy. J’avais remarqué qu’il y avait quelques itérateurs dans numpy, mais je n’avais pas bien compris leur fonctionnement.

      En revanche, il me semblait que pour bénéficier des performances de « vues sur les tableaux », il fallait vraiment se servir de « strides ». Si tu as des fonctions / macros de plus haut niveau, je suis preneur.

      C’est une stratégie similaire à numpy, que j’ai vue dans RapidMiner.

      Après, il y a moyen de rentrer carrément dans la sémantique des algorithmes, et de se demander si on peut trouver une version « incrémentale » de tous les algorithmes de base en algèbre linéaire.

      Pour l’instant, à ce sujet j’ai trouvé une piste intéressante, où il existe une version « Monte-Carlo » de toutes les opérations algébriques de base en algèbre matricielle. Chaque opération devient la somme de résultats partiels et successifs issus de tirages statistiques.

      C’est particulièrement adapté pour la notion de « q-bit » qui fonctionne un peu en mode « itérateur » :

      – Une librairie Python pour du Monte-Carlo / qbits : « quameon »

      – « Monte Carlo Linear Algebra » : http://www.mit.edu/~dimitrib/Monte_Carlo_Linear_Algebra.pdf

      Voilà, voilà…

  • Sam Post author

    Il y une différence entre intégrer toute la lib et intégrer juste l’API principale. L’api d’array, avec les opérations les plus courantes. Ou l’api de requests, avec idem, les requêtes les plus courantes, c’est un gain énorme.

  • Pompom

    Du peu que je connaisse des langages fonctionnels fold() n’est pas un équivalent à accumulate() mais à reduce()

  • kontre

    @hiramash Les strides sont utilisés en interne par numpy, c’est ça qui fait ses perfs, mais je ne vois pas quelle opération tu veux faire en utilisant les strides ? Tu as un exemple ?

    Ce à quoi il faut faire gaffe pour les perf c’est la dimension sur laquelle tu itères: si ça fait des sauts en mémoire c’est moins bon que si les données sont lues à la suite. La dimension en question dépend des strides en effet. Pour un tableau créé de manière standard, il faut itérer sur la dernière dimension de préférence (mais en pratique t’as pas souvent le choix).

    @Sam C’est vrai que parfois une API pour des tableaux à N dimensions ça serait pas du luxe. Mais bon dans mon cas j’ai souvent besoin d’algèbre linéaire, donc je ne réfléchis même pas à prendre autre chose que numpy.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.