Python: crea un elenco con capacità iniziale


188

Codice come questo spesso accade:

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

Questo è molto lento se stai per aggiungere migliaia di elementi al tuo elenco, poiché l'elenco dovrà essere costantemente ridimensionato per adattarsi ai nuovi elementi.

In Java, è possibile creare un ArrayList con una capacità iniziale. Se hai idea di quanto sarà grande il tuo elenco, questo sarà molto più efficiente.

Capisco che un codice come questo può spesso essere ricondotto in una comprensione dell'elenco. Se il ciclo for / while è molto complicato, tuttavia, questo non è fattibile. C'è qualche equivalente per noi programmatori Python?


12
Per quanto ne so, sono simili alle liste di array in quanto raddoppiano ogni volta le loro dimensioni. Il tempo ammortizzato di questa operazione è costante. Non è un grande successo come potresti pensare.
mmcdole,

sembra che tu abbia ragione!
Claudiu,

11
Forse la pre-inizializzazione non è strettamente necessaria per lo scenario del PO, ma a volte sicuramente è necessaria: ho un numero di elementi preindicizzati che devono essere inseriti in un indice specifico, ma risultano fuori servizio. Devo ampliare l'elenco in anticipo per evitare IndexErrors. Grazie per questa domanda
Neil Traft,

1
@Claudiu La risposta accettata è fuorviante. Il commento più votato sotto di esso spiega perché. Considereresti di accettare una delle altre risposte?
Neal Gokli,

Risposte:


126
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

Risultati . (valuta ogni funzione 144 volte e media la durata)

simple append 0.0102
pre-allocate  0.0098

Conclusione . Poco importa.

L'ottimizzazione prematura è la radice di tutti i mali.


18
Cosa succede se il metodo di preallocazione (dimensione * [Nessuno]) stesso è inefficiente? La VM python alloca effettivamente la lista in una volta, o la espande gradualmente, proprio come farebbe append ()?
Haridsv,

9
Hey. Presumibilmente può essere espresso in Python, ma nessuno lo ha ancora pubblicato qui. Il punto di Haridsv era che stiamo solo supponendo che "int * list" non si limiti ad aggiungere alla lista voce per voce. Questa ipotesi è probabilmente valida, ma il punto di Haridsv era che dovremmo verificarlo. Se non fosse valido, ciò spiegherebbe perché le due funzioni che hai mostrato impiegano tempi quasi identici - perché sotto le coperte, stanno facendo esattamente la stessa cosa, quindi non hanno effettivamente testato l'argomento di questa domanda. I migliori saluti!
Jonathan Hartley,

136
Questo non è valido; stai formattando una stringa con ogni iterazione, il che richiede un'eternità rispetto a ciò che stai provando a testare. Inoltre, dato che il 4% può ancora essere significativo a seconda della situazione, ed è una sottovalutazione ...
Philip Guin

40
Come sottolinea @Philip la conclusione qui è fuorviante. La preallocazione non ha importanza qui perché l'operazione di formattazione delle stringhe è costosa. Ho provato con un'operazione economica nel loop e ho scoperto che il preallocazione è quasi il doppio della velocità.
Keith,

12
Risposte sbagliate con molti voti sono l'ennesima radice di tutti i mali.
Hashimoto,

80

Gli elenchi Python non hanno pre-allocazione integrata. Se hai davvero bisogno di fare un elenco e devi evitare il sovraccarico di aggiunta (e dovresti verificarlo), puoi farlo:

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

Forse potresti evitare l'elenco usando un generatore invece:

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

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

In questo modo, l'elenco non è tutto archiviato in memoria, ma semplicemente generato secondo necessità.


7
Generatori +1 anziché elenchi. Molti algoritmi possono essere leggermente modificati per funzionare con generatori invece di elenchi interamente materializzati.
S. Lott,

i generatori sono una buona idea, vero. volevo un modo generale per farlo oltre all'impostazione sul posto. Immagino che la differenza sia minore, Thoguh.
Claudiu,

50

Versione breve: usare

pre_allocated_list = [None] * size

