Лучший способ для цикла Python for
Мы все знаем, что общий способ выполнения оператора определенное количество раз в Python заключается в использовании цикла for
.
Общий способ сделать это:
# I am assuming iterated list is redundant.
# Just the number of execution matters.
for _ in range(count):
pass
Я считаю, что никто не будет утверждать, что приведенный выше код является общей реализацией, однако есть и другой вариант. Использование скорости создания списка Python путем умножения ссылок.
# Uncommon way.
for _ in [0] * count:
pass
Существует также старый способ while
.
i = 0
while i < count:
i += 1
Я тестировал время выполнения этих подходов. Вот код.
import timeit
repeat = 10
total = 10
setup = """
count = 100000
"""
test1 = """
for _ in range(count):
pass
"""
test2 = """
for _ in [0] * count:
pass
"""
test3 = """
i = 0
while i < count:
i += 1
"""
print(min(timeit.Timer(test1, setup=setup).repeat(repeat, total)))
print(min(timeit.Timer(test2, setup=setup).repeat(repeat, total)))
print(min(timeit.Timer(test3, setup=setup).repeat(repeat, total)))
# Results
0.02238852552017738
0.011760978361696095
0.06971727824807639
Я бы не стал начинать тему, если была небольшая разница, однако можно видеть, что разница в скорости составляет 100%. Почему Python не поощряет такое использование, если второй метод намного эффективнее? Есть ли лучший способ?
Тест выполняется с помощью Windows 10 и Python 3.6.
Следуя предложению @Tim Peters,
.
.
.
test4 = """
for _ in itertools.repeat(None, count):
pass
"""
print(min(timeit.Timer(test1, setup=setup).repeat(repeat, total)))
print(min(timeit.Timer(test2, setup=setup).repeat(repeat, total)))
print(min(timeit.Timer(test3, setup=setup).repeat(repeat, total)))
print(min(timeit.Timer(test4, setup=setup).repeat(repeat, total)))
# Gives
0.02306803115612352
0.013021619340942758
0.06400113461638746
0.008105080015739174
Что предлагает намного лучший способ, и это в значительной степени отвечает на мой вопрос.
Почему это быстрее, чем range
, так как оба являются генераторами. Это потому, что значение никогда не меняется?
Ответы
Ответ 1
Использование
for _ in itertools.repeat(None, count)
do something
- неочевидный способ получить лучшее из всех миров: крошечное постоянное требование к пространству и никаких новых объектов, созданных на итерацию. Под обложками код C для repeat
использует собственный C-целочисленный тип (не целочисленный объект Python!), Чтобы отслеживать оставшееся количество.
По этой причине счет должен соответствовать типу платформы C ssize_t
, который обычно не больше 2**31 - 1
в 32-битном поле, а здесь в 64-битном поле:
>>> itertools.repeat(None, 2**63)
Traceback (most recent call last):
...
OverflowError: Python int too large to convert to C ssize_t
>>> itertools.repeat(None, 2**63-1)
repeat(None, 9223372036854775807)
Это много для моих петель; -)
Ответ 2
Первый метод (в Python 3) создает объект диапазона, который может выполнять итерацию по диапазону значений. (Он похож на объект-генератор, но вы можете перебирать его несколько раз.) Он не занимает много памяти, потому что он не содержит весь диапазон значений, а только текущий и максимальный значения, где он продолжает увеличиваться на размер шага (по умолчанию 1), пока он не достигнет максимума.
Сравните размер range(0, 1000)
с размером list(range(0, 1000))
: Попробуйте в Интернете!. Первый - очень эффективный с точки зрения памяти; он принимает только 48 байтов независимо от размера, тогда как весь список увеличивается линейно с точки зрения размера.
Второй метод, хотя и быстрее, занимает ту память, о которой я говорил в прошлом. (Кроме того, кажется, что хотя 0
занимает 24 байта, а None
принимает 16, массивы 10000
каждого имеют одинаковый размер. Интересно, вероятно, потому, что они указатели)
Интересно, что [0] * 10000
меньше, чем list(range(10000))
примерно на 10000, что имеет смысл, потому что в первом случае все является одним и тем же примитивным значением, поэтому его можно оптимизировать.
Третий вариант хорош, потому что он не требует другого значения стека (тогда как вызов range
требует другого места в стеке вызовов), хотя, поскольку он в 6 раз медленнее, это не стоит того.
Последний может быть самым быстрым только потому, что itertools
классно: P Я думаю, что он использует некоторые оптимизации C-библиотеки, если я правильно помню.
Ответ 3
Первые два метода должны выделять блоки памяти для каждой итерации, в то время как третий будет делать шаг для каждой итерации.
Диапазон - медленная функция, и я использую его только тогда, когда мне приходится запускать небольшой код, который не требует скорости, например, range(0,50)
. Я думаю, вы не можете сравнить три метода; они совершенно разные.
Согласно приведенному ниже замечанию, первый случай действителен только для Python 2.7, в Python 3 он работает как xrange и не выделяет блок для каждой итерации. Я протестировал его, и он прав.