Python-初期容量のリストを作成する


187

このようなコードはしばしば発生します:

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

新しい要素に合わせてリストのサイズを常に変更する必要があるため、数千の要素をリストに追加しようとすると、これは本当に遅くなります。

Javaでは、初期容量のArrayListを作成できます。リストの大きさがいくらかわかっている場合、これははるかに効率的です。

このようなコードは、多くの場合、リスト内包にリファクタリングできることを理解しています。for / whileループが非常に複雑な場合は、これは不可能です。私たちのPythonプログラマーに相当するものはありますか?


12
私が知る限り、サイズは毎回2倍になるという点でArrayListsに似ています。この操作の償却時間は一定です。それはあなたが思うほどのパフォーマンスのヒットではありません。
mmcdole 2008年

あなたが正しいようです!
Claudiu

10
おそらく、OPのシナリオでは事前初期化は厳密には必要ありませんが、場合によっては確実に必要になることがあります。特定のインデックスに挿入する必要のある事前にインデックス化されたアイテムがいくつかありますが、それらは順序が狂っています。IndexErrorsを回避するには、事前にリストを拡張する必要があります。この質問をありがとう。
Neil Traft 2012

1
@Claudiu受け入れられた答えは誤解を招くものです。その下で最も支持されているコメントが理由を説明しています。他の答えの1つを受け入れることを検討しますか?
ニールゴクリ

回答:


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

結果。(各関数を144回評価し、期間を平均化)

simple append 0.0102
pre-allocate  0.0098

結論。それはほとんど問題ではありません。

時期尚早の最適化は、すべての悪の根源です。


18
事前割り当て方法(size * [None])自体が非効率的である場合はどうなりますか?Python VMは、append()のように、実際にリストを一度に割り当てますか、それとも徐々に増やしますか?
haridsv 2009年

9
ねえ。おそらくPythonで表現できますが、まだ誰もここに投稿していません。haridsvのポイントは、 'int * list'がアイテムごとにリストに追加されるだけではないと想定していることです。その仮定はおそらく有効ですが、haridsvのポイントは、それをチェックする必要があるということでした。それが有効ではない場合、それはあなたが示した2つの関数がほとんど同じ時間をとる理由を説明します-カバーの下では、それらはまったく同じことをしているため、この質問の主題を実際にテストしていません。宜しくお願いします!
ジョナサンハートレー

135
これは無効です。文字列を繰り返しごとにフォーマットします。これは、テストしようとしているものに比べて永遠にかかります。さらに、状況によっては4%が依然として有意である可能性があり、過小評価であることを考えると
Philip Guin

39
@Philipが指摘するように、ここでの結論は誤解を招くものです。文字列のフォーマット操作にはコストがかかるため、ここでは事前割り当ては重要ではありません。ループ内の安価な操作でテストしたところ、事前割り当てがほぼ2倍高速であることがわかりました。
キース

11
多くの賛成票による誤った回答は、すべての悪の根源です。
橋本

79

Pythonリストには組み込みの事前割り当てはありません。本当にリストを作成する必要があり、追加のオーバーヘッドを回避する必要がある場合(そしてそれを確認する必要があります)、これを行うことができます:

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

おそらく代わりにジェネレータを使用することでリストを回避できます:

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

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

この方法では、リストがすべてメモリにすべて保存されるわけではなく、単に必要に応じて生成されます。


7
リストの代わりに+1ジェネレータ。多くのアルゴリズムは、完全に具体化されたリストの代わりにジェネレーターで動作するようにわずかに修正できます。
S.Lott、2008年

ジェネレーターは良いアイデアです。インプレースでの設定以外に、それを行うための一般的な方法が必要でした。違いは小さいと思います。
Claudiu

50

ショートバージョン:使用

pre_allocated_list = [None] * size

リストを事前に割り当てる(つまり、リストを追加して徐々にリストを形成するのではなく、リストの「サイズ」要素に対応できるようにする)。この操作は、大きなリストであっても非常に高速です。後でリスト要素に割り当てられる新しいオブジェクトを割り当てると、はるかに時間がかかり、パフォーマンス面でプログラムのボトルネックになります。

ロングバージョン:

初期化時間も考慮に入れるべきだと思います。Pythonではすべてが参照であるため、各要素をNoneに設定するか、文字列に設定するかは関係ありません。どちらにしても、それは参照にすぎません。ただし、参照する要素ごとに新しいオブジェクトを作成する場合は、さらに時間がかかります。

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

評価:

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

ご覧のとおり、同じNoneオブジェクトへの参照の大きなリストを作成するだけでは、ほとんど時間がかかりません。

プリペンドまたは拡張に時間がかかります(平均化はしませんでしたが、これを数回実行した後、拡張と追加にほぼ同じ時間がかかることを伝えることができます)。

各要素に新しいオブジェクトを割り当てる-これは最も時間がかかるものです。そして、S.Lottの答えはそれを行います-毎回新しい文字列をフォーマットします。これは厳密には必要ありません。スペースを事前に割り当てたい場合は、Noneのリストを作成し、リスト要素にデータを自由に割り当てます。どちらの方法でも、リストの作成中に生成した場合でもその後で生成した場合でも、リストの追加/拡張よりもデータの生成に時間がかかります。しかし、人口がまばらなリストが必要な場合は、Noneのリストから始めるのが間違いなく高速です。


うーん面白い。したがって、答えは間違いではありません-リストに要素を配置するための操作を行っているかどうかは問題ではありませんが、同じ要素すべての大きなリストが本当に必要な場合は、[]*アプローチを使用する必要があります
Claudiu

