2008-11-22 10 views
139

código como esto ocurre a menudo:Python - Crear una lista con una capacidad inicial

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

Esto es muy lento si estás a anexar miles de elementos a la lista, como la lista tendrá que ser constantemente redimensionado para adaptarse a los nuevos elementos.

En Java, puede crear una ArrayList con una capacidad inicial. Si tiene alguna idea de lo grande que será su lista, esto será mucho más eficiente.

Entiendo que código como este a menudo se puede volver a factorizar en una lista de comprensión. Si el ciclo for/while es muy complicado, sin embargo, esto es inviable. ¿Hay algún equivalente para nosotros los programadores de Python?

+7

Por lo que yo sé, son similares a ArrayLists en que el doble de su tamaño cada vez. El tiempo amortizado de esta operación es constante. No es tan grande como un golpe de rendimiento como se podría pensar. – mmcdole

+0

parece que tienes razón! – Claudiu

+6

Quizás la preinicialización no sea estrictamente necesaria para el escenario del PO, pero a veces es definitivamente necesario: tengo varios elementos preindicados que deben insertarse en un índice específico, pero salen de orden. Necesito hacer crecer la lista antes de tiempo para evitar IndexErrors. Gracias por esta pregunta –

Respuesta

104
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 

Resultados. (Evaluar cada función 144 veces y promediar la duración)

simple append 0.0102 
pre-allocate 0.0098 

Conclusión. Apenas importa.

La optimización prematura es la raíz de todos los males.

+0

¡bastante justo! Hice una prueba similar y encontré una diferencia de 1.00 contra 0.8 o más. Supongo que no importa mucho. – Claudiu

+0

Estoy de acuerdo 100% con esta respuesta. Lo único que agregaría es que puede obtener algo de valor usando expresiones de generador como: list (i para i en xrange (tamaño)) –

+9

¿Qué sucede si el método de asignación previa (tamaño * [Ninguno]) es ineficaz? ¿La VM de python realmente asigna la lista de una vez, o la hace crecer gradualmente, al igual que append()? – haridsv

57

Las listas de Python no tienen una preasignación incorporada. Si realmente necesita hacer una lista, y la necesidad de evitar la sobrecarga de añadir (y debe verificar que lo hace), se puede hacer esto:

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

Tal vez podría evitar la lista mediante el uso de un generador vez :

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

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

De esta manera, la lista no está almacenada en memoria, simplemente se genera según sea necesario.

+4

+1 Generadores en lugar de listas. Muchos algoritmos se pueden revisar ligeramente para trabajar con generadores en lugar de listas de material completo. –

+0

generadores son una buena idea, cierto. quería una forma general de hacerlo además de la configuración en el lugar. Supongo que la diferencia es menor, por cierto. – Claudiu

4

ejecuté el código de @ s.lott y produje el mismo 10% de aumento de rendimiento mediante la asignación previa. probé la idea de @ jeremy usando un generador y pude ver el rendimiento del gen mejor que el de doAllocate. Para mi proyecto, la mejora del 10% importa, así que gracias a todos, ya que ayuda un montón.

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

"Para mi proyecto, la mejora del 10% es importante"? De Verdad?Puede ** demostrar ** que la asignación de la lista es ** el ** cuello de botella? Me gustaría ver más sobre eso. ¿Tienes un blog donde podrías explicar cómo esto realmente ayudó? –

+1

@ S.Lott intente aumentar el tamaño en un orden de magnitud; el rendimiento disminuye en 3 órdenes de magnitud (en comparación con C++ donde el rendimiento disminuye ligeramente más de un orden de magnitud). – kfsone

+1

Este podría ser el caso porque a medida que crece una matriz, es posible que tenga que moverse en la memoria. (Piense en cómo se almacenan los objetos uno después del otro.) –

33

versión corta: usar

pre_allocated_list = [None] * size 

comprobar la validez de asignar una lista (es decir, para poder hacer frente a los elementos de 'tamaño' de la lista en lugar de formar gradualmente la lista anexando). Esta operación es MUY rápida, incluso en listas grandes. La asignación de nuevos objetos que luego se asignarán a los elementos de la lista tomará MUCHO más tiempo y será EL cuello de botella en su programa, en cuanto al rendimiento.

Versión larga:

creo que el tiempo de inicialización debe ser tomado en cuenta. Dado que en Python todo es una referencia, no importa si configuras cada elemento en Ninguno o en alguna cadena; en cualquier caso, es solo una referencia. Aunque tomará más tiempo si desea crear un nuevo objeto para cada elemento de referencia.

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

Evaluación:

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

Como se puede ver, sólo hacer una gran lista de referencias al mismo objeto Ninguno lleva muy poco tiempo.

