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)


  1. cosheimil
    05.09.2024 09:05

    Как часто применяются эти типы оптимизации? Или все чаще используется numpy, pandas и другие инструменты ds?


    1. Aleron75 Автор
      05.09.2024 09:05

      В идеале всегда замечать)


    1. Andrey_Solomatin
      05.09.2024 09:05

      Генераторы экономят память и позволяют закончить вычисления на промежуточном этапе. Полезны даже когда можно использовать векторизацию.

      map и списковые выражения, работают быстрее циклов и часто легче читаются. Я стараюсь их использовать вместов циклов (for, while). Но больше из-за чиатемость, чем производительности.

      Пример с append выроженный. Если данных мало, то пофиг, а если много, то стоит подключить numpy.

      С глобальными переменнами не эксперементировал. Хотя я бы упростил пример, там дело в том, что переменную ищут сначала в локальном словаре, а потом в словаре выше. Так что с любой константой скорее всего воспроизводится. C появление JIT, скорее всего станет не актуально.


  1. Andrey_Solomatin
    05.09.2024 09:05
    +2

    Использование append внутри цикла может замедлить выполнение функции, потому что Python каждый раз создает новый список.

    Учите матчасть. Не каждый.


  1. JastixXXX
    05.09.2024 09:05
    +1

    Вот хорошо написано, и полезно, но почему хоть какие-то замеры приведены только для первого пункта? Интересно было бы узнать, насколько сотня-другая for-ов выполнится быстрее while-ов и т.д.
    Не, можно конечно и самостоятельно поэкспериментировать, но автор статьи мог бы избавить читателей от этого.


    1. Andrey_Solomatin
      05.09.2024 09:05

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

      Ну и в разных версиях Питона будут сильно разные цифры.


      1. 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
        Разница ощутимая


  1. 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`
    


    1. Aleron75 Автор
      05.09.2024 09:05

      Это правда) Не стал про это писать


  1. 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