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

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

Как специалист по данным, эффективный код:

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

Оптимизированный код == эффективный код

В python эффективный код:

  • Pythonic — использует уникальный стиль и идиомы Python так, как это было задумано основателями и сообществом.
  • Удобочитаемый — легко читать и понимать, что делает код. Например, следуя соглашениям о правильном именовании, помня о пробелах и используя меньше строк кода, где это возможно.
  • Быстро — должно выполняться за минимально возможное время, потребляя минимальное количество памяти и ресурсов.

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

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

Волшебные команды — это специальные команды Python, которые начинаются с % или %% и поддерживаются в блокнотах Jupyter и ядре Ipython. Они обеспечивают быстрый и мощный способ выполнения таких задач, как синхронизация кода (обсуждается в этой статье), отображение визуализаций и навигация по каталогам.

Магии строк: Они имеют один % и работают с одной строкой ввода.
Магии ячеек: Они имеют два %% и работают с несколькими строками кода или клеточный блок.

Примечание. Возможно, вы знакомы с символом "!", который представляет собой краткую форму магической команды %system. Эта команда выполняет команды оболочки непосредственно в ноутбуке, такие как установка пакетов с помощью !pip install package.

Чтобы отобразить все встроенные в Python магические команды, используйте %lsmagic.

Чтобы узнать, что делает магическая команда, используйте код %magic_command? чтобы отобразить документацию на месте.

1. %время

Эта волшебная команда измеряет время, необходимое для выполнения одной строки кода. Он запускает код несколько раз и возвращает среднее время выполнения.

Синтаксис %timeit: За командой следует тестируемый код, все в одной строке.

%timeit code_to_execute

Пример вывода

34.6 ns ± 1.17 ns per loop (mean ± 
std. dev. of 7 runs, 10000000 loops each)

Объяснение вывода:

  • 32,4 нс = среднее время выполнения. Используйте приведенную ниже таблицу для преобразования времени.
  • 1,17 нс = стандартное отклонение измерений.

  • 7 запусков = количество запусков или итераций для повторения процесса. У нас есть разные прогоны для учета различий в таких факторах, как использование памяти и загрузка ЦП, которые могут оставаться одинаковыми в одном прогоне, но отличаться в других.
  • 10 000 000 циклов = количество раз выполнения кода за итерацию. Таким образом, код выполняется в общей сложности runs*loops раз.

Количество запусков и циклов определяется автоматически в зависимости от сложности кода, но вы также можете передать их в качестве аргументов, как описано ниже.

Задача 1. Синхронизация одной строки кода — сравнение [] и list() для создания экземпляра списка.

Создание списка с использованием буквенных символов []

%timeit l1=['sue','joe','liz']

###Result
34.6 ns ± 1.17 ns per loop (mean ± 
std. dev. of 7 runs, 10000000 loops each)

Создание списка с помощью list()

%timeit l2=list(['sue','joe','liz'])

###Result
92.8 ns ± 1.35 ns per loop (mean ± 
std. dev. of 7 runs, 10000000 loops each)

Объяснение вывода:использование литеральных символов занимает 34,6 нс, что вдвое меньше, чем при использовании имени функции (92,8 нс).

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

#Efficient
lst = []
tup = ()
dct = {}

#Not Efficient
lst = list()
tup = tuple()
dct = dict()

То же самое можно сказать и о создании списка чисел с помощью функции диапазона. Распаковка списка с использованием имени * более эффективна, чем использование имени list().

#Efficient
lst = [*range(10)]

#Less efficient
lst = list(range(10))

Указание прогонов и циклов. После команды %timeit вы можете передать в качестве аргументов нужные прогоны и циклы, используя -r и -n соответственно.

%timeit -r 5 -n 1000 list=['sue','liz','joe']

###Result
42 ns ± 0.458 ns per loop (mean ± std. dev. of 5 runs, 1000 loops each)

2. %%timeit

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

Синтаксис %%timeit: Команда записывается в начале блока ячеек, за которой сразу же следуют строки кода для хронометража.

%%timeit
code_line_1
code_line_2
...

Задача 2. Синхронизация нескольких строк кода (блока ячеек). Сравните цикл for и списочное понимание, возводящее в квадрат все числа от 0 до 1000.

