Вы даже не вспотеете

В этом посте я расскажу о 3 методах повышения эффективности Python, которые вы можете использовать в своих ежедневных сценариях, и о том, как измерить улучшение производительности между двумя решениями. Давайте начнем!

Как мы можем сравнить производительность двух возможных решений?

Производительность может относиться к множеству различных факторов в решении (например, время выполнения, использование ЦП, использование памяти и т. Д.). Однако в этом посте мы сосредоточимся на времени выполнения.

Уменьшение времени выполнения нового решения можно вычислить так же просто, как выполнить деление. То есть мы разделим время выполнения старого (или неоптимизированного) решения на новое (или оптимизированное) решение: Told / Tnew. Этот показатель обычно называют ускорением. Например, если бы мы получили коэффициент ускорения 2, наше улучшенное решение заняло бы вдвое меньше времени, чем исходное решение.

Чтобы сравнить производительность наших функций, мы создадим функцию, которая получает их обе, вычисляет время их выполнения и вычисляет полученное ускорение:

import time
def compute_speedup(slow_func, opt_func, func_name, tp=None):
  x = range(int(1e5))
  if tp: x = list(map(tp, x))
  slow_start = time.time()
  slow_func(x)
  slow_end = time.time()
  slow_time = slow_end - slow_start
  opt_start = time.time()
  opt_func(x)
  opt_end = time.time()
  opt_time = opt_end - opt_start
  speedup = slow_time/opt_time
  print('{} speedup: {}'.format(func_name, speedup))

Чтобы получить значимые результаты, мы будем использовать относительно большой массив (100 000 элементов) и передавать его обеим функциям в качестве аргумента. Затем мы вычислим время выполнения, используя модуль времени, и, наконец, предоставим полученное ускорение.

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

1. Избегайте объединения строк с помощью оператора +.

Вы можете столкнуться с распространенной ситуацией, когда вам нужно сформировать строку с несколькими ее частями. В Python есть удобный оператор +, который позволяет нам удобно объединять строки следующим образом:

def slow_join(x):
  s = ''
  for n in x:
    s += n

Несмотря на то, что это кажется нам чистым подходом, строки Python были созданы неизменными и, следовательно, не могут быть изменены. Это означает, что каждый раз, когда мы используем оператор +, Python фактически создает новую строку на основе обеих подстрок и возвращает новую строку. Учтите, что в нашем случае эта операция будет выполнена 100000 раз.

Очевидно, что у этого подхода есть затраты, и мы можем найти более дешевое решение, используя join (), как в следующем примере:

def opt_join(x):
  s = ''.join(x)

Это решение берет массив подстрок и объединяет их с помощью разделителя пустой строки. Давайте проверим наше улучшение производительности:

compute_speedup(slow_join, opt_join, 'join', tp=str)

У меня коэффициент ускорения 7,25! Я бы сказал, неплохо, учитывая небольшой объем работы, необходимой для реализации этой техники.

2. Используйте функцию карты

Когда нам нужно работать с каждым элементом в списке, мы обычно можем сделать это следующим образом: мы применяем понимание генератора и работаем с текущим элементом. Затем, при необходимости, мы можем перебрать его:

def slow_map(x):
  l = (str(n) for n in x)
  for n in l:
    pass

Однако во многих ситуациях вы можете предпочесть использование встроенной функции карты Python, которая применяет одну и ту же операцию к каждому элементу в итерируемом объекте и дает результаты. Это может быть реализовано просто следующим образом:

def opt_map(x):
  l = map(str, x)
  for n in l:
    pass

Пришло время проверить, насколько мы улучшили время выполнения! Запустите нашу функцию compute_speedup следующим образом:

compute_speedup(slow_map, opt_map, 'map')

Получаю ускорение в 1,55 раза. Можно утверждать, что понимание более читабельно, но я бы сказал, что также требуется очень немного времени, чтобы привыкнуть к синтаксису сопоставления, по крайней мере, в простых сценариях (например, когда не требуется никаких условий для итерируемого).

3. Избегайте переоценки функций.

Всякий раз, когда вы обнаруживаете, что неоднократно используете одну и ту же функцию над элементом внутри блока цикла, например:

y = []
for n in x:
  y.append(n)
  y.append(n**2)
  y.append(n**3)

… Или просто использовать такую ​​функцию один раз внутри блока цикла, но над большим списком, например, в следующем случае:

def slow_loop(x):
  y = []
  for n in x:
    y.append(n)

… Вы можете воспользоваться другим методом оптимизации.

Вы можете сэкономить на переоценке функции, если предварительно сохраните ее как переменную и повторно используете в блоке цикла. Следующий фрагмент демонстрирует это поведение:

def opt_loop(x):
  y = []
  append = y.append
  for n in x:
    append(n)

Обратите внимание: если вам нужно добавить текущий элемент в разные списки, вам нужно будет создать новую переменную для каждой функции добавления списка.

Давайте проверим ускорение с помощью compute_speedup:

compute_speedup(slow_loop, opt_loop, 'loop')

В этом случае я получаю ускорение в 2,07 раза! Опять же, нам не нужно было вносить какие-либо серьезные изменения, чтобы добиться такого улучшения.

Хотите больше советов по эффективности? Ознакомьтесь с этими статьями!

3 простых совета по эффективности Python

Поиск узких мест в производительности в Python