Preestablecer o extender lleva más tiempo (no promedié nada, pero después de ejecutar esto unas cuantas veces puedo decirle que ampliar y agregar toman aproximadamente el mismo tiempo).

Asignación de un nuevo objeto para cada elemento: eso es lo que lleva más tiempo. Y la respuesta de S.Lott lo hace: formatea una nueva cadena cada vez. Lo cual no es estrictamente necesario: si desea asignar previamente espacio, simplemente haga una lista de Ninguno, luego asigne datos a los elementos de la lista a voluntad. De cualquier forma, toma más tiempo generar datos que agregar/ampliar una lista, ya sea que la genere al crear la lista o después de eso. Pero si quieres una lista escasamente poblada, comenzar con una lista de None es definitivamente más rápido.

+0

hmm interesting. por lo tanto, la respuesta debe ser: no importa si está realizando alguna operación para poner elementos en una lista, pero si realmente quiere una gran lista de todos los elementos, debe usar el enfoque '[] *' – Claudiu

12

La forma Pythonic de esto es:

x = [None] * numElements 

o cualquier valor predeterminado que deseen prepop con, por ejemplo,

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

enfoque por defecto de Python puede ser bastante eficiente, aunque que la eficiencia decae a medida que aumenta el número de elementos.

Compare

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

En mi i7 Windows 7, 64 bits de Python da

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

Aunque C++ da (construido con MSVC, de 64 bits, Optimizaciones habilitadas)

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

C++ versión de depuración produce:

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

El punto aquí es que con Python se puede lograr una mejora del rendimiento del 7-8%, y si usted piensa que usted está escribiendo una aplicación de alto rendimiento (o si está escribiendo algo que se usa en un servicio web o algo así), entonces eso no se debe olfatear, pero es posible que deba reconsiderar su elección de idioma.

Además, el código de Python aquí no es realmente el código de Python.El cambio a código verdaderamente Pythonesque aquí da un mejor rendimiento:

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) 

que da

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

(en 32 bits doGenerator hace mejor que doAllocate).

Aquí la brecha entre doAppend y doAllocate es significativamente mayor.

Obviamente, las diferencias aquí realmente solo se aplican si lo hace más de una vez o si lo hace en un sistema muy cargado en el que esos números se ampliarán por órdenes de magnitud, o si estás tratando con listas considerablemente más grandes.

El punto aquí: hazlo de manera pitónica para obtener el mejor rendimiento.

Pero si te preocupa el rendimiento general de alto nivel, Python es el idioma equivocado. El problema más fundamental es que las llamadas a función de Python han sido tradicionalmente hasta 300 veces más lentas que otros lenguajes debido a las características de Python como decoradores, etc. (https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Data_Aggregation#Data_Aggregation).

+0

@ NilsvonBarth C++ no tiene 'timeit' – kfsone

+0

* Python * tiene' timeit', que deberías usar cuando sincronices tu código Python; No estoy hablando de C++, obviamente. –

+0

Manzanas para manzanas. – kfsone

3

Las preocupaciones sobre la preasignación en Python surgen si está trabajando con numpy, que tiene más matrices en forma de C. En este caso, las preocupaciones previas a la asignación se refieren a la forma de los datos y el valor predeterminado.

Considera numpy si estás haciendo cálculos numéricos en listas masivas y quieres un rendimiento.

0

Para algunas aplicaciones, un diccionario puede ser lo que estás buscando. Por ejemplo, en el método find_totient, me pareció más conveniente usar un diccionario ya que no tenía un índice cero.

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

Este problema también podría resolverse con una lista preasignado:

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

Siento que esto no es tan elegante y propenso a errores porque estoy almacenando Ninguno que podría lanzar una excepción si accidentalmente los uso incorrectamente, y porque necesito pensar en casos extremos que el mapa me permite evitar.

Es cierto que el diccionario no será tan eficiente, pero como han comentado otros, pequeñas diferencias de velocidad no son siempre vale la pena significativos peligros de mantenimiento.

6

Como han mencionado otros, la forma más simple de pre-sembrar una lista con objetos NoneType.

Dicho esto, debe comprender la forma en que funcionan realmente las listas de Python antes de decidir si es necesario. En la implementación de CPython de una lista, la matriz subyacente siempre se crea con una sala en la parte superior, en tamaños progresivamente mayores (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), por lo que el cambio de tamaño de la lista no ocurre con tanta frecuencia.

Debido a este comportamiento, la mayoría de list.append() funciones son O(1) complejidad de APPENDs, sólo tener una mayor complejidad a la hora de cruzar uno de estos límites, momento en que la complejidad será O(n). Este comportamiento es lo que conduce al aumento mínimo del tiempo de ejecución en la respuesta de S. Lott.

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

Cuestiones relacionadas