Le moyen le plus rapide de convertir un itérateur en liste


196

Avoir un iteratorobjet, y a-t-il quelque chose de plus rapide, meilleur ou plus correct qu'une compréhension de liste pour obtenir une liste des objets retournés par l'itérateur?

user_list = [user for user in user_iterator]

1
Avant d'optimiser cela, assurez-vous d'avoir effectué un profilage pour prouver qu'il s'agit vraiment du goulot d'étranglement.
S.Lott

2
@ S.Lott. Je suis normalement d'accord avec cette attitude mais, dans ce cas, elle devrait être optimisée stylistiquement ce qui, comme c'est souvent le cas avec Python, l'optimisera également pour la vitesse.
aaronasterling le

5
Le PO n'a rien dit sur un goulot d'étranglement. C'est une question générale parfaitement fine avec une réponse simple, elle n'a pas besoin de dépendre d'une application spécifique qui peut être exécutée via un profileur.
Ken Williams

4
Le moyen le plus compact est [*iterator].
Challenger5

Réponses:


356
list(your_iterator)

7
En fait, presque toujours un peu plus vite. Aussi, beaucoup plus évident.
Thomas Wouters

8
@systempuntoout Il fonctionne entièrement en C. La compréhension de la liste est en python. Bien sûr, cela fonctionne plus vite.
aaronasterling

4
Je déteste encore totalement qu'il n'y ait pas de meilleur moyen en python. Il est fastidieux de devoir modifier les deux côtés d'une expression pour pouvoir la découper ou l'indexer. (très courant en python3, si c'est une expression pure comme zip, ou une carte avec une fonction pure)
Jo So

5
Lors de mes tests rapides, j'ai [*your_iterator]semblé être environ deux fois plus rapide que list(your_iterator). Est-ce généralement vrai ou était-ce juste une occasion spécifique? (J'ai utilisé un mapitérateur.)
Neinstein

2
@Bachsau: Certes, c'est assez bon, mais comparez-le au script Bash où vous pouvez manipuler la sortie actuelle en ajoutant un tube et une autre commande de filtre strictement à droite de la commande actuelle. Ça craint que pour une distinction aussi mineure (itérateur vs liste matérialisée), vous deviez souvent déplacer le curseur en arrière.
Jo So


4

@Robino suggérait d'ajouter quelques tests qui ont du sens, voici donc un simple benchmark entre 3 façons possibles (peut-être les plus utilisées) de convertir un itérateur en liste:

  1. par constructeur de type

list(my_iterator)

  1. en déballant

[*my_iterator]

  1. en utilisant la compréhension de liste

[e for e in my_iterator]

J'utilise simple_bechmark bibliothèque

from simple_benchmark import BenchmarkBuilder
from heapq import nsmallest

b = BenchmarkBuilder()

@b.add_function()
def convert_by_type_constructor(size):
    list(iter(range(size)))

@b.add_function()
def convert_by_list_comprehension(size):
    [e for e in iter(range(size))]

@b.add_function()
def convert_by_unpacking(size):
    [*iter(range(size))]


@b.add_arguments('Convert an iterator to a list')
def argument_provider():
    for exp in range(2, 22):
        size = 2**exp
        yield size, size

r = b.run()
r.plot()

entrez la description de l'image ici

Comme vous pouvez le voir, il est très difficile de faire une différence entre la conversion par le constructeur et la conversion par décompression, la conversion par compréhension de liste est l'approche la plus «lente».


J'ai également testé différentes versions de Python (3.6, 3.7, 3.8, 3.9) en utilisant le script simple suivant:

import argparse
import timeit

parser = argparse.ArgumentParser(
    description='Test convert iterator to list')
parser.add_argument(
    '--size', help='The number of elements from iterator')

args = parser.parse_args()

size = int(args.size)
repeat_number = 10000

# do not wait too much if the size is too big
if size > 10000:
    repeat_number = 100


def test_convert_by_type_constructor():
    list(iter(range(size)))


def test_convert_by_list_comprehension():
    [e for e in iter(range(size))]


def test_convert_by_unpacking():
    [*iter(range(size))]


def get_avg_time_in_ms(func):
    avg_time = timeit.timeit(func, number=repeat_number) * 1000 / repeat_number
    return round(avg_time, 6)


funcs = [test_convert_by_type_constructor,
         test_convert_by_unpacking, test_convert_by_list_comprehension]

print(*map(get_avg_time_in_ms, funcs))

Le script sera exécuté via un sous-processus à partir d'un Jupyter Notebook (ou d'un script), le paramètre size sera passé via des arguments de ligne de commande et les résultats du script seront extraits de la sortie standard.

from subprocess import PIPE, run

import pandas

simple_data = {'constructor': [], 'unpacking': [], 'comprehension': [],
        'size': [], 'python version': []}


size_test = 100, 1000, 10_000, 100_000, 1_000_000
for version in ['3.6', '3.7', '3.8', '3.9']:
    print('test for python', version)
    for size in size_test:
        command = [f'python{version}', 'perf_test_convert_iterator.py', f'--size={size}']
        result = run(command, stdout=PIPE, stderr=PIPE, universal_newlines=True)
        constructor, unpacking,  comprehension = result.stdout.split()
        
        simple_data['constructor'].append(float(constructor))
        simple_data['unpacking'].append(float(unpacking))
        simple_data['comprehension'].append(float(comprehension))
        simple_data['python version'].append(version)
        simple_data['size'].append(size)

df_ = pandas.DataFrame(simple_data)
df_

entrez la description de l'image ici

Vous pouvez obtenir mon cahier complet d' ici .

Dans la plupart des cas, dans mes tests, le déballage s'avère plus rapide, mais la différence est si petite que les résultats peuvent changer d'une exécution à l'autre. Encore une fois, l'approche de compréhension est la plus lente, en fait, les 2 autres méthodes sont jusqu'à ~ 60% plus rapides.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.