Время выполнения функций
Вопрос новичка, связанный с кишками питона. Почему записанная в две строки функция выполняется быстрее, чему в одну? У второго варианта время выполнения стабильно меньше в 1,5 раза.
def benchmark(funct):
import time
def wrapper():
start = time.time()
funct()
print(f'Выполнение: {time.time()-start} сек.')
return wrapper
@benchmark
def test1():
res = [i for i in list(range(10000)) if i % 2 == 0]
@benchmark
def test2():
l = list(range(10000))
res = [i for i in l if i % 2 == 0]
test1()
test2()
Сделайте много замеров, например, 10000, а не по одному. Посмотрите, какой будет результат. У меня разница получается намного меньше, но тем не менее всё равно второй вариант быстрее. Весьма интересный вопрос. Но значительную практическую ценность результат вряд ли несёт: если я задумаюсь о производительности своей программы, я вряд ли буду писать её на питоне.
нипочему она не выполняется быстрее, просто питон медленно разгоняется, измени порядок вызова, сначала 2, потом 1 - теперь 1 быстрее, да? :)
Например, потому, что в первом случае list не имеет никакого смысла и только замедляет код:
def test3():
res = [i for i in range(10000) if i % 2 == 0]
Но даже это не имеет смысла, т.к. есть многократно более быстрый способ:
def test4():
res = list(range(0, 10000, 2))
На первый взгляд кажется, что оба варианта должны работать с одинаковой скоростью, так как они выполняют по сути одно и то же. Однако в действительности разница во времени выполнения может быть связана с деталями работы Python. Давайте разберем это по шагам:
### В чем различие между функциями?
1. **`test1`**:
res = [i for i in list(range(10000)) if i % 2 == 0]
Здесь `range(10000)` сначала преобразуется в список с помощью функции `list()`, а затем этот список используется в списочном выражении (`list comprehension`).
2. **`test2`**:
l = list(range(10000))
res = [i for i in l if i % 2 == 0]
Здесь `list(range(10000))` создается заранее и сохраняется в переменную `l`, а затем используется в списочном выражении.
### Почему `test2` быстрее?
1. **Обращение к встроенным функциям:**
В первом случае (`test1`) вызов `list(range(10000))` происходит **внутри списочного выражения**, что добавляет некоторую дополнительную нагрузку. Python вынужден каждый раз вызывать встроенную функцию `list()` и одновременно работать с результатом.
2. **Оптимизация промежуточных объектов:**
Во втором случае (`test2`) переменная `l` хранит уже готовый список. Python обращается к уже созданному объекту, что уменьшает накладные расходы на повторное выполнение `list(range(10000))`.
3. **Повторная работа с объектом:**
В первом случае Python обрабатывает генерацию списка и фильтрацию "на лету". Это может быть менее эффективно, поскольку требует больше переключений контекста.
4. **Кэширование объектов:**
В `test2` список `l` уже находится в оперативной памяти и готов к использованию. В `test1` каждый раз создается временный объект, что может быть менее оптимально с точки зрения времени выполнения.
### Как проверить разницу объективно?
Если вы хотите измерить разницу времени выполнения более точно, используйте модуль `timeit`, который специально предназначен для измерения производительности:
import timeit
setup_code = "l = list(range(10000))"
test1_code = "[i for i in list(range(10000)) if i % 2 == 0]"
test2_code = "[i for i in l if i % 2 == 0]"
print("test1:", timeit.timeit(test1_code, number=1000, globals=globals()))
print("test2:", timeit.timeit(test2_code, setup=setup_code, number=1000, globals=globals()))
### Итог
Разница во времени выполнения связана с тем, что `test1` каждый раз создает новый список с помощью `list(range(10000))`, а в `test2` список создается один раз и используется повторно. Это объясняет, почему `test2` работает быстрее.
Если вам важно максимизировать производительность, старайтесь избегать излишнего создания временных объектов внутри списочных выражений или циклов.
Работает не трогай, не работает - иди проветрись