Часто бывает такой код:
l = []
while foo:
# baz
l.append(bar)
# qux
Это очень медленно, если вы собираетесь добавить в свой список тысячи элементов, так как список придется постоянно изменять, чтобы он соответствовал новым элементам.
В Java вы можете создать ArrayList с начальной емкостью. Если вы представляете, насколько большим будет ваш список, это будет намного эффективнее.
Я понимаю, что подобный код часто можно преобразовать в понимание списка. Однако, если цикл для / пока очень сложен, это невозможно. Есть ли аналог для нас, программистов на Python?
похоже ты прав!
Возможно, предварительная инициализация не является строго необходимой для сценария OP, но иногда она определенно необходима: у меня есть ряд предварительно проиндексированных элементов, которые нужно вставить по определенному индексу, но они выходят из строя. Мне нужно заранее расширить список, чтобы избежать ошибок IndexErrors. Спасибо за этот вопрос.
@Claudiu Принятый ответ вводит в заблуждение. Комментарий под ним, получивший наибольшее количество голосов, объясняет, почему. Не могли бы вы принять один из других ответов?
while с неясными или недетерминированными условиями завершения itertools и генераторы могут в большинстве случаев спасти логику, чтобы перечислить область понимания.






Списки 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
Таким образом, список не хранится полностью в памяти, а просто создается по мере необходимости.
+1 Генераторы вместо списков. Многие алгоритмы можно немного изменить для работы с генераторами вместо полностью материализованных списков.
генераторы - хорошая идея, правда. Мне нужен был общий способ сделать это помимо настройки на месте. Думаю, разница незначительна, тхогух.
Насколько я понимаю, списки Python уже очень похожи на ArrayLists. Но если вы хотите настроить эти параметры, я нашел этот пост в Интернете, который может быть интересным (в основном, просто создайте свое собственное расширение ScalableList):
http://mail.python.org/pipermail/python-list/2000-May/035082.html
Ссылка не работает: «Не найдено. Запрошенный URL /pipermail/python-list/2000-May/035082.html не найден на этом сервере».
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
Заключение. Это не имеет значения.
Преждевременная оптимизация - корень всех зол.
справедливо! Я сделал аналогичный тест и обнаружил разницу в 1,00 против 0,8 или около того. Думаю, это не имеет большого значения.
Я согласен на 100% с этим ответом. Единственное, что я хотел бы добавить, это то, что вы можете получить некоторый подъем от использования таких выражений генератора, как: list (i for i in xrange (size))
Что делать, если метод предварительного распределения (size * [None]) сам по себе неэффективен? Действительно ли виртуальная машина python выделяет список сразу или постепенно увеличивает его, как это сделала бы append ()?
@haridsv: "Что делать, если метод предварительного распределения (size * [None]) сам по себе неэффективен?" Что это обозначает? Это одинаково эффективно. Это не неэффективно. Это то же самое.
Что мне было интересно, так это то, что если python предварительно выделяет список с окончательным размером или будет увеличивать его только по требованию (то есть, попытайтесь увеличить «размер» в несколько раз, как и для обычного append ()). Просто учитывая все углы ... это достаточно просто для оптимизации, поэтому вполне вероятно, что этот случай уже оптимизирован, но остается предположением, если кто-то точно не знает внутреннее устройство.
@haridsv: (1) Я не понимаю, какие волосы ты секешь. Время то же самое. Я не знаю, о каких еще «оптимизациях» вы могли бы говорить. (2) Внутренние компоненты легко доступны - вы можете прочитать исходный код в любое время.
@haridsv: Я провел несколько тестов, поместив выделение (result=size*[None]) за пределы функции, и не смог найти существенной разницы. Заменив тело цикла на result[i] = i, я получаю 1,13 мс против 0,62 мс с предварительным выделением.
@ S.Lott: haridsv говорит, что если int * list реализуется путем увеличения списка по одному элементу за раз, то неудивительно, что обе реализации, показанные выше, занимают одно и то же время. Если бы это было так, то, возможно, предварительное выделение памяти могло бы быть намного быстрее, но мы еще не нашли кода для проверки этого случая.
@Tartley: «еще не нашел кода для проверки этого случая». Непонятно. Если это невозможно выразить на Python, о чем мы говорим?
Привет. Предположительно, это можно выразить на Python, но здесь его еще никто не опубликовал. Идея haridsv заключалась в том, что мы просто предполагаем, что int * list не просто добавляется к списку элемент за элементом. Это предположение, вероятно, верно, но точка зрения Харидсва заключалась в том, что мы должны это проверить. Если бы это было неверно, это объясняло бы, почему две функции, которые вы показали, выполняются почти одинаково - потому что под крышкой они делают одно и то же, поэтому на самом деле не тестировали предмет этого вопроса. С уважением!
Это неверно; вы форматируете строку с каждой итерацией, что занимает вечность относительно того, что вы пытаетесь протестировать. Кроме того, учитывая, что 4% все еще могут быть значительными в зависимости от ситуации, и это заниженная оценка ...
Будет ли это работать быстрее, если мы уже использовали экземпляр того, что будет окончательным типом данных для предварительного заполнения списка, вместо None?
Другая проблема с этим ответом заключается в том, что использование памяти обоими неодинаково (конечно, асимптотически одинаково, но потенциально в два раза больше, чем должно быть). Настоящее преимущество предварительного распределения заключается не в скорости, а в том, что объект такой большой, каким должен быть.
Как отмечает @Philip, вывод здесь вводит в заблуждение. Предварительное выделение здесь не имеет значения, потому что операция форматирования строки стоит дорого. Я протестировал дешевую операцию в цикле и обнаружил, что предварительное выделение выполняется почти в два раза быстрее.
Этот ответ следует скорректировать, чтобы учесть комментарии о написании строк внутри цикла. Может быть, я должен сказать что-нибудь остроумное, как в этом ответе ... «Неправильное тестирование оптимизаций - самый эффективный способ нулевого доверия».
@Keith, здесь вас поддерживает теоретическая информатика. В зависимости от реализации добавления он должен быть в 2–3 раза быстрее. cs.stackexchange.com/questions/9380/….
Неправильные ответы с большим количеством голосов - еще один корень всего зла.
Чтобы быть ясным, несмотря на положительные голоса, этот ответ определенно неверен.
Как сказано выше, это неправильный ответ полностью. см. stackoverflow.com/a/7733316/2157240 для правильного профилирования
@Philip Это меньше полмиллисекунды в интерпретаторе, который известен своей медлительностью, чтобы такое время имело значение. Вы бы не стали писать код Python, если бы такой уровень производительности был критичным.
Кто сказал, что это досрочная оптимизация? Расскажите об этом любому разработчику игр, который может предвидеть, где будут находиться точки давления 80/20.
Как и ожидалось, принятый ответ субъективно и объективно вреден. Что неожиданно, так это то, что S.Lott - здесь активный Pythonista очень - не смог отредактировать свои ложные утверждения, предпочитая вместо этого выдвигать безосновательную повестку дня «Преждевременная оптимизация - корень всех зол». Добро пожаловать в 2020,, где даже отказ институционализирован.
Используя timeit на Python 3.8.5 и удалив форматирование строки, я получил 5,68 мс (предварительное выделение) vs 8,26 мс (добавление). Улучшение почти на 50%, не так уж плохо!
@CecilCurry то же самое касается столь же активного OP, который, похоже, не желает изменять принятый ответ. Я чувствую сговор! (шутка) Но если серьезно, ребята, зачем держать в неведении пользователей ТАК в темноте?
Я запустил Код С.Лотта и произвел такое же увеличение производительности на 10% за счет предварительного выделения. Я попробовал Идея Неда Батчелдера с использованием генератора и смог увидеть производительность генератора лучше, чем 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
«Для моего проекта улучшение на 10% имеет значение»? Действительно? Вы можете доказывать, что распределение списка является узким местом в? Я бы хотел узнать об этом больше. У вас есть блог, в котором вы могли бы объяснить, как это на самом деле помогло?
@ S.Lott попробуйте увеличить размер на порядок; производительность падает на 3 порядка (по сравнению с C++, где производительность падает чуть более чем на один порядок).
Это может быть так, потому что по мере роста массива его, возможно, придется перемещать в памяти. (Подумайте, как объекты хранятся там один за другим.)
Краткая версия: используйте
pre_allocated_list = [None] * size
для предварительного распределения списка (то есть, чтобы иметь возможность обращаться к элементам «размера» списка вместо постепенного формирования списка путем добавления). Эта операция выполняется быстро очень даже для больших списков. Выделение новых объектов, которые позже будут назначены элементам списка, займет на много больше времени и станет узким местом в в вашей программе с точки зрения производительности.
Длинная версия:
Думаю, что нужно учитывать время инициализации.
Поскольку в Python все является ссылкой, не имеет значения, устанавливаете ли вы каждый элемент в Никто или в какую-либо строку - в любом случае это всего лишь ссылка. Хотя это займет больше времени, если вы захотите создать новый объект для каждого элемента, на который будет ссылаться.
Для 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, а затем назначьте данные элементам списка по своему желанию. В любом случае для генерации данных требуется больше времени, чем для добавления / расширения списка, независимо от того, генерируете ли вы его при создании списка или после этого. Но если вам нужен редко заполненный список, то начать со списка Никто определенно быстрее.
хм интересно. так что ответ на этот вопрос будет - на самом деле не имеет значения, выполняете ли вы какую-либо операцию по помещению элементов в список, но если вам действительно просто нужен большой список всех тех же элементов, вы должны использовать подход []*
Как неприятный момент, это имеет интересное поведение, когда это делается для списков (например, для предварительного выделения матрицы m * n): x = 3 * [3 *[0]] дает [[0, 0, 0], [0, 0, 0], [0, 0, 0]], но тогда назначение неудобно: x[0][0] = 1 дает [[1, 0, 0], [1, 0, 0], [1, 0, 0]].
Да, потому что x = 3 * [3 *[0]] выделяет только два списка. См. этот канонический пост по проблеме.
Пифонический способ для этого:
x = [None] * numElements
Или любое другое значение по умолчанию, которое вы хотите предварительно заполнить, например
bottles = [Beer()] * 99
sea = [Fish()] * many
vegetarianPizzas = [None] * peopleOrderingPizzaNotQuiche
(Пусть покупатель будет бдителен: синтаксис [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 Core i7 64-битный Python дает
Elements: 100000, Iterations: 144
doAppend: 3587.204933ms
doAllocate: 2701.154947ms
doGenerator: 1721.098185ms
Хотя C++ дает (построен с Microsoft Visual C++, 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. Переключение на настоящий 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 традиционно выполнялись до 300 раз медленнее, чем другие языки, из-за таких функций Python, как декораторы и т. д. (PythonSpeed / PerformanceTips, агрегирование данных).
@NilsvonBarth C++ не имеет timeit
timeit, который следует использовать при синхронизации кода Python; Я, разумеется, не говорю о C++.
Это неправильный ответ. bottles = [Beer()] * 99 не создает 99 объектов пива. Вместо этого создает один объект Beer с 99 ссылками на него. Если вы измените его, все элементы в списке будут видоизменены, вызывая (bottles[i] is bootles[j]) == True для каждого i != j. 0<= i, j <= 99.
@erhesto Вы посчитали ответ неправильным, потому что автор использовал ссылки в качестве примера для заполнения списка? Во-первых, никому не нужно создавать 99 объектов пива (вместо одного объекта и 99 ссылок). В случае предварительного заселения (о чем он говорил) быстрее - лучше, поскольку значение будет заменено позже. Во-вторых, ответ вовсе не об отсылках или мутации. Вам не хватает общей картины.
@YongweiWu На самом деле ты прав. Этот пример не делает весь ответ неверным, он может вводить в заблуждение, и о нем просто стоит упомянуть.
Опасения по поводу предварительного распределения в Python возникают, если вы работаете с NumPy, который имеет больше массивов типа C. В этом случае проблемы предварительного распределения связаны с формой данных и значением по умолчанию.
Рассмотрите NumPy, если вы выполняете численные вычисления в огромных списках и хотите производительности.
Для некоторых приложений словарь может быть тем, что вы ищете. Например, в методе 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, который может вызвать исключение, если я случайно использую их неправильно, и потому, что мне нужно подумать о крайних случаях, которых карта позволяет мне избежать.
Это правда, что словарь не будет таким эффективным, но, как отмечали другие, различия в скорости маленький не всегда стоят опасностей обслуживания существенный.
Как уже упоминалось, самый простой способ предварительно заполнить список - использовать объекты 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). Такое поведение приводит к минимальному увеличению времени выполнения в Ответ С.Лотта.
Источник: Реализация списка Python
Насколько мне известно, они похожи на ArrayLists в том, что они каждый раз удваивают свой размер. Амортизированное время этой операции постоянное. Это не такой большой удар по производительности, как вы думаете.