12 проверенных способов оптимизации функций Python
Вступление
Написание кода и поддержание его в идеальном состоянии — дело сложное и хлопотное. Особенно нелегко приходится с нестандартными проблемами, которые предполагают несколько решений, а какое из них правильное — определить трудно.Тем не менее программирование на высокоуровневых языках можно упростить с помощью определенных приемов. В этой статье мы расскажем, как оптимизировать написание функций.
#1: Ввод/вывод
Для начала подумайте, для чего вы создаете функцию. В конечном счете, ее предназначение — это возврат или изменение чего-либо. Спросите себя: “Что нужно сделать, чтобы достичь этого?”.Без четкой установки писать функцию просто не имеет смысла. Сначала решите, что хотите получить от нее — это и будет вывод. Затем подумайте, что нужно добавить, чтобы добиться этого результата. И только после этого приступайте к написанию. В некоторых случаях может быть полезно запустить функцию с возврата.
Следующий пример, хотя и относительно прост, может наглядно продемонстрировать эту концепцию. Напишем функцию для вычисления среднего значения.
Для начала определимся с выводом. Нам надо получить вычисление среднего значения. Подобные данные обычно хранятся в векторе или списке на Python, поэтому можно предположить, что это ввод:
Код:
def mean(x : list):
return(mu)
Код:
def mean(x : list):
mu = sum(x) / len(x)
return(mu)
#2: Извлечение
Еще одна полезная практика написания функций — это извлечение, которое является важным компонентом очистки кода. Извлечение — это создание большего количества методов для обработки данных внутри одной функции. При этом функция не должна быть перегружена сбором различных значений. Вместо этого лучше написать функцию для получения этих значений. Секрет хорошего кода — простые функции с краткими директивами.Перед демонстрацией упрощенного примера на Python посмотрим, как работает эта техника при написании функций на языке Julia:
Код:
function OddFrame(file_path::String)
# Метки/колонки
extensions = Dict("csv" => read_csv)
extension = split(file_path, '.')[2]
labels, columns = extensions[extension](file_path)
length_check(columns)
name_check(labels)
types, columns = read_types(columns)
# Coldata
coldata = generate_coldata(columns, types)
# Head
"""dox"""
head(x::Int64) = _head(labels, columns, coldata, x)
head() = _head(labels, columns, coldata, 5)
# Drop
drop(x) = _drop(x, columns)
drop(x::Symbol) = _drop(x, labels, columns, coldata)
drop(x::String) = _drop(Symbol(x), labels, columns, coldata)
dropna() = _dropna(columns)
dtype(x::Symbol) = typeof(coldata[findall(x->x == x, labels)[1]][1])
dtype(x::Symbol, y::Type) = _dtype(columns[findall(x->x == x, labels)[1]], y)
# тип
self = new(labels, columns, coldata, head, drop, dropna, dtype);
select!(self)
return(self);
end
Как видите, из функции извлечено все, что нельзя записать менее чем в три строки. Если бы все эти функции были записаны как одна, то она была бы слишком длинной. Кроме того, было бы практически невозможно отслеживать шаг за шагом процесс ее создания.
Еще одна серьезная проблема, с которой можно столкнуться, — трассировка стека. Намного сложнее отследить в стеке ошибку, если она содержится в огромной функции. При каждом получении трассировки стека мы получаем функции, выходящие друг из друга там, где произошла ошибка. С учетом этого, мы можем видеть точный вызов в каждой функции с ошибкой.
Пример на языке Julia может быть немного непонятен, особенно тем, кто пишет на Python. Чтобы лучше разобраться в этой концепции, создадим функцию нормализации, которая использует извлечение более простым способом.
Обратите внимание, что все эти функции доступны в библиотеках, которые можно импортировать. Но сейчас мы говорим о самостоятельной реализации.
Код:
from numpy import sqrt
def norm(x : list):
mu = sum(x) / len(x)
x2 = [(i-mu) ** 2 for i in x]
m = sum(x2) / len(x2)
std = sqrt(m)
return([(i - mu) / std for i in x])
Начнем с первой строки. Учитывая, что этот пакет вычисляет норму данных, можно предположить, что он будет ориентирован на статистику. Тем не менее среднее значение, вероятно, будет использоваться не только в этой функции, а гораздо чаще.
Это однострочная операция, но, скорее всего, мы будем применять ее чаще. Кроме того, всегда лучше свести операции внутри основной функции, подобной этой, к минимуму. Конечно, это не самый сложный пример, но достаточно показательный.
Далее вычисляем x², то есть xbar² для xbar в x. Как видите, это просто значение, которое нужно, чтобы получить стандартное отклонение в функции. После этого пишем мы выписываем арифметику и повторяем тот же самый код, чтобы вычислить среднее значение. Наконец, мы получаем стандартное отклонение, а затем возвращаем нормально распределенные данные.
Этот метод, безусловно, можно улучшить. Вместо того, чтобы помещать всю арифметику стандартного отклонения внутрь функции, сделаем для нее вызов метода. То же самое используем и для mean, что позволяет сократить ее до трех строк.
Код:
def mean(x : list):
return(sum(x) / len(x))
def std(x : list):
mu = sum(x) / len(x)
x2 = [(i-mu) ** 2 for i in x]
m = sum(x2) / len(x2)
return(sqrt(m))
def betternorm(x : list):
mu = mean(x)
st = std(x)
return([(i - mu) / st for i in x])
Единственным недостатком этой версии является то, что среднее значение вычисляется дважды: внутри области видимости betternorm() и std(). Конечно, и этот недостаток можно исправить. Но, учитывая незначительность затрат на производительность, необходимых для компромиссного решения, можно считать его наиболее приемлемым.
#3: Именование
Многие не думают о том, как важно дать функции правильное имя. В большинстве случаев оно должно сообщать о том, что выводит функция.Кроме того, докстринги используются, только чтобы определить, как должен быть отформатирован вход и какие типы можно ожидать, передавая их на вход. Допустим, следующая функция заставляет «Джерри» съесть «огурец» (pickle):
Код:
def pickle(n_pickles : int):
pass
Имя “pickle” не очень специфично. Дело в том, что вашим коллегам будет тяжело догадаться, кого накормят огурцом с помощью этого метода. Поэтому функция должна называться примерно так:
Код:
def jerry_eat_pickle(n_pickles : int):
pass
Этот простой пример показывает, как важны имена. И выбор наиболее подходящего — секундное дело. Например, если нужно выполнить операцию слияния со словарями Pandas, на ум сразу приходит слово merge (слияние). Называть функцию mer() или m() — не самая лучшая идея.
Другой аспект проблемы именования заключается в том, что, согласно соглашению, методы в Python должны быть названы в нижнем регистре без заглавных букв. Псевдонимы с заглавной буквы должны быть зарезервированы для типов.
Еще одна проблема — именование отдельных частей функции. Вернемся к примеру на Julia — там нет избыточных комментариев. Конечно, одно дело, если вы пытаетесь объяснить что-то шаг за шагом, другое — подобные комментарии:
Код:
# multiply 5 by x
5 * x
Еще одно преимущество именования заключается в том, что заставляет программистов группировать различные данные. Это намного облегчает чтение кода, поскольку позволяет читать не все сразу, а постепенно.