pre-allocare un elenco (vale a dire, essere in grado di indirizzare gli elementi 'size' dell'elenco invece di formare gradualmente l'elenco aggiungendo). Questa operazione è MOLTO veloce, anche su grandi liste. Allocare nuovi oggetti che verranno successivamente assegnati agli elementi dell'elenco richiederà MOLTO più tempo e sarà IL collo di bottiglia nel tuo programma, dal punto di vista delle prestazioni.

Versione lunga:

Penso che il tempo di inizializzazione dovrebbe essere preso in considerazione. Dato che in Python tutto è un riferimento, non importa se si imposta ogni elemento in None o in qualche stringa - in entrambi i casi è solo un riferimento. Tuttavia, ci vorrà più tempo se si desidera creare un nuovo oggetto per ogni elemento a cui fare riferimento.

Per 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()

Valutazione:

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] = ()

Come puoi vedere, basta creare un grande elenco di riferimenti allo stesso oggetto Nessuno richiede pochissimo tempo.

Preparare o estendere richiede più tempo (non ho fatto nulla nella media, ma dopo averlo eseguito alcune volte posso dirti che l'estensione e l'aggiunta richiedono circa lo stesso tempo).

Allocare un nuovo oggetto per ogni elemento: questo è ciò che richiede più tempo. E la risposta di S.Lott lo fa: formatta ogni volta una nuova stringa. Il che non è strettamente necessario: se si desidera pre-allocare un po 'di spazio, basta creare un elenco di Nessuno, quindi assegnare i dati agli elementi dell'elenco a piacimento. In entrambi i casi è necessario più tempo per generare dati che per aggiungere / estendere un elenco, sia che lo si generi durante la creazione dell'elenco o successivamente. Ma se vuoi un elenco scarsamente popolato, iniziare con un elenco di Nessuno è decisamente più veloce.


hmm interessante. quindi la risposta è acaro: non importa se stai facendo qualsiasi operazione per mettere elementi in un elenco, ma se vuoi davvero un grande elenco di tutti gli stessi elementi dovresti usare l' []*approccio
Claudiu

26

Il modo Pythonic per questo è:

x = [None] * numElements

o qualunque sia il valore predefinito che desideri prepopare, ad es

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

[EDIT: Caveat Emptor La [Beer()] * 99sintassi crea uno Beer e quindi popola un array con 99 riferimenti alla stessa singola istanza]

L'approccio predefinito di Python può essere piuttosto efficiente, sebbene tale efficienza diminuisca quando si aumenta il numero di elementi.

Confrontare

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)

con

#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);
}

Sul mio Windows 7 i7, Python a 64 bit dà

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

Mentre C ++ dà (costruito con MSVC, 64-bit, ottimizzazioni abilitate)

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

La build di debug C ++ produce:

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

Il punto qui è che con Python puoi ottenere un miglioramento delle prestazioni del 7-8% e se pensi di scrivere un'app ad alte prestazioni (o se stai scrivendo qualcosa che viene utilizzato in un servizio web o qualcosa), allora che non deve essere sniffato, ma potrebbe essere necessario ripensare la scelta della lingua.

Inoltre, il codice Python qui non è proprio il codice Python. Passare al vero codice Pythonesque qui offre prestazioni migliori:

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)

Che dà

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

(in doGenerator a 32 bit funziona meglio di doAllocate).

Qui il divario tra doAppend e doAllocate è significativamente maggiore.

Ovviamente, le differenze qui si applicano solo se lo stai facendo più di una manciata di volte o se lo stai facendo su un sistema pesantemente caricato dove quei numeri verranno ridimensionati per ordini di grandezza, o se hai a che fare con elenchi considerevolmente più grandi.

Il punto qui: fallo nel modo pitonico per le migliori prestazioni.

