Подтвердить что ты не робот

Лучший способ для цикла 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, так как оба являются генераторами. Это потому, что значение никогда не меняется?

4b9b3361

Ответ 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 и не выделяет блок для каждой итерации. Я протестировал его, и он прав.