Python - Створення списку з початковою ємністю


188

Такий код часто трапляється:

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

Це дуже повільно, якщо ви збираєтеся додати до свого списку тисячі елементів, оскільки список доведеться постійно змінювати, щоб відповідати новим елементам.

У Java ви можете створити ArrayList з початковою ємністю. Якщо ви маєте уявлення про те, наскільки великий буде ваш список, це буде набагато ефективніше.

Я розумію, що такий код часто може бути повторно врахований для розуміння списку. Якщо цикл for / while дуже складний, це неможливо. Чи є еквівалент для нас програмістів Python?


12
Наскільки я знаю, вони схожі на ArrayLists тим, що щоразу подвоюють свій розмір. Амортизований час цієї операції є постійним. Це не такий великий хіт на виставу, як можна було б подумати.
mmcdole

здається, ти маєш рацію!
Клавдіу

11
Можливо, попередня ініціалізація не є строго необхідною для сценарію ОП, але іноді вона обов'язково потрібна: у мене є ряд попередньо індексованих елементів, які потрібно вставити за певним індексом, але вони виходять з ладу. Мені потрібно виробити список заздалегідь, щоб уникнути IndexErrors. Дякую за це запитання.
Ніл Трафт

1
@Claudiu Прийнята відповідь вводить в оману. Найпопулярніший коментар під ним пояснює, чому. Чи можете ви прийняти одну з інших відповідей?
Ніл Гоклі

Відповіді:


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
Що робити, якщо метод попереднього розміщення (розмір * [Немає]) сам по собі неефективний? Чи дійсно VM python виділяє список одразу чи збільшує його поступово, як і додаток ()?
haridsv

9
Гей. Імовірно, це можна виразити в Python, але ніхто ще не розмістив його тут. Точка haridsv полягала в тому, що ми просто припускаємо, що "int * list" не просто додає до списку пункт за пунктом. Це припущення, ймовірно, справедливе, але точка Харидсва полягала в тому, що ми повинні це перевірити. Якщо воно не було дійсним, це пояснило б, чому дві функції, які ви показали, займають майже однакові часи - адже під обкладинками вони роблять абсолютно те саме, отже, насправді не перевірили тему цього питання. З найкращими побажаннями!
Джонатан Хартлі

136
Це не вірно; ви форматуєте рядок з кожною ітерацією, яка займає назавжди відносно того, що ви намагаєтеся перевірити. Крім того, враховуючи, що 4% все ще можуть бути вагомими залежно від ситуації, і це недооцінка ...
Філіп Гін

40
Як зазначає @Philip, висновок тут вводить в оману. Попереднє розміщення тут не має значення, оскільки операція форматування рядків дорога. Я перевірив дешеву операцію в циклі і виявив, що попереднє розташування майже вдвічі швидше.
Кіт

12
Неправильні відповіді з багатьма відгуками - ще один корінь усього зла.
Хашимото

80

У списках 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

генератори - хороша ідея, правда. я хотів загальний спосіб зробити це, крім встановлення на місці. Я здогадуюсь, різниця незначна, тогу.
Клавдіу

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 займає зовсім небагато часу.

Попередження або продовження займає більше часу (я нічого не оцінював, але після декількох разів, я можу сказати, що розширення та додавання займають приблизно однаковий час).

Виділення нового об'єкта для кожного елемента - саме на це потрібно найбільше часу. І відповідь С.Лотта робить це - кожен раз формує новий рядок. Що не потрібно суворо - якщо ви хочете заздалегідь виділити деякий простір, просто складіть список "Немає", а потім призначте дані для елементів списку за бажанням. Будь-який спосіб потребує більше часу для створення даних, ніж для додавання / розширення списку, незалежно від того, створюєте ви його під час створення списку чи після цього. Але якщо ви хочете малонаселений список, то, починаючи зі списку None, безумовно, швидше.


хм цікаво. тож відповідь кліща - це не має значення, чи робиш ви якусь операцію, щоб внести елементи до списку, але якщо ви просто хочете великий список все того ж елемента, вам слід скористатися []*підходом
Клавді

26

Пітонічний шлях для цього:

x = [None] * numElements

або будь-яке значення за замовчуванням, яке ви хочете доповнити, наприклад

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

[EDIT: Caveat Emptor[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% підвищення продуктивності, і якщо ви думаєте, що пишете високоефективний додаток (або якщо ви пишете те, що використовується у веб-службі чи щось), то це не слід нюхати, але вам може знадобитися переосмислити свій вибір мови.

Також код Python тут насправді не є кодом 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():
    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 традиційно піднімаються на 300 разів повільніше, ніж інші мови через функції Python, такі як декоратори тощо ( https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Data_Aggregation#Data_Aggregation ).


@NilsvonBarth C ++ не маєtimeit
kfsone

У Python є timeit, який слід використовувати під час встановлення часу вашого Python-коду; Я не кажу про C ++, очевидно.
Нілс фон Барт

4
Це неправильна відповідь. bottles = [Beer()] * 99не створює 99 об'єктів пива. Натомість створює один об’єкт Beer з 99 посиланнями на нього. Якщо ви будете його вимкнути, усі елементи у списку будуть вимкнено, причиною (bottles[i] is bootles[j]) == Trueдля кожного i != j. 0<= i, j <= 99.
erhesto

@erhesto Ви оцінили відповідь неправильною, оскільки автор використав посилання на приклад для заповнення списку? По-перше, нікому не потрібно створювати 99 об'єктів пива (проти одного об'єкта та 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). Така поведінка призводить до мінімального збільшення часу виконання у відповіді С. Лотта.

Джерело: http://www.laurentluce.com/posts/python-list-implementation/


4

Я побіг код @ s.lott і створив стільки ж 10% збільшення perf, попередньо виділивши. спробував ідею @ jeremy, використовуючи генератор, і зміг побачити перфект роду краще, ніж у doAllocate. На мій погляд, 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% покращення"? Дійсно? Ви можете довести , що розподіл списку вузького місця ? Я хотів би дізнатися більше про це. Чи є у вас блог, де ви могли б пояснити, як це насправді допомогло?
С.Лотт

2
@ S.Lott спробуйте збільшити розмір на порядок; продуктивність падає на 3 порядки (порівняно з C ++, коли продуктивність падає трохи більше, ніж на один порядок).
kfsone

2
Це може бути так, тому що в міру зростання масиву його, можливо, доведеться переміщувати в пам'яті. (Подумайте, як об’єкти зберігаються один за одним.)
Євгеній Сергеєв

3

Побоювання щодо попереднього розподілу в Python виникають, якщо ви працюєте з numpy, у якому є більше C-подібних масивів. У цьому випадку проблеми попереднього розподілу стосуються форми даних та значення за замовчуванням.

Подумайте про нупі, якщо ви робите чисельні обчислення в масивних списках і хочете швидкодіяти.


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

Я відчуваю, що це не так елегантно і схильне до помилок, тому що я зберігаю жодного, який міг би винести виняток, якщо випадково їх неправильно використовую, і тому що мені потрібно думати про крайові випадки, які карта дозволяє мені уникати.

Це правда словник не буде настільки ж ефективним, але як і інші відзначили, невеликі відмінності в швидкості не завжди варто значних небезпек обслуговування.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.