web-dev-qa-db-fra.com

Comment puis-je limiter les itérations d'une boucle en Python?

Supposons que j'ai une liste d'éléments et que je veuille en parcourir les premiers éléments:

items = list(range(10)) # I mean this to represent any kind of iterable.
limit = 5

Mise en œuvre naïve

Le naïf Python provenant d’autres langues) écrirait probablement ce code parfaitement utilisable et performant (s’il n’est pas idiotomatique):

index = 0
for item in items: # Python's `for` loop is a for-each.
    print(item)    # or whatever function of that item.
    index += 1
    if index == limit:
        break

Mise en œuvre plus idiomatique

Mais Python a enumerate, ce qui absorbe gentiment environ la moitié de ce code:

for index, item in enumerate(items):
    print(item)
    if index == limit: # There's gotta be a better way.
        break

Nous avons donc coupé le code supplémentaire de moitié. Mais il doit y avoir un meilleur moyen.

Peut-on approximer le comportement du pseudocode ci-dessous?

Si enumerate a pris un autre argument optionnel stop (par exemple, il faut un argument start comme celui-ci: enumerate(items, start=1)) qui serait, je pense, idéal, mais ce qui suit ne n'existe pas (voir le documentation sur énumérer ici ):

# hypothetical code, not implemented:
for _, item in enumerate(items, start=0, stop=limit): # `stop` not implemented
    print(item)

Notez qu'il ne serait pas nécessaire de nommer le index car il n'est pas nécessaire de le référencer.

Existe-t-il un moyen idiomatique d’écrire ce qui précède? Comment?

Une question secondaire: pourquoi cela n’est-il pas intégré à énumérer?

36
Aaron Hall

Comment puis-je limiter les itérations d'une boucle en Python?

_for index, item in enumerate(items):
    print(item)
    if index == limit:
        break
_

Existe-t-il une méthode idiomatique plus courte pour écrire ce qui précède? Comment?

Y compris l'indice

Zip s'arrête sur le plus court itérable de ses arguments. (Contrairement au comportement de _Zip_longest_, qui utilise l'itéré le plus long.)

range peut fournir un nombre itérable limité que nous pouvons transmettre à Zip avec notre fichier itérable primaire.

Nous pouvons donc passer un objet range (avec son argument stop) à Zip et l'utiliser comme une énumération limitée.

Zip(range(limit), items)

Utiliser Python 3, Zip et range renvoient des itérables, qui acheminent les données au lieu de les matérialiser dans des listes pour les étapes intermédiaires.

_for index, item in Zip(range(limit), items):
    print(index, item)
_

Pour obtenir le même comportement dans Python 2, il suffit de remplacer xrange par range et _itertools.izip_ par Zip.

_from itertools import izip
for index, item in izip(xrange(limit), items):
    print(item)
_

Si vous n'avez pas besoin de l'index, _itertools.islice_

Vous pouvez utiliser _itertools.islice_:

_for item in itertools.islice(items, 0, stop):
    print(item)
_

qui ne nécessite pas d'affectation à l'index.

Composer enumerate(islice(items, stop)) pour obtenir l'index

Comme le souligne Pablo Ruiz Ruiz, nous pouvons également composer islice avec énumération.

_for index, item in enumerate(islice(items, limit)):
    print(index, item)
_

Pourquoi n'est-ce pas intégré à enumerate?

Voici l'énumération implémentée dans pure Python (avec les modifications possibles pour obtenir le comportement souhaité dans les commentaires)):

_def enumerate(collection, start=0):  # could add stop=None
    i = start
    it = iter(collection)
    while 1:                         # could modify to `while i != stop:`
        yield (i, next(it))
        i += 1
_

Ce qui précède serait moins performant pour ceux qui utilisent déjà énumérer, car il faudrait vérifier s’il est temps d’arrêter chaque itération. Nous pouvons simplement vérifier et utiliser l'ancienne énumération si vous n'obtenez pas d'argument stop:

__enumerate = enumerate

def enumerate(collection, start=0, stop=None):
    if stop is not None:
        return Zip(range(start, stop), collection)
    return _enumerate(collection, start)
_

