Python - Tạo một danh sách với dung lượng ban đầu


187

Mã như thế này thường xảy ra:

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

Điều này thực sự chậm nếu bạn sắp thêm hàng ngàn yếu tố vào danh sách của mình, vì danh sách sẽ phải liên tục thay đổi kích thước để phù hợp với các yếu tố mới.

Trong Java, bạn có thể tạo một ArrayList với dung lượng ban đầu. Nếu bạn có một số ý tưởng rằng danh sách của bạn sẽ lớn như thế nào, điều này sẽ hiệu quả hơn rất nhiều.

Tôi hiểu rằng mã như thế này thường có thể được đưa vào lại thành một danh sách hiểu. Tuy nhiên, nếu vòng lặp for / while rất phức tạp, điều này là không khả thi. Có bất kỳ tương đương cho chúng tôi lập trình viên Python?


12
Theo như tôi biết, chúng tương tự như ArrayLists ở chỗ chúng tăng gấp đôi kích thước mỗi lần. Thời gian khấu hao của hoạt động này là không đổi. Nó không phải là một hit lớn như bạn nghĩ.
mmcdole

có vẻ như bạn đúng!
Claudiu

10
Có lẽ việc khởi tạo trước không thực sự cần thiết cho kịch bản của OP, nhưng đôi khi chắc chắn là cần thiết: Tôi có một số mục được lập chỉ mục trước cần được chèn vào một chỉ mục cụ thể, nhưng chúng không theo thứ tự. Tôi cần phát triển danh sách trước thời hạn để tránh IndexErrors. Cảm ơn câu hỏi này.
Neil Traft

1
@Claudiu Câu trả lời được chấp nhận là sai lệch. Các bình luận được đánh giá cao nhất dưới nó giải thích lý do tại sao. Bạn có muốn xem xét chấp nhận một trong những câu trả lời khác không?
Neal Gokli

Câu trả lời:


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

Kết quả . (đánh giá mỗi chức năng 144 lần và trung bình thời lượng)

simple append 0.0102
pre-allocate  0.0098

Kết luận . Nó hầu như không quan trọng.

Tối ưu hóa sớm là gốc rễ của mọi tội lỗi.


18
Điều gì xảy ra nếu bản thân phương thức preallocation (kích thước * [Không]) không hiệu quả? Có phải VM python thực sự phân bổ danh sách cùng một lúc, hoặc tăng dần nó, giống như append () không?
haridsv

9
Chào. Nó có lẽ có thể được thể hiện bằng Python, nhưng chưa ai đăng nó ở đây. Quan điểm của haridsv là chúng tôi chỉ giả sử 'int * list' không chỉ nối vào mục danh sách theo mục. Giả định đó có lẽ là hợp lệ, nhưng quan điểm của haridsv là chúng ta nên kiểm tra điều đó. Nếu nó không hợp lệ, điều đó sẽ giải thích tại sao hai hàm bạn hiển thị mất thời gian gần như giống hệt nhau - bởi vì dưới vỏ bọc, chúng đang thực hiện chính xác cùng một thứ, do đó thực sự chưa thử nghiệm chủ đề của câu hỏi này. Trân trọng!
Jonathan Hartley

135
Điều này không hợp lệ; bạn đang định dạng một chuỗi với mỗi lần lặp, sẽ mất mãi mãi so với những gì bạn đang cố kiểm tra. Ngoài ra, cho rằng 4% vẫn có thể có ý nghĩa tùy thuộc vào tình huống và đó là sự đánh giá thấp ...
Philip Guin

39
Như @Philip chỉ ra kết luận ở đây là sai lệch. Preallocation không quan trọng ở đây vì hoạt động định dạng chuỗi rất tốn kém. Tôi đã thử nghiệm với một hoạt động giá rẻ trong vòng lặp và thấy preallocating nhanh gần gấp đôi.
Keith

11
Câu trả lời sai với nhiều upvote là một gốc rễ khác của tất cả các ác.
Hashimoto

79

Danh sách Python không có phân bổ trước tích hợp. Nếu bạn thực sự cần lập một danh sách và cần tránh chi phí bổ sung (và bạn nên xác minh rằng bạn làm), bạn có thể làm điều này:

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

Có lẽ bạn có thể tránh danh sách bằng cách sử dụng trình tạo thay thế:

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

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

Bằng cách này, danh sách không phải tất cả được lưu trữ trong bộ nhớ, chỉ được tạo khi cần thiết.


7
+1 Máy phát điện thay vì danh sách. Nhiều thuật toán có thể được sửa đổi một chút để làm việc với các trình tạo thay vì các danh sách cụ thể hóa.
S.Lott

máy phát điện là một ý tưởng tốt, đúng. tôi muốn có một cách chung để làm điều đó ngoài việc thiết lập tại chỗ. Tôi đoán sự khác biệt là nhỏ, thoguh.
Claudiu

50

Phiên bản ngắn: sử dụng

pre_allocated_list = [None] * size

