Python - язык программирования, известный своей простотой и читабельностью, но когда доходит до скорости - он проигрывает всем. Что очень критично, когда работаешь в машинном обучении и имеешь дело с огромным количеством данных. Сегодня рассмотрим парочку примеров, как можно ускорить ваш код на ровном месте
Использование генераторов вместо списков ?
Начнем с оптимизации по памяти. Представим, что перед нами стоит задача пройтись по всем данным и видоизменить их. Причем использовать векторизованные вычисления запрещено (Numpy, Pandas и др.). В этом случае первое что приходит в голову: создать список и все туда накидать. Давайте проверим сколько это займет памяти:
Для этого напишем простенькую функцию для отображения сколько занимает объект оперативки и тут же сгенерируем данных:
Скрытый текст
from sys import getsizeof
from random import random
def memory_consumption(obj):
return f"Total MBs size: {getsizeof(obj) / 1024 ** 2}"
data = [random() for _ in range(10 ** 6)]
После подгрузки данные занимают: Total MBs size: 8.057334899902344
Теперь создаем две функции:
# Первая функция через список
def squares_list(data):
result = []
for elem in data:
result.append(elem * 2)
return result
# Вторая функция через генератор
def squares_generator(data):
for elem in data:
yield elem * 2
# Пример использования
squares_list(1_000_000) # Занимает много памяти
squares_generator(1_000_000) # Экономит память
Тут вторая функция ничего не создает а просто возвращает вам итератор, по которому можно пробежаться и сохранить то, что вам необходимо.
✅Итоговые замеры: решение в "лоб" занимает столько же, сколько исходные данные - 8.06 MBs
, а генераторы обогнали - 1e-6 MBs
! Второе решение удобно применять там, где вам важно много раз создавать объекты либо же хранить только часть вывода
Локальные переменные ?
Когда пишете свои pipeline, стоит учитывать, что обращение к глобальным переменным может затормозить ваш код. Избавляемся от этого немедленно!
Если это возможно не используйте оператор global
. Лучше создать новую переменную и ее сохранить в каком-либо виде
global_variable = 10
def func1():
global gloval_variable
global_variable = 52
print(global_variable)
def func2():
local_variable = 10
print(local_variable)
func1() # Доступ к глобальной переменной
func2() # Доступ к локальной переменной
Использование for вместо while ⚡️
Циклы for
в Python часто быстрее, чем циклы while
, потому что они оптимизированы для итерации по последовательностям. Если вы знаете количество итераций, используйте for
вместо while
.
# Цикл `while`
i = 0
while i < 10:
print(i)
i += 1
# Цикл `for`
for i in range(10):
print(i)
Избегайте append в циклах ?
Использование append
внутри цикла может замедлить выполнение функции, потому что Python каждый раз создает новый список. Если возможно, используйте list
сразу с нужным размером, чтобы избежать частых перераспределений памяти.
# Использование `append`
result = []
for i in range(10):
result.append(i)
# Создание списка с нужным размером
result = [0] * 10
for i in range(10):
result[i] = i
Использование map и filter ?
Эти функции позволяют применить функцию к каждому элементу итерируемого объекта (например, списка), не прибегая к написанию собственного цикла. Это делает код более понятным и иногда ускоряет его выполнение.
# Использование `map`
numbers = [1, 2, 3, 4]
squares = map(lambda x: x ** 2, numbers)
print(list(squares))
# Вывод: [1, 4, 9, 16]
# Использование `filter`
numbers = [1, 2, 3, 4, 5]
even_numbers = filter(lambda x: x % 2 == 0, numbers)
print(list(even_numbers))
# Вывод: [2, 4]
Итог ?
Эти 5 способов оптимизации функций в Python помогут вам сделать ваш код быстрее и эффективнее. Важно помнить, что не всегда нужно использовать все эти методы одновременно. Экспериментируйте и выбирайте подходящие решения в зависимости от конкретной задачи.
Бонусом забирайте mock-собеседование, где я рассказал что еще можно встретить на собеседование на позицию Data Science. Больше про эффективную оптимизацию кода для рабочих задач я написал тут - пользуйся!
Комментарии (10)
Andrey_Solomatin
05.09.2024 09:05+2Использование
append
внутри цикла может замедлить выполнение функции, потому что Python каждый раз создает новый список.Учите матчасть. Не каждый.
JastixXXX
05.09.2024 09:05+1Вот хорошо написано, и полезно, но почему хоть какие-то замеры приведены только для первого пункта? Интересно было бы узнать, насколько сотня-другая for-ов выполнится быстрее while-ов и т.д.
Не, можно конечно и самостоятельно поэкспериментировать, но автор статьи мог бы избавить читателей от этого.Andrey_Solomatin
05.09.2024 09:05Сотня другая итераций циклов, будет сильно ниже погрешности измерений. Тут миллионы нужны.
Ну и в разных версиях Питона будут сильно разные цифры.JastixXXX
05.09.2024 09:05+1Ну или миллионы. Версию взять самую последнюю, думаю это имеет смысл.
Просто хочется видеть, насколько велика разница, ну как минимум проценты или доли процента, есть ли смысл вообще заморачиваться такими оптимизациями.
Вот, например, ради интереса для циклов
Скрытый текст
from time import perf_counter
def count_time(fun):
start = perf_counter()
fun()
end = perf_counter()
print(end - start)
def fun_for():
for i in range(10000):
_ = i + i
for j in range(2000):
_ = i + j
def fun_while():
i = 0
while i < 10000:
_ = i + i
j = 0
i += 1
while j < 2000:
_ = i + j
j += 1
count_time(fun_for)
count_time(fun_while)
Получилось:
2.5038714 для for
3.6994507 для while
Разница ощутимая
Veritaris
05.09.2024 09:05+3Использование map и filter
Сам Гвидо когда-то писал что лучше использовать list comprehension вместо map, т.к. производительность выше (источник – http://python-history.blogspot.com/2010/06/from-list-comprehensions-to-generator.html). Да и в принципе сейчас легко гуглится что list comprehension заменяет и map, и filter в одном
Да, map / filter возвращают не массив, а генератор, но в контексте, который представлен в статье, берётся list от этого генератора, и смысл пропадает. При этом есть возможность таким же образом создать и генератор (Py 3.11):a = [1, 2, 3, 4] a_map_list = [x*x for x in a] # вернёт [1, 4, 9, 16] a_map_gen = (x*x for x in a) # вернёт generator, который при итерации выдаст те же значения, что и при итерации по `a_map_list`
onegreyonewhite
05.09.2024 09:05Избегайте append в циклах
Вот тест timeit. Результаты теста о советах подобного типа говорят сами за себя.
# python3.10 -m timeit 'result = []' 'for i in range(10):' ' result.append(i)' 500000 loops, best of 5: 676 nsec per loop # python3.10 -m timeit 'result = [0] * 10' 'for i in range(10):' ' result[i] = i' 500000 loops, best of 5: 545 nsec per loop # python3.12 -m timeit 'result = []' 'for i in range(10):' ' result.append(i)' 1000000 loops, best of 5: 352 nsec per loop # python3.12 -m timeit 'result = [0] * 10' 'for i in range(10):' ' result[i] = i' 1000000 loops, best of 5: 354 nsec per loop
Использование map и filter
Вот прям очень говорящий пример, почему статья в минусах:
# python3.10 -m timeit -s 'numbers = [1, 2, 3, 4]' 'list(filter(lambda x: x % 2 == 0, numbers))' 500000 loops, best of 5: 616 nsec per loop # python3.10 -m timeit -s 'numbers = [1, 2, 3, 4]' '[x for x in numbers if x % 2 == 0]' 1000000 loops, best of 5: 365 nsec per loop # python3.12 -m timeit -s 'numbers = [1, 2, 3, 4]' 'list(filter(lambda x: x % 2 == 0, numbers))' 500000 loops, best of 5: 598 nsec per loop # python3.12 -m timeit -s 'numbers = [1, 2, 3, 4]' '[x for x in numbers if x % 2 == 0]' 1000000 loops, best of 5: 200 nsec per loop
cosheimil
Как часто применяются эти типы оптимизации? Или все чаще используется numpy, pandas и другие инструменты ds?
Aleron75 Автор
В идеале всегда замечать)
Andrey_Solomatin
Генераторы экономят память и позволяют закончить вычисления на промежуточном этапе. Полезны даже когда можно использовать векторизацию.
map и списковые выражения, работают быстрее циклов и часто легче читаются. Я стараюсь их использовать вместов циклов (for, while). Но больше из-за чиатемость, чем производительности.
Пример с append выроженный. Если данных мало, то пофиг, а если много, то стоит подключить numpy.
С глобальными переменнами не эксперементировал. Хотя я бы упростил пример, там дело в том, что переменную ищут сначала в локальном словаре, а потом в словаре выше. Так что с любой константой скорее всего воспроизводится. C появление JIT, скорее всего станет не актуально.