Python - Crie uma lista com capacidade inicial

188

Código como esse geralmente acontece:

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

Isso é muito lento se você estiver prestes a anexar milhares de elementos à sua lista, pois a lista precisará ser constantemente redimensionada para se ajustar aos novos elementos.

Em Java, você pode criar um ArrayList com uma capacidade inicial. Se você tem alguma idéia de quão grande será sua lista, isso será muito mais eficiente.

Entendo que códigos como esse geralmente podem ser redefinidos em uma compreensão de lista. Se o loop for / while for muito complicado, isso é inviável. Existe algum equivalente para nós programadores Python?

Claudiu
fonte
12
Tanto quanto eu sei, eles são semelhantes aos ArrayLists, pois dobram de tamanho a cada vez. O tempo amortizado desta operação é constante. Não é um sucesso tão grande quanto você imagina.
22008 mmcdole
parece que você está certo!
Claudiu
11
Talvez a pré-inicialização não seja estritamente necessária para o cenário do OP, mas às vezes é definitivamente necessária: tenho vários itens pré-indexados que precisam ser inseridos em um índice específico, mas ficam fora de ordem. Preciso aumentar a lista com antecedência para evitar IndexErrors. Obrigado por esta pergunta.
Neil Traft
1
@ Claudiu A resposta aceita é enganosa. O comentário mais votado abaixo explica o porquê. Você consideraria aceitar uma das outras respostas?
Neal Gokli

Respostas:

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

Resultados . (avalie cada função 144 vezes e calcule a média da duração)

simple append 0.0102
pre-allocate  0.0098

Conclusão . Isso quase não importa.

Otimização prematura é a raiz de todo o mal.

S.Lott
fonte
18
E se o próprio método de pré-alocação (tamanho * [Nenhum]) for ineficiente? A VM python realmente aloca a lista de uma só vez ou aumenta gradualmente, exatamente como o append () faria?
haridsv
9
Ei. Presumivelmente, pode ser expresso em Python, mas ninguém ainda o postou aqui. O ponto de haridsv é que estamos apenas assumindo que 'int * list' não se apega apenas à lista item por item. Essa suposição é provavelmente válida, mas o ponto de haridsv era que deveríamos verificar isso. Se não fosse válido, isso explicaria por que as duas funções mostradas levam tempos quase idênticos - porque, debaixo das cobertas, eles estão fazendo exatamente a mesma coisa, portanto, na verdade, não testaram o assunto desta pergunta. Cumprimentos!
9788 Jonathan
136
Isso não é válido; você está formatando uma sequência com cada iteração, o que leva uma eternidade em relação ao que você está tentando testar. Além disso, dado que 4% ainda pode ser significativa, dependendo da situação, e é uma subestimação ...
Philip Guin
40
Como a @Philip aponta, a conclusão aqui é enganosa. A pré-alocação não importa aqui, porque a operação de formatação de string é cara. Eu testei com uma operação barata no loop e constatei que a pré-alocação é quase duas vezes mais rápida.
Keith
12
Respostas erradas com muitos votos positivos são mais uma raiz de todo mal.
Hashimoto
80

As listas Python não têm pré-alocação embutida. Se você realmente precisa fazer uma lista e evitar a sobrecarga de anexar (e deve verificar se o faz), faça o seguinte:

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

Talvez você possa evitar a lista usando um gerador:

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

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

Dessa forma, a lista nem todos são armazenados na memória, apenas gerados conforme necessário.

Ned Batchelder
fonte
7
+1 Geradores em vez de listas. Muitos algoritmos podem ser revisados ​​levemente para trabalhar com geradores em vez de listas totalmente materializadas.
S.Lott 22/11/2008
geradores são uma boa ideia, é verdade. Eu estava querendo uma maneira geral de fazê-lo, além da configuração no local. Eu acho que a diferença é pequena, thoguh.
Claudiu
50

Versão curta: use

pre_allocated_list = [None] * size