để phân bổ trước một danh sách (nghĩa là có thể giải quyết các yếu tố 'kích thước' của danh sách thay vì dần dần hình thành danh sách bằng cách nối thêm). Thao tác này RẤT nhanh, ngay cả trong danh sách lớn. Việc phân bổ các đối tượng mới sẽ được gán sau này cho các phần tử danh sách sẽ mất nhiều thời gian hơn và sẽ là nút cổ chai trong chương trình của bạn, hiệu suất khôn ngoan.

Phiên bản dài:

Tôi nghĩ rằng thời gian khởi tạo nên được tính đến. Vì trong python, mọi thứ đều là tham chiếu, không quan trọng bạn đặt từng phần tử thành Không hoặc chuỗi nào - dù đó chỉ là tham chiếu. Mặc dù sẽ mất nhiều thời gian hơn nếu bạn muốn tạo đối tượng mới cho mỗi phần tử để tham chiếu.

Đối với 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()

Đánh giá:

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

Như bạn có thể thấy, chỉ cần tạo một danh sách lớn các tham chiếu đến cùng một đối tượng Không có thời gian rất ít.

Việc chuẩn bị hoặc gia hạn mất nhiều thời gian hơn (tôi không trung bình bất cứ điều gì, nhưng sau khi chạy điều này một vài lần tôi có thể nói với bạn rằng việc gia hạn và nối thêm mất khoảng thời gian như nhau).

Phân bổ đối tượng mới cho mỗi yếu tố - đó là những gì mất nhiều thời gian nhất. Và câu trả lời của S.Lott thực hiện điều đó - định dạng một chuỗi mới mỗi lần. Điều này không bắt buộc - nếu bạn muốn phân bổ trước một số dung lượng, chỉ cần tạo một danh sách Không có, sau đó gán dữ liệu cho danh sách các thành phần theo ý muốn. Dù bằng cách nào thì cũng cần nhiều thời gian hơn để tạo dữ liệu hơn là nối / mở rộng danh sách, cho dù bạn tạo dữ liệu trong khi tạo danh sách hay sau đó. Nhưng nếu bạn muốn có một danh sách dân cư thưa thớt, thì bắt đầu với một danh sách Không có gì chắc chắn là nhanh hơn.


hmm thú vị Vì vậy, câu trả lời là - không thực sự quan trọng nếu bạn thực hiện bất kỳ thao tác nào để đưa các yếu tố vào danh sách, nhưng nếu bạn thực sự chỉ muốn một danh sách lớn của tất cả các yếu tố tương tự, bạn nên sử dụng []*phương pháp tiếp cận
Claudiu

26

Cách Pythonic cho việc này là:

x = [None] * numElements

hoặc bất kỳ giá trị mặc định nào bạn muốn chuẩn bị trước, vd

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

[EDIT: Caveat Emptor Các [Beer()] * 99cú pháp tạo ra một Beer và sau đó populates một mảng với sự tham khảo 99 đến trường hợp duy nhất cùng]

Cách tiếp cận mặc định của Python có thể khá hiệu quả, mặc dù hiệu quả đó giảm dần khi bạn tăng số lượng phần tử.

Đối chiếu

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)

với

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

Trên Windows 7 i7 của tôi, Python 64 bit cho

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

Trong khi C ++ cung cấp (được xây dựng với MSVC, 64 bit, bật Tối ưu hóa)

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

Bản dựng gỡ lỗi C ++ tạo ra:

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

Vấn đề ở đây là với Python, bạn có thể đạt được cải thiện hiệu suất 7-8% và nếu bạn nghĩ rằng bạn đang viết một ứng dụng hiệu suất cao (hoặc nếu bạn đang viết một cái gì đó được sử dụng trong dịch vụ web hoặc một cái gì đó) thì điều đó không được đánh hơi, nhưng bạn có thể cần suy nghĩ lại về lựa chọn ngôn ngữ của mình.

Ngoài ra, mã Python ở đây không thực sự là mã Python. Chuyển sang mã Pythonesque thực sự ở đây cho hiệu suất tốt hơn:

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)

Mà cho

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

(trong 32-bit doGenerator làm tốt hơn do Allocate).

Ở đây, khoảng cách giữa doAppend và do Allocate lớn hơn đáng kể.

Rõ ràng, sự khác biệt ở đây thực sự chỉ áp dụng nếu bạn đang thực hiện việc này nhiều hơn một vài lần hoặc nếu bạn đang thực hiện việc này trên một hệ thống tải nặng, nơi những con số đó sẽ được thu nhỏ theo các đơn đặt hàng lớn hoặc nếu bạn đang xử lý danh sách lớn hơn đáng kể.

Vấn đề ở đây: Làm theo cách pythonic để có hiệu suất tốt nhất.