26

これをPythonで行う方法は次のとおりです。

x = [None] * numElements

または、事前ポップしたいデフォルト値、たとえば

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

[EDIT:買主の危険負担[Beer()] * 99構文が作成するものを Beerした後、同じ単一のインスタンスに99個の参照を持つ配列を移入]

Pythonのデフォルトのアプローチはかなり効率的ですが、要素の数を増やすと効率は低下します。

比較する

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)

#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では、64ビットPythonが

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

C ++が提供する(MSVC、64ビット、最適化を有効にして構築)

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

C ++デバッグビルドは以下を生成します。

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

ここでのポイントは、Pythonを使用すると7〜8%のパフォーマンスの向上を達成できることです。高性能アプリを作成している場合(またはWebサービスなどで使用されているものを作成している場合)これは手探りではありませんが、言語の選択を再考する必要があるかもしれません。

また、ここのPythonコードは実際にはPythonコードではありません。ここで本当にPythonesqueコードに切り替えると、パフォーマンスが向上します。

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)

与える

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

(32ビットのdoGeneratorはdoAllocateよりも優れています)。

ここでは、doAppendとdoAllocateの間のギャップが大幅に大きくなっています。

明らかに、ここでの違いは、これをほんの数回以上実行している場合、またはこれらの数値が桁違いにスケールアウトされる負荷の高いシステムでこれを実行している場合、またはかなり大きなリスト。

ここでのポイント:最高のパフォーマンスを得るには、Pythonの方法を使用してください。

しかし、一般的な高レベルのパフォーマンスについて心配している場合、Pythonは間違った言語です。最も基本的な問題は、デコレータなどのPythonの機能(https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Data_Aggregation#Data_Aggregation)により、Python関数呼び出しが他の言語よりも最大300倍遅くなっていることです。


@NilsvonBarth C ++にはありませんtimeit
kfsone

Pythonにはがありtimeit、Pythonコードのタイミングをとるときに使用する必要があります。もちろん、C ++のことではありません。
Nils von Barth、2015年

4
これは正解ではありません。bottles = [Beer()] * 9999個のビールオブジェクトを作成しません。代わりに、99個の参照を持つ1つのBeerオブジェクトを作成します。変更する場合、リストのすべての要素が変更さ(bottles[i] is bootles[j]) == Trueれますi != j. 0<= i, j <= 99
erhesto 2018

@erhesto作者が参考文献を例としてリストに記入したため、答えは正しくないと判断しましたか?まず、99個のビールオブジェクトを作成する必要はありません(1つのオブジェクトと99個の参照に対して)。事前入力(彼が話したこと)の場合、値は後で置き換えられるため、より速い方が良いです。第二に、答えはリファレンスや突然変異についてではありません。全体像を見逃している。
Yongwei Wu

@YongweiWuあなたは正しいです。この例は、全体の答えを間違ったものにするものではありません。誤解を招く可能性があるため、言及するだけの価値があります。
erhesto 2018年

8

他の人が述べたように、リストを事前シードする最も簡単な方法は NoneTypeオブジェクトを。

そうは言っても、これが必要であると判断する前に、Pythonリストが実際に機能する方法を理解する必要があります。リストのCPython実装では、基になる配列は常にオーバーヘッドルームを使用して作成され、徐々にサイズが大きく( 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)なるため、リストのサイズ変更はほとんど発生しません。

この動作のため、ほとんどの list.append()関数はO(1)追加の複雑さであり、これらの境界の1つを超えると複雑さが増します。O(n)。この動作は、S。Lottの回答で実行時間の最小限の増加につながるものです。

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


4

@ s.lottのコードを実行し、事前に割り当てることで同じ10%のパフォーマンス向上を実現しました。ジェネレータを使用して@jeremyのアイデアを試してみたところ、doAllocateよりもgenのパフォーマンスがよくわかりました。私のプロジェクトでは10%の改善が重要なので、これはたくさんの助けになるのでみんなに感謝します。

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
「私のプロジェクトでは、10%の改善が重要です」?本当に?あなたはできることを証明リストの割り当てがあるとボトルネック?それについてもっと知りたいのですが。これが実際にどのように役立ったかを説明できるブログがありますか?
S.Lott 09/10/28

2
@ S.Lottは、サイズを1桁増やします。パフォーマンスが3桁低下します(パフォーマンスが1桁以上低下するC ++と比較して)。
kfsone 2014年

2
これは、配列が大きくなると、メモリ内で移動する必要がある場合があるためです。(オブジェクトが次々にそこに格納される方法を考えてください。)
Evgeni Sergeev

3

Cのような配列が多いnumpyを使用している場合、Pythonでの事前割り当てに関する懸念が生じます。この場合、事前割り当ての問題は、データの形状とデフォルト値に関するものです。

大規模なリストで数値計算を実行していてパフォーマンスが必要な場合は、numpyを検討してください。


0

一部のアプリケーションでは、辞書が必要な場合があります。たとえば、find_totientメソッドでは、インデックスがゼロではないため、辞書を使用する方が便利です。

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

この問題は、事前に割り当てられたリストでも解決できます。

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

Noneを格納しているため、誤って使用すると例外がスローされる可能性があり、マップで回避できるエッジケースについて考慮する必要があるため、これはエレガントでバグが発生しやすいと思います。

確かに辞書はそれほど効率的ではありませんが、他の人がコメントしたように、速度の小さな違いは、重大なメンテナンスの危険に必ずしも値するわけではありません。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.