Python-초기 용량을 가진 목록 만들기


188

이와 같은 코드가 자주 발생합니다.

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

목록에 새 요소에 맞게 크기를 조정해야하므로 수천 개의 요소를 목록에 추가하려는 경우에는 실제로 속도가 느립니다.

Java에서는 초기 용량으로 ArrayList를 작성할 수 있습니다. 목록이 얼마나 큰지 알면 훨씬 효율적입니다.

나는 이와 같은 코드가 종종 목록 이해력으로 리팩터링 될 수 있음을 이해합니다. 그러나 for / while 루프가 매우 복잡한 경우 이는 불가능합니다. 우리 파이썬 프로그래머에게 동등한 것이 있습니까?


12
내가 아는 한, 그들은 매번 크기를 두 배로 늘린다는 점에서 ArrayLists와 유사합니다. 이 작업의 상각 시간은 일정합니다. 당신이 생각하는 것만 큼 큰 성능 저하는 아닙니다.
mmcdole

당신이 옳은 것 같습니다!
Claudiu

11
OP의 시나리오에는 사전 초기화가 꼭 필요한 것은 아니지만 때로는 꼭 필요한 경우가 있습니다. 특정 색인에 삽입해야하는 사전 색인 된 항목이 많이 있지만 순서가 잘못되었습니다. IndexErrors를 피하기 위해 미리 목록을 늘려야합니다. 이 질문에 감사드립니다.
Neil Traft

1
@Claudiu 허용되는 답변이 잘못되었습니다. 그 아래에 가장 많이 언급 된 의견이 그 이유를 설명합니다. 다른 답변 중 하나를 수락 하시겠습니까?
닐 Gokli

답변:


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

9
야. 아마도 파이썬으로 표현 될 수는 있지만 아직 아무도 그것을 게시하지 않았습니다. haridsv의 요점은 'int * list'가 항목별로 목록 항목에 추가되지 않는다고 가정하고 있다는 것입니다. 그 가정은 아마 유효하지만 haridsv의 요점은 우리가 그것을 확인해야한다는 것입니다. 그것이 유효하지 않다면, 그것은 당신이 보여준 두 기능이 거의 동일한 시간을 소비하는 이유를 설명 할 것입니다. 친애하는!
Jonathan Hartley

136
유효하지 않습니다. 각 반복마다 문자열을 형식화하고 있습니다. 테스트하려는 대상과 관련하여 영원히 걸립니다. 또한 상황에 따라 4 %가 여전히 중요 할 수 있으며 과소 평가 된 수치입니다.
Philip Guin

40
@Philip이 지적한 것처럼 여기에 결론이 잘못되었습니다. 문자열 형식화 작업이 비싸기 때문에 사전 할당은 여기서 중요하지 않습니다. 루프에서 저렴한 작업으로 테스트했으며 사전 할당이 거의 두 배 빠릅니다.
Keith

12
많은 공감대를 가진 오답은 또 다른 악의 근원입니다.
하시모토

80

파이썬리스트에는 내장 된 사전 할당이 없습니다. 실제로 목록을 작성해야하고 추가로 인한 오버 헤드를 피해야하는 경우 (및 확인해야 함) 다음을 수행 할 수 있습니다.

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

발전기는 좋은 생각입니다. 적절한 설정 외에도 일반적인 방법을 원했습니다. 나는 그 차이가 사소한 것 같아요.
Claudiu

50

짧은 버전 : 사용

pre_allocated_list = [None] * size

목록을 미리 할당합니다 (즉, 추가하여 목록을 점차적으로 형성하는 대신 목록의 '크기'요소를 처리 할 수 ​​있도록). 이 작업은 큰 목록에서도 매우 빠릅니다. 나중에 요소를 나열하기 위해 할당 할 새 개체를 할당하면 시간이 많이 걸리고 성능 측면에서 프로그램의 병목 현상이 발생합니다.

긴 버전 :

초기화 시간을 고려해야한다고 생각합니다. 파이썬에서는 모든 것이 참조이므로 각 요소를 없음 또는 문자열로 설정했는지 여부는 중요하지 않습니다. 참조 할 각 요소에 대해 새 객체를 만들려면 시간이 더 오래 걸립니다.

파이썬 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의 대답은 매번 새로운 문자열을 포맷합니다. 꼭 필요한 것은 아닙니다. 일부 공간을 미리 할당하려면 없음 목록을 만든 다음 마음대로 목록 요소에 데이터를 할당하십시오. 어느 쪽이든, 목록을 생성하는 동안 또는 생성 한 후에 목록을 추가 / 확장하는 것보다 데이터를 생성하는 데 시간이 더 걸립니다. 그러나 드물게 채워진 목록을 원하면 없음 목록으로 시작하는 것이 훨씬 빠릅니다.