Nhưng nếu bạn lo lắng về hiệu suất chung, cấp cao, Python là ngôn ngữ sai. Vấn đề cơ bản nhất là các lệnh gọi hàm Python có truyền thống chậm hơn tới 300 lần so với các ngôn ngữ khác do các tính năng của Python như trang trí, v.v ( https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Data_Aggregation#Data_Aggregation ).


@NilsvonBarth C ++ không cótimeit
kfsone

Pythontimeit, cái mà bạn nên sử dụng khi định thời gian mã Python của bạn; Tôi không nói về C ++, rõ ràng.
Nils von Barth

4
Đây không phải là câu trả lời đúng. bottles = [Beer()] * 99không tạo ra 99 đối tượng Bia. Thay vào đó, tạo một đối tượng Bia với 99 tham chiếu đến nó. Nếu bạn thay đổi nó, tất cả các yếu tố trong danh sách sẽ bị thay đổi, nguyên nhân (bottles[i] is bootles[j]) == Truecho mọi i != j. 0<= i, j <= 99.
tuyên bố

@erhesto Bạn đánh giá câu trả lời là không đúng, vì tác giả đã sử dụng tài liệu tham khảo làm ví dụ để điền vào danh sách? Đầu tiên, không ai yêu cầu tạo 99 đối tượng Bia (so với một đối tượng và 99 tài liệu tham khảo). Trong trường hợp chuẩn bị trước (những gì anh ấy nói về), nhanh hơn là tốt hơn, vì giá trị sẽ được thay thế sau này. Thứ hai, câu trả lời không phải là về tài liệu tham khảo hoặc đột biến. Bạn đang thiếu bức tranh lớn.
Yongwei Wu

@YongweiWu Bạn thực sự đúng. Ví dụ này không làm cho toàn bộ câu trả lời không chính xác, nó có thể chỉ gây hiểu lầm và đơn giản là nó đáng để đề cập.
tuyên bố

8

Như những người khác đã đề cập, cách đơn giản nhất để chọn trước một danh sách với NoneTypecác đối tượng.

Điều đó đang được nói, bạn nên hiểu cách danh sách Python thực sự hoạt động trước khi quyết định điều này là cần thiết. Trong triển khai CPython của một danh sách, mảng bên dưới luôn được tạo với phòng trên cao, với kích thước lớn hơn dần dần ( 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), do đó việc thay đổi kích thước danh sách không xảy ra quá thường xuyên.

Do hành vi này, hầu hết các list.append() chức năng là O(1)phức tạp cho các phụ lục, chỉ tăng độ phức tạp khi vượt qua một trong những ranh giới này, tại thời điểm đó độ phức tạp sẽ là O(n). Hành vi này là nguyên nhân dẫn đến sự gia tăng tối thiểu thời gian thực hiện trong câu trả lời của S. Lott.

Nguồn: http://www.laurentluce.com/posts/python-list-im THỰCation /


4

tôi đã chạy mã của @ s.lott và tạo ra mức tăng hoàn hảo 10% bằng cách phân bổ trước. đã thử ý tưởng của @ jeremy bằng cách sử dụng một trình tạo và có thể thấy sự hoàn hảo của gen tốt hơn so với doAllocate. Đối với proj của tôi, vấn đề cải thiện 10%, vì vậy cảm ơn mọi người vì điều này giúp ích rất nhiều.

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
"Đối với proj của tôi, vấn đề cải thiện 10%"? Có thật không? Bạn có thể chứng minh rằng việc phân bổ danh sách là các nút cổ chai? Tôi muốn xem thêm về điều đó. Bạn có một blog nơi bạn có thể giải thích điều này thực sự hữu ích như thế nào không?
S.Lott

2
@ S.Lott thử tăng kích thước lên theo một độ lớn; hiệu suất giảm 3 bậc độ lớn (so với C ++ khi hiệu suất giảm hơn một chút so với một độ lớn).
kfsone

2
Đây có thể là trường hợp bởi vì khi một mảng phát triển, nó có thể phải được di chuyển xung quanh trong bộ nhớ. (Hãy nghĩ về cách các vật thể được lưu trữ ở cái này đến cái khác.)
Evgeni Sergeev

3

Mối quan tâm về phân bổ trước trong Python phát sinh nếu bạn đang làm việc với numpy, có nhiều mảng giống C hơn. Trong trường hợp này, mối quan tâm trước khi phân bổ là về hình dạng của dữ liệu và giá trị mặc định.

Hãy xem xét numpy nếu bạn đang thực hiện tính toán số trong danh sách lớn và muốn hiệu suất.


0

Đối với một số ứng dụng, một từ điển có thể là những gì bạn đang tìm kiếm. Ví dụ: trong phương thức find_totient, tôi thấy việc sử dụng từ điển thuận tiện hơn vì tôi không có chỉ số bằng không.

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

Vấn đề này cũng có thể được giải quyết với một danh sách preallocated:

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

Tôi cảm thấy rằng điều này không thanh lịch và dễ bị lỗi vì tôi đang lưu trữ Không ai có thể ném ngoại lệ nếu tôi vô tình sử dụng sai và vì tôi cần suy nghĩ về các trường hợp cạnh mà bản đồ cho phép tôi tránh.

Đúng là từ điển sẽ không hiệu quả, nhưng như những người khác đã nhận xét, những khác biệt nhỏ về tốc độ không phải lúc nào cũng có giá trị bảo trì đáng kể .


Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.