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

3 60

3 ответа:

используя

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)

который достаточно большой для моих петель ;-)

первый метод (в 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-библиотеки оптимизации, если я правильно помню.

первые два метода должны выделять блоки памяти для каждой итерации, в то время как третий будет просто делать шаг для каждой итерации.

Range-это медленная функция, и я использую ее только тогда, когда мне нужно запустить небольшой код, который не требует скорости, например,range(0,50). Я думаю, что вы не можете сравнить эти три метода; они совершенно разные.

согласно комментарию ниже, первый случай действителен только для Python 2.7, в Python 3 он работает как xrange и не работает выделите блок для каждой итерации. Я проверил его, и он прав.