Python - İlk kapasiteye sahip bir liste oluşturun


188

Bunun gibi kod genellikle olur:

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

Listeye binlerce öğe eklemek üzereyseniz bu gerçekten yavaştır, çünkü listenin yeni öğelere sığacak şekilde sürekli olarak yeniden boyutlandırılması gerekecektir.

Java'da, ilk kapasiteye sahip bir ArrayList oluşturabilirsiniz. Listenizin ne kadar büyük olacağı hakkında bir fikriniz varsa, bu çok daha verimli olacaktır.

Bunun gibi kodların genellikle liste kavrayışına dönüştürülebileceğini anlıyorum. For / while döngüsü çok karmaşıksa, bu mümkün değildir. Bizim için Python programcıları eşdeğeri var mı?


12
Bildiğim kadarıyla, ArrayLists'e benziyorlar, çünkü her seferinde boyutlarını ikiye katlıyorlar. Bu işlemin itfa edilmiş süresi sabittir. Düşündüğünüz gibi bir performans hitinden büyük değil.
mmcdole

haklısın gibi görünüyor!
Claudiu

11
Belki de ön başlatma OP'nin senaryosu için kesinlikle gerekli değildir, ancak bazen kesinlikle gereklidir: Belirli bir dizine eklenmesi gereken bir dizi önceden dizinlenmiş öğem var, ancak sıra dışı. IndexErrors önlemek için liste vaktinde büyümek gerekiyor. Bu soru için teşekkürler.
Neil Traft

1
@Claudiu Kabul edilen cevap yanıltıcı. Altında en yüksek oyu alan yorum nedenini açıklıyor. Diğer cevaplardan birini kabul etmeyi düşünür müsünüz?
Neal Gokli

Yanıtlar:


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

Sonuçlar . (her işlevi 144 kez değerlendirin ve süreyi ortalayın)

simple append 0.0102
pre-allocate  0.0098

Sonuç . Neredeyse önemli değil.

Erken optimizasyon tüm kötülüklerin köküdür.


18
Önceden yerleştirme yönteminin (size * [None]) kendisi verimsizse ne olur? Python VM, listeyi aynen append () 'nin yaptığı gibi bir kerede tahsis mi, yoksa kademeli olarak büyütüyor mu?
haridsv

9
Hey. Muhtemelen Python ile ifade edilebilir, ancak kimse henüz buraya göndermedi. haridsv'in amacı, sadece 'int * list' öğesinin sadece liste öğeye eklenmediğini varsaymaktı. Bu varsayım muhtemelen geçerlidir, ancak haridsv'in amacı bunu kontrol etmemiz gerektiğiydi. Geçerli değilse, bu gösterdiğiniz iki işlevin neden neredeyse aynı zaman aldığını açıklar - çünkü kapakların altında tam olarak aynı şeyi yapıyorlar, bu nedenle bu sorunun konusunu gerçekten test etmediler. Saygılarımla!
Jonathan Hartley

136
Bu geçerli değil; bir dizeyi her yinelemeyle biçimlendiriyorsunuz, bu da test etmeye çalıştığınız şeye göre sonsuza dek sürüyor. Buna ek olarak,% 4 hala duruma bağlı olarak önemli olabilir ve bu bir hafife alıyor ...
Philip Guin

40
@Philip'in belirttiği gibi, buradaki sonuç yanıltıcıdır. Dizgi biçimlendirme işlemi pahalı olduğu için burada yeniden konumlandırma önemli değildir. Döngüde ucuz bir işlemle test ettim ve önceden konumlandırmanın neredeyse iki kat daha hızlı olduğunu gördüm.
Keith

12
Birçok upvotes ile yanlış cevaplar tüm kötülüklerin bir başka köküdür.
Hashimoto

80

Python listelerinde yerleşik ön ayırma yoktur. Gerçekten bir liste yapmanız ve ekleme yükünden kaçınmanız gerekiyorsa (ve bunu yaptığınızı doğrulamanız gerekir), bunu yapabilirsiniz:

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

Belki de bunun yerine bir jeneratör kullanarak listeden kaçınabilirsiniz:

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

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

Bu şekilde, liste sadece bellekte saklanmaz, yalnızca gerektiği gibi oluşturulur.


7
Listeler yerine +1 Üreteçleri. Birçok algoritma, tam materyal listeleri yerine jeneratörlerle çalışacak şekilde biraz revize edilebilir.
S.Lott

jeneratörler iyi bir fikir, doğru. yerinde ayar yanında bunu yapmak için genel bir yol istiyordu. sanırım fark küçük, thoguh.
Claudiu

50

Kısa versiyon: kullanın