For-loopниже мы используем %%timeit и передаем желаемое количество прогонов (5) и циклов за прогон (1000).

%%timeit -r 5 -n 1000
squared_list=[]
for num in range(1000):
    num_squared=num**2
    squared_list.append(num_squared)


###Result
198 µs ± 9.31 µs per loop (mean ± 
std. dev. of 5 runs, 1000 loops each)

Выполнение кода занимает 198 микросекунд.

Понимание списка — здесь мы используем %timeit с одним знаком процента, потому что мы измеряем только одну строку кода.

%timeit -r 5 -n 1000 squared_list=[num**2 for num in range(1000)]

###Result
173 µs ± 7.22 µs per loop (mean ± 
std. dev. of 5 runs, 1000 loops each)

Код понимания списка быстрее — 173 микросекунды.

Поэтому, когда это возможно, и если это не ставит под угрозу читабельность, используйте понимание списка вместо цикла for.

%lprun — Профилирование линии

Эта команда исходит из библиотеки line-profiler, которая описывает время выполнения функции, программы или скрипта Python.

Он проверяет, сколько времени занимает каждая строка кода в функции, и возвращает результат построчного анализа.

Синтаксис %lprun: за командой следует -f, что означает, что мы анализируем функцию. Затем вы передаете имя функции, затем вызов функции с ее параметрами.

%lprun -f function_name function_name(args)

Профилировщик линий не встроен в Python, и его необходимо установить при первом использовании в вашей системе. Вам также необходимо загружать его в сеанс ipython каждый раз, когда вы запускаете новое ядро.

!pip install line_profiler

%load_ext line_profiler

Возвращаемая таблица представляет собой анализ каждой строки в функции со следующими столбцами:

  • Номер строки: позиция строки в функции.
  • Попадания: количество раз выполнения строки.
  • Время. Общее время, затраченное линией. Блок таймера указан в верхней части таблицы.
  • За обращение: среднее время, необходимое для выполнения строки (время/количество обращений).
  • % времени. Процент времени, затраченного на строку по сравнению с другими строками.
  • Содержимое строки: фактический исходный код строки.

Задача 3. Синхронизация функции. Сравните цикл for и встроенную функцию Python для удаления дубликатов из списка.

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

Использование цикла for

def remove_dups1(lst):
    uniques = []
    for name in lst:
        if name not in uniques:
            uniques.append(name)
    return uniques
%lprun -f remove_dups1 remove_dups1(lst)

Единицей измерения таймера является секунда (1e-07 с), что соответствует 0,1 микросекунды в соответствии с таблицей ниже. Вся функция выполнялась в течение 14,6 микросекунд, а коды цикла for выполнялись по отдельности (много попаданий).

Использование функции set()

def remove_dups2(lst):
    return list(set(lst))
%lprun -f remove_dups2 remove_dups2(lst)

У этой функции была только одна строка кода, которая запускалась один раз (1 обращение). Вся функция работала 3,3 микросекунды.

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

%mprun — профилирование памяти

Эта команда исходит из библиотеки memory profiler, которая описывает использование памяти функцией.

Таким образом, если %lprun измеряет время, %mprun измеряет потребляемую память и возвращает построчный анализ ресурсов памяти.

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

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

!pip install memory_profiler

%load_ext memory_profiler

Синтаксис %mprun: за командой следует -f, затем имя функции и, наконец, вызов функции.

from my_file import func_name

%mprun -f func_name func_name(params) 

Возвращаемая таблица содержит следующую информацию для каждой строки кода:

  • Строка №: номер выполняемой строки.
  • Использование памяти: объем памяти, используемый интерпретатором Python после выполнения этой строки кода, измеряемый в байтах.
  • Приращение: разница в используемой памяти по сравнению с предыдущей строкой. Думайте об этом как о влиянии этой строки на память.
  • Вхождения: количество экземпляров элементов одного типа, созданных в этой строке.
  • Содержимое строки:исходный код в этой строке.

Задача 4. Синхронизация функции в Pandas DataFrame. Какой наиболее эффективный способ выполнения вычислений в столбце Pandas?

В нашем примере ниже мы будем работать с фреймом данных Pandas и выполнять некоторые вычисления в столбце. Я использую набор данных Kaggle расход топлива, доступный здесь по лицензии Open Database.

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

import pandas as pd