Ma se ti preoccupi delle prestazioni generali di alto livello, Python è la lingua sbagliata. Il problema fondamentale è che la funzione Python chiama è stata tradizionalmente fino a 300 volte più lenta di altre lingue a causa delle funzionalità di Python come decoratori ecc. ( Https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Data_Aggregation#Data_Aggregation ).


@NilsvonBarth C ++ non hatimeit
kfsone

Python ha timeit, che dovresti usare quando cronometri il tuo codice Python; Non sto parlando di C ++, ovviamente.
Nils von Barth,

4
Questa non è una risposta corretta. bottles = [Beer()] * 99non crea 99 oggetti Beer. Invece, crea un oggetto Beer con 99 riferimenti ad esso. Se lo mutassi, tutti gli elementi dell'elenco verranno mutati, causa (bottles[i] is bootles[j]) == Trueper ogni i != j. 0<= i, j <= 99.
Erhesto,

@erhesto Hai giudicato la risposta non corretta, perché l'autore ha usato i riferimenti come esempio per compilare un elenco? Innanzitutto, nessuno è tenuto a creare 99 oggetti Beer (rispetto a un oggetto e 99 riferimenti). Nel caso della prepopolazione (di cui ha parlato), più veloce è meglio, poiché il valore verrà sostituito in seguito. In secondo luogo, la risposta non riguarda affatto riferimenti o mutazione. Ti stai perdendo il quadro generale.
Yongwei Wu,

@YongweiWu In realtà hai ragione. Questo esempio non rende l'intera risposta errata, potrebbe essere solo fuorviante e vale la pena menzionarla.
erhesto,

8

Come altri hanno già detto, il modo più semplice di pre-seedare un elenco con NoneTypeoggetti.

Detto questo, dovresti capire come funzionano effettivamente gli elenchi Python prima di decidere che ciò è necessario. Nell'implementazione di CPython di un elenco, l'array sottostante viene sempre creato con un overhead, in dimensioni progressivamente più grandi ( 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), in modo che il ridimensionamento dell'elenco non avvenga quasi così spesso.

A causa di questo comportamento, la maggior parte delle list.append() funzioni sono O(1)complesse per gli appendi, avendo solo una maggiore complessità quando si attraversa uno di questi confini, a quel punto la complessità sarà O(n). Questo comportamento è ciò che porta al minimo aumento dei tempi di esecuzione nella risposta di S. Lott.

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


4

ho eseguito il codice di @ s.lott e ho prodotto lo stesso aumento del 10% perf pre-allocando. ho provato l'idea di @ jeremy usando un generatore ed è stato in grado di vedere il perf della gen migliore di quello del doAllocate. Per il mio proj il miglioramento del 10% è importante, quindi grazie a tutti perché questo aiuta un sacco.

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

5
"Per il mio proj il miglioramento del 10% è importante"? Veramente? Puoi provare che l'allocazione dell'elenco è il collo di bottiglia? Mi piacerebbe vedere di più su questo. Hai un blog in cui puoi spiegare in che modo ciò ha effettivamente aiutato?
S.Lott

2
@ S. Lott prova ad aumentare le dimensioni di un ordine di grandezza; le prestazioni diminuiscono di 3 ordini di grandezza (rispetto al C ++ dove le prestazioni diminuiscono di poco più di un singolo ordine di grandezza).
kfsone,

2
Questo potrebbe essere il caso perché quando un array cresce, potrebbe essere necessario spostarlo in memoria. (Pensa a come gli oggetti vengono archiviati uno dopo l'altro).
Evgeni Sergeev,

3

Le preoccupazioni sulla pre-allocazione in Python sorgono se stai lavorando con numpy, che ha più array di tipo C. In questo caso, le preoccupazioni di pre-allocazione riguardano la forma dei dati e il valore predefinito.

Prendi in considerazione l'intorpidimento se stai eseguendo calcoli numerici su elenchi di grandi dimensioni e desideri prestazioni.


0

Per alcune applicazioni, un dizionario potrebbe essere quello che stai cercando. Ad esempio, nel metodo find_totient, ho trovato più conveniente usare un dizionario poiché non avevo un indice zero.

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])

Questo problema potrebbe anche essere risolto con un elenco preallocato:

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])

Sento che questo non è così elegante e soggetto a bug perché sto memorizzando Nessuno che potrebbe generare un'eccezione se li uso accidentalmente in modo errato e perché devo pensare a casi limite che la mappa mi consente di evitare.

È vero che il dizionario non sarà così efficiente, ma come altri hanno commentato, le piccole differenze di velocità non sempre valgono rischi significativi per la manutenzione.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.