pre_allocated_list = [None] * size

bir listeyi önceden ayırmak (yani listeyi yavaş yavaş ekleyerek oluşturmak yerine listenin 'boyut' öğelerini ele almak). Bu işlem büyük listelerde bile ÇOK hızlıdır. Daha sonra liste öğelerine atanacak yeni nesnelerin tahsis edilmesi ÇOK daha uzun sürecek ve programınızda performans açısından darboğaz olacaktır.

Uzun versiyon:

Başlatma zamanının dikkate alınması gerektiğini düşünüyorum. Python'da her şey bir referans olduğundan, her öğeyi Yok veya bir dize olarak ayarlamanız önemli değildir - her iki durumda da sadece bir referanstır. Her öğenin başvurması için yeni bir nesne oluşturmak istiyorsanız daha uzun sürecektir.

Python 3.2 için:

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

Değerlendirme:

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

Gördüğünüz gibi, aynı None nesnesine büyük bir referans listesi hazırlamak çok az zaman alıyor.

Ekleme veya genişletme daha uzun sürer (hiçbir şeyi ortalama yapmadım, ancak bunu birkaç kez çalıştırdıktan sonra, uzatma ve ekleme işlemlerinin kabaca aynı zamanı alacağını söyleyebilirim).

Her öğe için yeni nesne tahsis etmek - en çok zaman alan budur. Ve S.Lott'un cevabı bunu yapıyor - her seferinde yeni bir dize biçimlendiriyor. Bu kesinlikle gerekli değildir - eğer biraz yer ayırmak istiyorsanız, sadece Yok listesini hazırlayın, sonra istediğiniz zaman liste öğelerine veri atayın. Her iki şekilde de, veri oluşturmak, listeyi oluştururken veya listeden sonra oluşturup oluşturmamaya bir liste eklemek / genişletmekten daha fazla zaman alır. Ancak seyrek nüfuslu bir liste istiyorsanız, Hiçbiri listesiyle başlamak kesinlikle daha hızlıdır.


Hmm, ilginç. bu yüzden cevap akarı - öğeleri bir listeye koymak için herhangi bir işlem yapmanız gerçekten önemli değil, ancak gerçekten aynı öğenin büyük bir listesini istiyorsanız []*yaklaşımı kullanmalısınız
Claudiu

26

Bunun için Pythonic yolu:

x = [None] * numElements

veya önceden basmak istediğiniz varsayılan değerden herhangi biri, ör.

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

[DÜZENLEME: Uyarı Emptor[Beer()] * 99 sözdizimi oluşturur tek Beer ve aynı bir örneğine 99 referansları ile bir dizi doldurur]

Python'un varsayılan yaklaşımı oldukça verimli olabilir, ancak eleman sayısını artırdıkça bu verimlilik azalır.

Karşılaştırmak

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)

ile

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

Windows 7 i7 cihazımda 64 bit Python

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

C ++ verirken (MSVC, 64 bit, Optimizasyonlar etkin)

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

C ++ hata ayıklama derlemesi üretir:

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

Buradaki nokta, Python ile% 7-8 performans artışı elde edebileceğiniz ve yüksek performanslı bir uygulama yazdığınızı düşünüyorsanız (veya bir web hizmetinde veya başka bir şeyde kullanılan bir şey yazıyorsanız) koklamayacak, ancak dil seçiminizi yeniden düşünmeniz gerekebilir.

Ayrıca, buradaki Python kodu gerçekten Python kodu değildir. Burada gerçekten Pythonesque koduna geçmek daha iyi performans sağlar:

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)

Hangi verir

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

(32 bit doGenerator'da doAllocate'den daha iyi sonuç verir).

Burada doAppend ve doAllocate arasındaki boşluk önemli ölçüde daha büyük.

Açıkçası, buradaki farklar gerçekten sadece bunu bir avuçtan daha fazla yapıyorsanız veya bunu, bu sayıların büyüklük derecelerine göre ölçekleneceği ağır yüklü bir sistemde yapıyorsanız veya bununla uğraşıyorsanız geçerlidir. oldukça büyük listeler.

Buradaki nokta: En iyi performans için pythonic bir yol yapın.

