230 votes

Python - créer une liste avec la capacité initiale

Code comme cela arrive souvent:

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

C'est vraiment très lent si vous êtes sur le point d'ajouter des milliers d'éléments de votre liste, la liste devra être constamment re-initialisé à croître. (Je comprends que les listes ne sont pas seulement les wrappers autour de certains de type array-chose, mais quelque chose de plus compliqué. Je pense que cela s'applique toujours, mais, laissez-moi savoir si ce n').

En Java, vous pouvez créer une liste de tableaux avec une capacité initiale. Si vous avez une idée de comment grand votre liste sera, ce sera beaucoup plus efficace.

Je comprends que ce type de code peut souvent être de nouveau pris en compte dans une compréhension de liste. Si le/la boucle while est très compliqué, si, c'est impossible. Est-il l'équivalent pour nous de python pour les programmeurs?

120voto

S.Lott Points 207588
<pre><code></code><p><strong>Résultats</strong>. (chaque fonction doit être évaluée 144 fois et la durée moyenne)</p><pre><code></code></pre><p><strong>Conclusion</strong>. Il est important à peine. </p><p>L’optimisation prématurée est la racine de tout mal.</p></pre>

98voto

Ned Batchelder Points 128913

Listes de Python n’ont aucune pré-allocation intégrée. Si vous avez vraiment besoin de faire une liste et doivent éviter la surcharge d’ajout (et vous devez vérifier que vous faites), vous pouvez faire ceci :

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

De cette façon, la liste n’est pas tous tout en mémoire du tout, simplement généré selon les besoins.

70voto

LRN Points 183

Version courte: utilisation

pre_allocated_list = [None] * size

à pré-allouer une liste (qui est, pour être en mesure de l'adresse 'taille' éléments de la liste au lieu de constituer progressivement la liste en ajoutant). Cette opération est TRÈS rapide, même sur de grandes listes. L'attribution de nouveaux objets qui seront ensuite affecté à la liste des éléments qui va prendre BEAUCOUP plus de temps et être LE goulot d'étranglement dans votre programme, en terme de performance.

Version longue:

Je pense que le temps d'initialisation doit être pris en compte. Depuis en python tout est une référence, il n'a pas d'importance si vous réglez chaque élément dans Aucune ou une chaîne de caractères - de toute façon c'est seulement une référence. Même si cela prendra plus de temps si vous voulez créer un nouvel objet pour chaque élément de référence.

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 __name__ == '__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 voir, juste de faire une grosse liste de références pour le même Aucun objet prend très peu de temps.

Ajoutant ou en étendant prend plus de temps (je n'ai pas moyen de rien, mais après l'exécution de ce à quelques reprises, je peux vous dire que l'extension et l'ajout de prendre à peu près à la même heure).

L'attribution de nouveaux objets pour chaque élément - qu'est ce qui prend le plus de temps. Et S. Lott réponse du fait que - les formats d'une nouvelle chaîne de tous les temps. Ce qui n'est pas strictement nécessaire - si vous souhaitez pré-allouer de l'espace, il suffit de faire une liste de rien, puis affecter des données à la liste des éléments à volonté. De toute façon, il faut plus de temps pour générer des données qu'à ajouter/étendre une liste, que vous générer lors de la création de la liste, ou après. Mais si vous voulez un peu densément peuplées de la liste, puis de commencer par une liste d'Aucun n'est certainement plus rapide.

39voto

kfsone Points 7375

Le Pythonism pour ce qui est

x = elements * [None]

ou quelle que soit la valeur par défaut que vous souhaitez prepop avec.

Python par défaut de l'approche peut être assez efficace, bien que l'efficacité se désintègre comme vous augmentez le nombre d'éléments.

Comparer

le délai d'importation

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-bit Python donne

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

Tandis que le C++ donne (construit avec MSVC, 64-bit, les Optimisations activées)

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

C++ debug produit:

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

Le point ici est que, avec Python, vous pouvez obtenir une de 7 à 8% d'amélioration de la performance, et si vous pensez que vous êtes en train de rédiger une haute performance de l'application (ou si vous écrivez quelque chose qui est utilisé dans un service web ou quelque chose), ce n'est pas être sous-estimées, mais vous devrez peut-être repenser votre choix de langue.

Aussi, le code Python ici n'est pas vraiment de code Python. De commutation pour vraiment Pythonesque code ici 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)

Ce qui donne

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

(en 32 bits doGenerator fait mieux que doAllocate).

Ici, l'écart entre doAppend et doAllocate est nettement plus grande.

De toute évidence, les différences vraiment s'appliquent uniquement si vous faites cela plus qu'une poignée de fois, ou si vous faites cela sur un système fortement chargé où ces chiffres vont obtenir à l'échelle de plusieurs ordres de grandeur, ou si vous êtes aux prises avec de beaucoup plus grandes listes.

Le point ici: Faire de la pythonic façon pour les meilleures performances.

Mais si vous vous faites du souci à propos de général, des performances de haut niveau, le Python est la mauvaise langue. Le problème le plus fondamental étant que Python appels de fonction a toujours été jusqu'à 300x plus lent que les autres langues en raison de Python fonctionnalités comme les décorateurs etc (https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Data_Aggregation#Data_Aggregation).

5voto

Jason Wiener Points 41

J’ai couru code de @s .lott et produit la même augmentation de 10 % perf par préallocation. essayé l’idée de @jeremy à l’aide d’un générateur et a pu constater la perf de la gen meilleure que celle de la doAllocate. Pour mes proj l’amélioration de 10 % est important, donc Merci à tous car cela contribue à un bouquet.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X