흠. 따라서 대답은 진드기입니다-요소를 목록에 넣는 작업을 수행하는 경우 실제로 중요하지 않지만 실제로 동일한 요소의 큰 목록을 원할 경우 []*접근 방식을 사용해야합니다
Claudiu

26

이를위한 Pythonic 방법은 다음과 같습니다.

x = [None] * numElements

또는 미리 채울 기본값, 예를 들어

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

[편집 : emptor 경고[Beer()] * 99구문 생성 Beer 후, 동일한 단일 인스턴스 99 참조 배열을 채우는]

파이썬의 기본 접근 방식은 요소 수를 늘리면 효율성이 떨어지지 만 상당히 효율적 일 수 있습니다.

비교

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

여기서 중요한 점은 파이썬을 사용하면 7-8 %의 성능 향상을 달성 할 수 있으며, 고성능 앱을 작성한다고 생각하거나 웹 서비스 또는 무언가에 사용되는 것을 작성한다고 생각되면 스니핑되지는 않지만 언어 선택을 다시 생각해야 할 수도 있습니다.

또한 여기의 파이썬 코드는 실제로 파이썬 코드가 아닙니다. 진정한 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의 간격이 상당히 큽니다.

분명히 여기의 차이점은 소수 이상의 작업을 수행하거나로드가 많은 시스템에서이 작업을 수행하는 경우에만 적용됩니다. 상당히 큰 목록.

요점은 다음과 같습니다. 최고의 성능을 얻으려면 pythonic 방식으로하십시오.

그러나 일반적인 높은 수준의 성능에 대해 걱정한다면 Python은 잘못된 언어입니다. 가장 근본적인 문제는 Python 함수 호출이 데코레이터 등과 같은 Python 기능으로 인해 전통적으로 다른 언어보다 최대 300 배 느리다는 것입니다 ( https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Data_Aggregation#Data_Aggregation ).


@NilsvonBarth C ++에는 없습니다timeit
kfsone

파이썬timeit파이썬 코드 타이밍을 정할 때 사용해야합니다. 나는 분명히 C ++에 대해 이야기하고 있지 않습니다.
Nils von Barth

4
이것은 정답이 아닙니다. bottles = [Beer()] * 9999 맥주 개체를 만들지 않습니다. 대신 99 개의 참조로 하나의 Beer 객체를 만듭니다. 변경하면 목록의 모든 요소가 변경되어 모든 원인이 발생 (bottles[i] is bootles[j]) == True합니다 i != j. 0<= i, j <= 99.
erhesto

@erhesto 저자는 목록을 채우기 위해 예제를 참조로 사용했기 때문에 정답이 맞지 않다고 판단 했습니까? 먼저, 99 개의 Beer 객체를 생성 할 필요가 없습니다 (한 개의 객체와 99 개의 참조와 비교). 사전 채우기 (그가 말한 것)의 경우 나중에 값이 대체되므로 더 빠릅니다. 둘째, 답은 전혀 언급이나 돌연변이에 관한 것이 아닙니다. 큰 그림이 없습니다.
Yongwei Wu

@YongweiWu 실제로 옳습니다. 이 예는 전체 답변을 잘못하지는 않으며 오해의 소지가 있으며 언급 할 가치가 있습니다.
erhesto

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)추가에 대한 복잡성으로, 이러한 경계 중 하나를 넘어갈 때 복잡성이 증가하고 복잡성이 증가합니다 O(n). 이 동작은 S. Lott의 답변에서 실행 시간을 최소로 늘리는 것입니다.

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


4

@ s.lott의 코드를 실행하고 사전 할당에 의해 동일한 10 % perf 증가를 생성했습니다. 발전기를 사용하여 @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

2
@ S.Lott는 크기를 한 단계 씩 올립니다. 성능이 3 배 감소합니다 (C ++과 비교하여 성능이 단일 크기보다 약간 더 떨어짐).
kfsone 2016 년

2
배열이 커지면 메모리에서 이동해야 할 수도 있기 때문입니다. (객체가 하나씩 저장하는 방법을 생각할 수 있습니다.)
예브게니 Sergeev에게

3

파이썬에서 사전 할당에 대한 우려는 C와 같은 배열이 더 많은 numpy로 작업하는 경우 발생합니다. 이 경우 사전 할당 문제는 데이터의 모양과 기본값에 관한 것입니다.

대규모 목록에서 숫자 계산을 수행하고 성능을 원하면 numpy를 고려하십시오.


0

일부 응용 프로그램의 경우 사전이 원하는 것일 수 있습니다. 예를 들어 find_totient 메소드에서 인덱스가 0이 아니기 때문에 사전을 사용하는 것이 더 편리하다는 것을 알았습니다.

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.