Ancak genel, üst düzey performans konusunda endişeleniyorsanız, Python yanlış dildir. En temel sorun, dekoratörler vb . Gibi Python özellikleri nedeniyle Python işlev çağrılarının geleneksel olarak diğer dillerden 300 kat daha yavaş olmasıdır ( https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Data_Aggregation#Data_Aggregation ).


@NilsvonBarth C ++ 'a sahip değiltimeit
kfsone

Pythontimeit kodunuzu zamanlarken kullanmanız gereken Python'a sahiptir ; Açıkçası C ++ hakkında konuşmuyorum.
Nils von Barth

4
Bu doğru cevap değil. bottles = [Beer()] * 9999 Bira nesnesi oluşturmaz. Bunun yerine, 99 referansla bir Beer nesnesi oluşturur. Bunu mutasyona uğratırsanız, listedeki tüm öğeler mutasyona uğrar, çünkü (bottles[i] is bootles[j]) == Trueher şey için i != j. 0<= i, j <= 99.
erhesto

@erhesto Yazarı bir listeyi doldurmak için referans olarak kullandığından cevabı doğru olarak değerlendirdiniz mi? Birincisi, hiç kimse 99 Beer nesnesi yaratmaya gerek duymaz (bir nesneye ve 99 referansa karşı). Ön nüfus durumunda (konuştuğu şey), değer daha sonra değiştirileceğinden daha hızlı daha iyidir. İkincisi, cevap referanslar veya mutasyonla ilgili değildir. Büyük resmi kaçırıyorsunuz.
Yongwei Wu

@YongweiWu Haklısın aslında haklısın. Bu örnek tüm cevabı yanlış yapmaz, sadece yanıltıcı olabilir ve bahsetmeye değer.
erhesto

8

Diğerlerinin de belirttiği gibi, bir listeyi NoneTypenesnelerle önceden tohumlamanın en basit yolu .

Bununla birlikte, bunun gerekli olduğuna karar vermeden önce Python listelerinin gerçekte nasıl çalıştığını anlamanız gerekir. Bir listenin CPython uygulamasında, altta yatan dizi her zaman ek oda ile, giderek daha büyük boyutlarda oluşturulur ( 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), böylece listenin yeniden boyutlandırılması neredeyse bu kadar sık ​​olmaz.

Bu davranış nedeniyle, çoğu list.append() fonksiyonları O(1)karmaşıklık olacak bu noktada bu sınırların, birini geçerken sadece artan karmaşıklığa sahip, ekler için karmaşıklığı O(n). Bu davranış, S. Lott'ın cevabında yürütme süresinde minimum artışa neden olan şeydir.

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


4

Ben @ s.lott kodunu koştu ve ön tahsis ile aynı% 10 perf artış üretti. @ jeremy'nin bir jeneratör kullanarak fikrini denedi ve genin mükemmelliğini doAllocate'inkinden daha iyi görebildi. Projem için% 10 iyileştirme önemli, bu yüzden herkese teşekkürler, çünkü bu bir grup yardımcı oluyor.

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
"Projem için% 10 iyileştirme önemlidir"? Gerçekten mi? Sen edebilirsiniz kanıtlamak liste tahsisi olduğunu darboğaz? Bununla ilgili daha fazlasını görmek istiyorum. Bunun gerçekten nasıl yardımcı olduğunu açıklayabileceğiniz bir blogunuz var mı?
S.Lott

2
@ S.Lott büyüklüğü bir büyüklük sırasına kadar darbelemeye çalışın; performans 3 büyüklük azalır (performansın tek bir büyüklük sırasından biraz daha fazla düştüğü C ++ ile karşılaştırıldığında).
kfsone

2
Böyle bir durum olabilir, çünkü bir dizi büyüdükçe, bellekte hareket ettirilmesi gerekebilir. (Nesnelerin birbiri ardına orada nasıl saklandığını düşünün.)
Evgeni Sergeev

3

Python'da ön ayırma ile ilgili endişeler, daha fazla C benzeri diziye sahip numpy ile çalışıyorsanız ortaya çıkar. Bu durumda, ön tahsis endişeleri verinin şekli ve varsayılan değer ile ilgilidir.

Büyük listelerde sayısal hesaplama yapıyorsanız ve performans istiyorsanız, numpy'yi düşünün.


0

Bazı uygulamalar için sözlük, aradığınız şey olabilir. Örneğin, find_totient yönteminde, sıfır endeksim olmadığından sözlük kullanmayı daha uygun buldum.

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

Bu sorun önceden belirlenmiş bir listeyle de çözülebilir:

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

Bu kadar zarif ve hatalara eğilimli olmadığını hissediyorum çünkü yanlışlıkla yanlış kullanırsam bir istisna atabilecek Hiçbiri depolamıyorum ve çünkü haritanın kaçınmamı sağlayan son durumları düşünmem gerekiyor.

Sözlüğün verimli olmayacağı doğrudur, ancak diğerlerinin de söylediği gibi, hızdaki küçük farklılıklar her zaman önemli bakım tehlikelerine değmez .


Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.