web-dev-qa-db-fra.com

Python - Crée une liste avec une capacité initiale

Un code comme celui-ci arrive souvent:

l = []
while foo:
    #baz
    l.append(bar)
    #qux

C'est vraiment lent si vous êtes sur le point d'ajouter des milliers d'éléments à votre liste, car la liste devra être constamment redimensionnée pour s'adapter aux nouveaux éléments.

En Java, vous pouvez créer une ArrayList avec une capacité initiale. Si vous avez une idée de la taille de votre liste, cela sera beaucoup plus efficace.

Je comprends qu'un code comme celui-ci peut souvent être reformulé dans une compréhension de liste. Si la boucle for/while est très compliquée, c'est irréalisable. Existe-t-il un équivalent pour nous Python programmeurs?

175
Claudiu
def doAppend( size=10000 ):
    result = []
    for i in range(size):
        message= "some unique object %d" % ( i, )
        result.append(message)
    return result

def doAllocate( size=10000 ):
    result=size*[None]
    for i in range(size):
        message= "some unique object %d" % ( i, )
        result[i]= message
    return result

Résultats. (évalue chaque fonction 144 fois et en moyenne la durée)

simple append 0.0102
pre-allocate  0.0098

Conclusion. Cela compte à peine.

L'optimisation prématurée est la racine de tout Mal.

119
S.Lott

Les listes Python n'ont pas de pré-allocation intégrée. Si vous avez vraiment besoin de faire une liste et d’éviter la surcharge liée à l’ajout (et vous devez vérifier que c’est le cas), vous pouvez le faire:

l = [None] * 1000 # Make a list of 1000 None's
for i in xrange(1000):
    # baz
    l[i] = bar
    # qux

Peut-être pourriez-vous éviter la liste en utilisant un générateur à la place:

def my_things():
    while foo:
        #baz
        yield bar
        #qux

for thing in my_things():
    # do something with thing

De cette façon, la liste n'est pas entièrement stockée dans la mémoire, mais générée au besoin.

73
Ned Batchelder

Version courte: utiliser

pre_allocated_list = [None] * size

pré-allouer une liste (c'est-à-dire pouvoir traiter les éléments 'taille' de la liste au lieu de former progressivement la liste en l'ajoutant). Cette opération est TRES rapide, même sur de grandes listes. L'attribution de nouveaux objets qui seront affectés ultérieurement aux éléments de la liste prendra BEAUCOUP plus long et constituera LE goulot d'étranglement de votre programme en termes de performances.

Version longue:

Je pense que le temps d'initialisation devrait être pris en compte. Comme dans python tout est une référence, peu importe que vous définissiez chaque élément en Aucune ou en une chaîne - de toute façon, il ne s'agit que d'une référence. Bien que cela prenne plus de temps si vous voulez créer un nouvel objet pour chaque élément à référencer.

Pour Python 3.2:

import time
import copy

def print_timing (func):
  def wrapper (*arg):
    t1 = time.time ()
    res = func (*arg)
    t2 = time.time ()
    print ("{} took {} ms".format (func.__name__, (t2 - t1) * 1000.0))
    return res

  return wrapper

@print_timing
def prealloc_array (size, init = None, cp = True, cpmethod=copy.deepcopy, cpargs=(), use_num = False):
  result = [None] * size
  if init is not None:
    if cp:
      for i in range (size):
          result[i] = init
    else:
      if use_num:
        for i in range (size):
            result[i] = cpmethod (i)
      else:
        for i in range (size):
            result[i] = cpmethod (cpargs)
  return result

@print_timing
def prealloc_array_by_appending (size):
  result = []
  for i in range (size):
    result.append (None)
  return result

@print_timing
def prealloc_array_by_extending (size):
  result = []
  none_list = [None]
  for i in range (size):
    result.extend (none_list)
  return result