Cette vérification supplémentaire aurait un impact légèrement négligeable sur les performances.

Quant à pourquoi enumerate n'a pas d'argument stop, cela avait été proposé à l'origine (voir PEP 279 ):

Cette fonction a été initialement proposée avec des arguments optionnels de démarrage et d'arrêt. GvR [Guido van Rossum] a fait remarquer que l'appel de fonction enumerate(seqn, 4, 6) avait une interprétation alternative plausible comme une tranche qui renverrait les quatrième et cinquième éléments de la séquence. Pour éviter toute ambiguïté, les arguments optionnels ont été supprimés même si cela impliquait une perte de flexibilité en tant que compteur de boucle. Cette flexibilité était la plus importante pour le cas courant de compter à partir d'un, comme dans:

_for linenum, line in enumerate(source,1):  print linenum, line
_

Donc, apparemment start a été conservé car il était très précieux et stop a été abandonné car il comportait moins de cas d'utilisation et contribuait à la confusion quant à l'utilisation de la nouvelle fonction.

Évitez de trancher avec la notation en indice

Une autre réponse dit:

Pourquoi ne pas simplement utiliser

_for item in items[:limit]: # or limit+1, depends
_

Voici quelques inconvénients:

  • Cela ne fonctionne que pour les iterables acceptant le découpage en tranches, il est donc plus limité.
  • S'ils acceptent le découpage en tranches, il crée généralement une nouvelle structure de données en mémoire, au lieu d'itérer sur la structure de données de référence, il gaspille ainsi de la mémoire (tous les objets intégrés font des copies lorsqu'ils sont découpés en tranches. ).
  • Les iterables non licenciables nécessiteraient un autre type de manipulation. Si vous passez à un modèle d'évaluation paresseux, vous devrez également modifier le code avec découpage.

Vous ne devriez utiliser le découpage en tranches qu'avec une notation en indice lorsque vous en comprenez les limites et que vous réalisez une copie ou une vue.

Conclusion

Je présume que la communauté Python) connaît maintenant l’utilisation d’énumérer, les coûts de confusion seraient compensés par la valeur de l’argument.

Jusque-là, vous pouvez utiliser:

_for index, element in Zip(range(limit), items):
    ...
_

ou

_for index, item in enumerate(islice(items, limit)):
    ...
_

ou, si vous n'avez pas du tout besoin de l'index:

_for element in islice(items, 0, limit):
    ...
_

Et évitez de découper avec la notation en indice, sauf si vous en comprenez les limites.

68
Aaron Hall

Vous pouvez utiliser itertools.islice pour cela. Il accepte les arguments start, stop et step. Si vous ne transmettez qu'un seul argument, il est considéré comme stop. Et ça marchera avec n'importe quel itérable.

itertools.islice(iterable, stop)
itertools.islice(iterable, start, stop[, step])

Démo:

>>> from itertools import islice
>>> items = list(range(10))
>>> limit = 5
>>> for item in islice(items, limit):
    print item,
...
0 1 2 3 4

Exemple de docs:

islice('ABCDEFG', 2) --> A B
islice('ABCDEFG', 2, 4) --> C D
islice('ABCDEFG', 2, None) --> C D E F G
islice('ABCDEFG', 0, None, 2) --> A C E G
21
Ashwini Chaudhary

Pourquoi ne pas simplement utiliser

for item in items[:limit]: # or limit+1, depends
    print(item)    # or whatever function of that item.

Cela ne fonctionnera que pour certains iterables, mais puisque vous avez spécifié des listes, cela fonctionne.

Cela ne fonctionne pas si vous utilisez des sets ou des dicts, etc.

19
JeD

Pourquoi ne pas boucler jusqu'à la limite ou la fin de la liste, selon la première éventualité, comme ceci:

items = range(10)
limit = 5
for i in range(min(limit, len(items))):
  print items[i]

Sortie:

0
1
2
3
4
1
codeforester

Passer islice avec la limite intérieure énumérée

a = [2,3,4,2,1,4]

for a, v in enumerate(islice(a, 3)): 
   print(a, v)

Sortie:

0 2
1 3
2 4
1
Pablo Ruiz Ruiz