pré-alocar uma lista (ou seja, poder endereçar elementos de 'tamanho' da lista em vez de gradualmente formar a lista anexando). Esta operação é MUITO rápida, mesmo em grandes listas. A alocação de novos objetos que serão atribuídos posteriormente aos elementos da lista levará MUITO mais tempo e será o gargalo no seu programa, em termos de desempenho.

Versão longa:

Eu acho que o tempo de inicialização deve ser levado em consideração. Como no python tudo é uma referência, não importa se você define cada elemento como None ou alguma string - de qualquer forma, é apenas uma referência. Embora demore mais tempo, se você deseja criar um novo objeto para cada elemento a ser referenciado.

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

Avaliação:

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 você pode ver, apenas fazer uma grande lista de referências ao mesmo objeto None leva muito pouco tempo.

Anexar ou estender leva mais tempo (não calculei a média de nada, mas depois de executar isso algumas vezes, posso dizer que estender e anexar levam aproximadamente o mesmo tempo).

Alocar novo objeto para cada elemento - é isso que leva mais tempo. E a resposta de S.Lott faz isso - formata uma nova string sempre. O que não é estritamente necessário - se você quiser pré-alocar algum espaço, faça uma lista de Nenhum e atribua dados aos elementos da lista à vontade. De qualquer maneira, leva mais tempo para gerar dados do que anexar / estender uma lista, seja você gerada durante a criação da lista ou depois disso. Mas se você deseja uma lista pouco povoada, começar com uma lista de Nenhum é definitivamente mais rápido.

LRN
fonte
Hmm interessante. Portanto, a resposta ácaro ser - doesnt realmente importa se você está fazendo qualquer operação de colocar elementos em uma lista, mas se você realmente quer apenas uma grande lista de todos o mesmo elemento você deve usar a []*abordagem
Claudiu
26

O caminho pitônico para isso é:

x = [None] * numElements

ou qualquer valor padrão com o qual você deseja pré-pop-up, por exemplo

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

[EDIT: Caveat Emptor A [Beer()] * 99sintaxe cria uma Beer e preenche uma matriz com 99 referências à mesma instância única]

A abordagem padrão do Python pode ser bastante eficiente, embora essa eficiência decaia à medida que você aumenta o número de elementos.

Comparar

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)

com

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

No meu Windows 7 i7, o Python de 64 bits oferece

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

Enquanto o C ++ fornece (construído com MSVC, 64 bits, otimizações ativadas)

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

A compilação de depuração do C ++ produz:

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

O ponto aqui é que, com o Python, você pode obter uma melhoria de desempenho de 7 a 8%; se você acha que está escrevendo um aplicativo de alto desempenho (ou se está escrevendo algo que é usado em um serviço da Web ou algo assim), então isso não deve ser desprezado, mas pode ser necessário repensar sua escolha de idioma.

Além disso, o código Python aqui não é realmente um código Python. Mudar para o código verdadeiramente Pythonesque aqui oferece melhor desempenho:

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 dá

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

(no doGenerator de 32 bits é melhor que doAllAllate).

Aqui, a diferença entre doAppend e doAllocate é significativamente maior.

Obviamente, as diferenças aqui realmente se aplicam apenas se você estiver fazendo isso mais do que algumas vezes ou se estiver fazendo isso em um sistema muito carregado, em que esses números serão redimensionados por ordens de magnitude ou se você estiver lidando com listas consideravelmente maiores.

O ponto aqui: faça da maneira pitônica o melhor desempenho.