def main ():
  n = 1000000
  x = prealloc_array_by_appending(n)
  y = prealloc_array_by_extending(n)
  a = prealloc_array(n, None)
  b = prealloc_array(n, "content", True)
  c = prealloc_array(n, "content", False, "some object {}".format, ("blah"), False)
  d = prealloc_array(n, "content", False, "some object {}".format, None, True)
  e = prealloc_array(n, "content", False, copy.deepcopy, "a", False)
  f = prealloc_array(n, "content", False, copy.deepcopy, (), False)
  g = prealloc_array(n, "content", False, copy.deepcopy, [], False)

  print ("x[5] = {}".format (x[5]))
  print ("y[5] = {}".format (y[5]))
  print ("a[5] = {}".format (a[5]))
  print ("b[5] = {}".format (b[5]))
  print ("c[5] = {}".format (c[5]))
  print ("d[5] = {}".format (d[5]))
  print ("e[5] = {}".format (e[5]))
  print ("f[5] = {}".format (f[5]))
  print ("g[5] = {}".format (g[5]))

if __== '__main__':
  main()

Évaluation:

prealloc_array_by_appending took 118.00003051757812 ms
prealloc_array_by_extending took 102.99992561340332 ms
prealloc_array took 3.000020980834961 ms
prealloc_array took 49.00002479553223 ms
prealloc_array took 316.9999122619629 ms
prealloc_array took 473.00004959106445 ms
prealloc_array took 1677.9999732971191 ms
prealloc_array took 2729.999780654907 ms
prealloc_array took 3001.999855041504 ms
x[5] = None
y[5] = None
a[5] = None
b[5] = content
c[5] = some object blah
d[5] = some object 5
e[5] = a
f[5] = []
g[5] = ()

Comme vous pouvez le constater, le simple fait de faire une grande liste de références au même objet None prend très peu de temps.

Le fait de prévoir ou d’étendre prend plus de temps (je n’ai fait aucune moyenne, mais après avoir exécuté cela plusieurs fois, je peux vous dire que l’extension et l’ajout prennent à peu près le même temps).

Allouer un nouvel objet pour chaque élément - c'est ce qui prend le plus de temps. Et la réponse de S.Lott fait cela - formate une nouvelle chaîne à chaque fois. Ce qui n’est pas strictement requis - si vous souhaitez pré-allouer de l’espace, créez simplement une liste de None, puis assignez des données à la liste des éléments à volonté. Quoi qu'il en soit, la génération de données prend plus de temps que l'ajout/l'extension d'une liste, que vous la génériez lors de la création de la liste ou après. Mais si vous voulez une liste peu dense, commencer par une liste de None est nettement plus rapide.

45
LRN

La manière pythonique pour cela est:

x = [None] * numElements

ou quelle que soit la valeur par défaut que vous souhaitez utiliser, par ex.

bottles = [Beer()] * 99
sea = [Fish()] * many
vegetarianPizzas = [None] * peopleOrderingPizzaNotQuiche

[EDIT: Caveat Emptor La syntaxe [Beer()] * 99 crée un Beer puis remplit un tableau avec 99 références à la même instance unique]

L'approche par défaut de Python peut être assez efficace, bien que cette efficacité diminue avec l'augmentation du nombre d'éléments.

Comparer

import time

class Timer(object):
    def __enter__(self):
        self.start = time.time()
        return self

    def __exit__(self, *args):
        end = time.time()
        secs = end - self.start
        msecs = secs * 1000  # millisecs
        print('%fms' % msecs)

Elements   = 100000
Iterations = 144

print('Elements: %d, Iterations: %d' % (Elements, Iterations))


def doAppend():
    result = []
    i = 0
    while i < Elements:
        result.append(i)
        i += 1

def doAllocate():
    result = [None] * Elements
    i = 0
    while i < Elements:
        result[i] = i
        i += 1

def doGenerator():
    return list(i for i in range(Elements))


def test(name, fn):
    print("%s: " % name, end="")
    with Timer() as t:
        x = 0
        while x < Iterations:
            fn()
            x += 1


test('doAppend', doAppend)
test('doAllocate', doAllocate)
test('doGenerator', doGenerator)

avec

#include <vector>
typedef std::vector<unsigned int> Vec;

static const unsigned int Elements = 100000;
static const unsigned int Iterations = 144;