data = pd.read_csv('Fuel_Consumption_2000-2022.csv')

Функция принимает кадр данных Pandas, умножает значения столбца на скаляр и возвращает измененный кадр данных. Мы протестируем четыре функции, чтобы проверить наиболее эффективный подход к памяти.

Помните, что %mprun должен обращаться к функции из файла. Чтобы сохранить функции в одном файле, запустите блок ячеек ниже, где верхняя строка %%file your_file.py.. Это создает и записывает (или перезаписывает) содержимое в your_file.py.

%%file my_file.py
def calc_apply(df):
    column = df['COMB (mpg)']
    new_vals = column.apply(lambda x: x* 0.425)
    df['kml'] = new_vals
    return df

def calc_listcomp(df):
    column = df['COMB (mpg)']
    new_vals = [x*0.425 for x in column]
    df['kml'] = new_vals
    return df

def calc_direct(df):
    column = df['COMB (mpg)']
    new_vals = column*0.425
    df['kml'] = new_vals
    return df

def calc_numpy(df):
    column = df['COMB (mpg)'].values
    new_vals = column*0.425
    df['kml'] = pd.Series(new_vals)
    return df

Затем загрузите расширение профилировщика памяти и импортируйте свои функции из файла.

%load_ext memory_profiler

from my_file import calc_apply, calc_listcomp, 
                    calc_direct, calc_numpy

Способ 1. Использование функции применить с помощью лямбда

%mprun -f calc_apply calc_apply(data.copy())

Функциональная строка apply, в которой происходит умножение, дает 45 000 вхождений и приращение памяти на 1,6 МБ.

Способ 2. Использование понимания списка

%mprun -f calc_listcomp calc_listcomp(data.copy())

Использование понимания списка вдвое уменьшает количество вхождений примерно до 22 500. Однако для двух строк отмечается аналогичное увеличение памяти на 1,7 МБ.

Способ 3. Прямое умножение.

%mprun -f calc_direct calc_direct(data.copy())

Использование метода прямого умножения приводит только к одному вхождению этого элемента в память и очень небольшому увеличению памяти на 0,4 МБ.

Способ 4. Использование NumPy путем первого вызова Series.values для преобразования столбца в массив NumPy.

Четвертый метод включает в себя сначала преобразование столбца в массив NumPy, а затем умножение его на скалярное значение. Как и в предыдущем методе 3, в памяти имеется только одно вхождение элемента и аналогичное увеличение памяти на 0,4 МБ.

Скорость прямого умножения по сравнению с умножением NumPy.

Вычисление Numpy выполняется быстрее, даже несмотря на то, что оно потребляет ту же память, что и прямой метод. Посмотрите результаты двух функций, используя %lprun, который измеряет время, затрачиваемое на строку.

Прямое умножение — медленнее

%lprun -f calc_direct calc_direct(data.copy())

Расчеты NumPy — быстрее

%lprun -f calc_numpy calc_numpy(data.copy())

Вычисление NumPy (столбец сначала преобразуется в массив NumPy с использованием Series.values) выполняется быстрее и занимает всего 137 мс по сравнению с 1150 мс для прямого умножения. Процент времени также намного меньше — 9,7% по сравнению с 45% при прямом умножении.

По этой причине числовые вычисления в фрейме данных наиболее эффективны при использовании NumPy, так как он оптимизирован для поэлементных операций.

Заключение

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

Мы изучили четыре волшебные команды; %timeit, %%timeit, %lprun, и %mprun. Первые три проверяют время, необходимое для выполнения кода, а последний измеряет потребляемую память. Мы также узнали, что магия строк работает с одной строкой кода и имеет префикс в один %. С другой стороны, магия ячеек начинается с двух %% и работает с несколькими строками кода непосредственно под ним.

Надеюсь, вам понравилась статья. Чтобы получать больше таких сообщений всякий раз, когда я публикую новый, подпишитесь здесь. Если вы еще не являетесь средним участником и хотели бы поддержать меня как писателя, перейдите по этой ссылке, чтобы подписаться за 5 долларов, и я получу небольшую комиссию. Спасибо за чтение!

Ссылки

  1. 25 привычек нуби-питона от MCoding на YouTube
  2. 29 лучших встроенных функций Python от Finxter с примерами.

3. Волшебные команды для профилирования в Jupyter Notebook