Mas se você está preocupado com o desempenho geral de alto nível, o Python é a linguagem errada. O problema mais fundamental é que as chamadas de função do Python tradicionalmente são até 300 vezes mais lentas que outras linguagens devido a recursos do Python, como decoradores etc. ( https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Data_Aggregation#Data_Aggregation ).

kfsone
fonte
Não @NilsvonBarth C ++ não temtimeit
kfsone
Python possui timeit, que você deve usar quando cronometrar seu código Python; Não estou falando de C ++, obviamente.
Nils von Barth
4
Esta não é a resposta correta. bottles = [Beer()] * 99não cria 99 objetos Beer. Em vez disso, cria um objeto Beer com 99 referências a ele. Se você o alterar, todos os elementos da lista serão alterados, causa (bottles[i] is bootles[j]) == Truepara todos i != j. 0<= i, j <= 99.
erhesto 4/02/18
@erhesto Você julgou a resposta como incorreta, porque o autor usou referências como exemplo para preencher uma lista? Primeiro, ninguém está exigindo a criação de 99 objetos Beer (em comparação a um objeto e 99 referências). No caso da pré-população (sobre o que ele falou), mais rápido é melhor, pois o valor será substituído mais tarde. Segundo, a resposta não é sobre referências ou mutação. Você está perdendo o quadro geral.
Yongwei Wu 29/11
@YongweiWu Você está certo, certo. Este exemplo não torna a resposta inteira incorreta, pode ser apenas enganosa e vale a pena mencionar.
erhesto
8

Como outros mencionaram, a maneira mais simples de pré-propagar uma lista com NoneTypeobjetos.

Dito isto, você deve entender como as listas Python realmente funcionam antes de decidir que isso é necessário. Na implementação de uma lista do CPython, a matriz subjacente é sempre criada com espaço para sobrecarga, em tamanhos progressivamente maiores ( 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), para que o redimensionamento da lista não ocorra com tanta frequência.

Devido a esse comportamento, a maioria das list.append() funções é de O(1)complexidade para anexos, tendo apenas uma complexidade aumentada ao cruzar um desses limites, momento em que a complexidade será O(n). Esse comportamento é o que leva ao aumento mínimo no tempo de execução na resposta de S. Lott.

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

Russell Troxel
fonte
4

Corri o código do @ s.lott e produzi o mesmo aumento de 10% no perf pré-alocando. tentou a idéia de @ jeremy usando um gerador e foi capaz de ver o desempenho do gen melhor do que o do doAllocate. Para o meu projeto, a melhoria de 10% é importante, portanto, obrigado a todos, pois isso ajuda bastante.

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
Jason Wiener
fonte
5
"Para o meu proj, a melhoria de 10% é importante"? Realmente? Você pode provar que a alocação de lista é o gargalo? Eu gostaria de ver mais sobre isso. Você tem um blog onde você pode explicar como isso realmente ajudou?
S.Lott
2
@ S.Lott tente aumentar o tamanho em uma ordem de magnitude; o desempenho cai em três ordens de magnitude (em comparação com o C ++, em que o desempenho cai um pouco mais do que uma única ordem de magnitude).
kfsone
2
Pode ser esse o caso, porque, à medida que uma matriz cresce, ela pode ter que ser movimentada na memória. (Pense em como os objetos são armazenados lá um após o outro.)
Evgeni Sergeev
3

As preocupações com a pré-alocação no Python surgem se você estiver trabalhando com o numpy, que possui mais matrizes do tipo C. Nesse caso, as preocupações de pré-alocação são sobre a forma dos dados e o valor padrão.

Considere entorpecido se estiver fazendo cálculos numéricos em listas massivas e desejar desempenho.

J450n
fonte
0

Para alguns aplicativos, um dicionário pode ser o que você está procurando. Por exemplo, no método find_totient, achei mais conveniente usar um dicionário, pois não tinha um índice 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])

Esse problema também pode ser resolvido com uma lista pré-alocada:

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

Sinto que isso não é tão elegante e propenso a erros, porque estou armazenando o None, o que poderia gerar uma exceção se eu acidentalmente os usar incorretamente e porque preciso pensar em casos extremos que o mapa me permita evitar.

É verdade que o dicionário não será tão eficiente, mas como outros comentaram, pequenas diferenças de velocidade nem sempre valem riscos significativos de manutenção.

Josiah Yoder
fonte