void doAppend()
{
    Vec v;
    for (unsigned int i = 0; i < Elements; ++i) {
        v.Push_back(i);
    }
}

void doReserve()
{
    Vec v;
    v.reserve(Elements);
    for (unsigned int i = 0; i < Elements; ++i) {
        v.Push_back(i);
    }
}

void doAllocate()
{
    Vec v;
    v.resize(Elements);
    for (unsigned int i = 0; i < Elements; ++i) {
        v[i] = i;
    }
}

#include <iostream>
#include <chrono>
using namespace std;

void test(const char* name, void(*fn)(void))
{
    cout << name << ": ";

    auto start = chrono::high_resolution_clock::now();
    for (unsigned int i = 0; i < Iterations; ++i) {
        fn();
    }
    auto end = chrono::high_resolution_clock::now();

    auto elapsed = end - start;
    cout << chrono::duration<double, milli>(elapsed).count() << "ms\n";
}

int main()
{
    cout << "Elements: " << Elements << ", Iterations: " << Iterations << '\n';

    test("doAppend", doAppend);
    test("doReserve", doReserve);
    test("doAllocate", doAllocate);
}

Sur mon Windows 7 i7, 64 bits Python donne

Elements: 100000, Iterations: 144
doAppend: 3587.204933ms
doAllocate: 2701.154947ms
doGenerator: 1721.098185ms

Alors que C++ donne (construit avec MSVC, 64 bits, optimisations activées)

Elements: 100000, Iterations: 144
doAppend: 74.0042ms
doReserve: 27.0015ms
doAllocate: 5.0003ms

La version de débogage C++ produit:

Elements: 100000, Iterations: 144
doAppend: 2166.12ms
doReserve: 2082.12ms
doAllocate: 273.016ms

Le point ici est que, avec Python, vous pouvez améliorer de 7 à 8% les performances, et si vous pensez que vous écrivez une application haute performance (ou si vous écrivez quelque chose qui est utilisé dans une service Web ou quelque chose du genre), il ne faut pas en perdre, mais vous devrez peut-être repenser votre choix de langue.

De plus, le code Python n'est pas vraiment le code Python. Le passage à un code véritablement python donne de meilleures performances:

import time

class Timer(object):
    def __enter__(self):
        self.start = time.time()
        return self

    def __exit__(self, *args):
        end = time.time()
        secs = end - self.start
        msecs = secs * 1000  # millisecs
        print('%fms' % msecs)

Elements   = 100000
Iterations = 144

print('Elements: %d, Iterations: %d' % (Elements, Iterations))


def doAppend():
    for x in range(Iterations):
        result = []
        for i in range(Elements):
            result.append(i)

def doAllocate():
    for x in range(Iterations):
        result = [None] * Elements
        for i in range(Elements):
            result[i] = i

def doGenerator():
    for x in range(Iterations):
        result = list(i for i in range(Elements))


def test(name, fn):
    print("%s: " % name, end="")
    with Timer() as t:
        fn()


test('doAppend', doAppend)
test('doAllocate', doAllocate)
test('doGenerator', doGenerator)

Qui donne

Elements: 100000, Iterations: 144
doAppend: 2153.122902ms
doAllocate: 1346.076965ms
doGenerator: 1614.092112ms

(dans doGenerator 32 bits fait mieux que doAllocate).

Ici, l’écart entre doAppend et doAllocate est beaucoup plus grand.

Évidemment, les différences ici ne s’appliquent vraiment que si vous le faites plus d’une poignée de fois ou si vous le faites sur un système très chargé, où ces chiffres vont être réduits en ordre de grandeur, ou si vous avez affaire à des listes considérablement plus grandes.

Le point ici: faites-le à la manière pythonique pour la meilleure performance.

Mais si vous vous inquiétez des performances générales de haut niveau, Python n'est pas le bon langage. Le problème le plus fondamental étant que Python les appels de fonctions ont été jusqu'à 300 fois plus lents que d’autres langages en raison de Python fonctionnalités telles que les décorateurs, etc. ( https: //wiki.python. org/moin/PythonSpeed ​​/ PerformanceTips # Data_Aggregation # Data_Aggregation ).

22
kfsone

Comme d'autres l'ont mentionné, le moyen le plus simple de pré-ensemencer une liste d'objets NoneType.

Cela étant dit, vous devez comprendre le fonctionnement des listes Python avant de décider si cela est nécessaire. Dans l'implémentation CPython d'une liste, le tableau sous-jacent est toujours créé avec un espace supplémentaire, dans des tailles progressivement plus grandes ( 4, 8, 16, 25, 35, 46, 58, 72, 88, 106, 126, 148, 173, 201, 233, 269, 309, 354, 405, 462, 526, 598, 679, 771, 874, 990, 1120, etc), de sorte que le redimensionnement de la liste ne se produit pas presque aussi souvent.

En raison de ce comportement, la plupart des fonctions list.append() ont la complexité O(1) pour les ajouts, leur complexité augmentant uniquement lorsque vous franchissez l'une de ces limites; O(n). Ce comportement est ce qui conduit à l'augmentation minimale du temps d'exécution dans la réponse de S. Lott.

Source: http://www.laurentluce.com/posts/python-list-implementation/

8
Russell Troxel

j'ai lancé le code de @ s.lott et obtenu la même augmentation de 10% en effectuant une pré-allocation. essayé l'idée de @ jeremy en utilisant un générateur et était capable de voir la performance du gen mieux que celle du doAllocate. Pour mon projet, les 10% d’amélioration sont importants, alors merci à tous car cela aide beaucoup.

def doAppend( size=10000 ):
    result = []
    for i in range(size):
        message= "some unique object %d" % ( i, )
        result.append(message)
    return result

def doAllocate( size=10000 ):
    result=size*[None]
    for i in range(size):
        message= "some unique object %d" % ( i, )
        result[i]= message
    return result

def doGen( size=10000 ):
    return list("some unique object %d" % ( i, ) for i in xrange(size))

size=1000
@print_timing
def testAppend():
    for i in xrange(size):
        doAppend()

@print_timing
def testAlloc():
    for i in xrange(size):
        doAllocate()

@print_timing
def testGen():
    for i in xrange(size):
        doGen()


testAppend()
testAlloc()
testGen()

testAppend took 14440.000ms
testAlloc took 13580.000ms
testGen took 13430.000ms
4
Jason Wiener

Des préoccupations concernant la pré-allocation dans Python se posent si vous travaillez avec numpy, qui contient davantage de tableaux de type C. Dans ce cas, les problèmes de pré-allocation concernent la forme des données et la valeur par défaut.

Pensez à numpy si vous effectuez des calculs numériques sur des listes volumineuses et souhaitez des performances.

3
J450n

Pour certaines applications, un dictionnaire peut être ce que vous recherchez. Par exemple, dans la méthode find_totient, j'ai trouvé qu'il était plus pratique d'utiliser un dictionnaire car je n'avais pas d'indice zéro.

def totient(n):
    totient = 0

    if n == 1:
        totient = 1
    else:
        for i in range(1, n):
            if math.gcd(i, n) == 1:
                totient += 1
    return totient

def find_totients(max):
    totients = dict()
    for i in range(1,max+1):
        totients[i] = totient(i)

    print('Totients:')
    for i in range(1,max+1):
        print(i,totients[i])

Ce problème pourrait également être résolu avec une liste préallouée:

def find_totients(max):
    totients = None*(max+1)
    for i in range(1,max+1):
        totients[i] = totient(i)

    print('Totients:')
    for i in range(1,max+1):
        print(i,totients[i])

J’ai le sentiment que ce n’est pas aussi élégant et sujet aux bogues parce que j’en stocke aucun qui pourrait renvoyer une exception si je l’utilise par mégarde, et parce que j’ai besoin de penser aux cas Edge que la carte me permet d’éviter.

Il est vrai que le dictionnaire ne sera pas aussi efficace, mais comme d'autres l'ont déjà fait remarquer, les différences de vitesse petites ne valent pas toujours importants risques d’entretien.

0
Josiah Yoder