3.1 Пользовательские функции#
Ознакомившись с управляющими конструкциями, мы теперь готовы писать алгоритмы для решения конкретных задач. Однако если нам потребуется переиспользовать этот алгоритм где-то в другом месте, в другом скрипте или просто другой части нашего кода, то у нас пока нет иного решения, как дублирование этого алгоритма и, соответственно, дублирование написанного кода. Такое дублирование может приводить к потенциальным ошибкам: если мы решим исправить алгоритм, то можно упустить одно из мест, где он использовался. Чтобы избежать такого дублирования и избежать многих проблем, мы можем оборачивать наш код в функции.
В прошлых модулях мы уже определили функцию как последовательность инструкций, которую можно вызвать по имени. Такая последовательность может принимать другие переменные как параметры, влияющие на выполнение инструкций, и возвращать значения. Обращаться к функции можно как к обычной переменной.
Определение функции может показаться достаточно простым, однако на практике часто можно столкнуться с трудностью выбора, что именно определить как функцию. Частой ошибкой будет выделение в функцию целиком сложных алгоритмов, содержащих в себе много различных конструкций. Как правило, такие алгоритмы требуют предварительной декомпозиции на более простые составные части, на отдельные подзадачи. Только после декомпозиции мы можем оформить основной алгоритм в виде простой функции, включающей в себя вызовы других функций, решающих наши подзадачи. Таким образом, функция должна выглядеть достаточно просто и решать одну конкретную задачу.
Следующей частой проблемой становится вопрос, какие простые последовательности инструкций стоит выносить в отдельные функции, а какие нет. Ответом на этот вопрос будет принцип DRY (Don’t Repeat Yourself). Если при написании кода мы начинаем повторять себя, такой код нужно выделить в отдельную функцию и использовать уже её.
Посмотрим принцип DRY на примере:
# Допустим в нашей программе на вход мы получили несколько точек:
point_1 = (30.3351, 59.93428)
point_2 = (37.6173, 55.75583)
point_3 = (39.46104, 55.92639)
# Далее нам нужно посчитать расстояние между ними и вывести его на экран
x1, y1 = point_1
x2, y2 = point_2
distance_1_2 = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
print('Расстояние между точками 1 и 2:', distance_1_2)
x1, y1 = point_2
x2, y2 = point_3
distance_2_3 = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
print('Расстояние между точками 2 и 3:', distance_2_3)
Расстояние между точками 1 и 2: 8.395825227010146
Расстояние между точками 2 и 3: 1.8516122437486706
Здесь мы видим, что код для подсчета расстояния дублируется, а значит имеет смысл вынести подсчет расстояния как отдельную функцию print_distance:
point_1 = (30.3351, 59.93428)
point_2 = (37.6173, 55.75583)
point_3 = (39.46104, 55.92639)
# Определим свою функцию
def print_distance(start_point, finish_point):
x1, y1 = start_point
x2, y2 = finish_point
distance = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
print('Расстояние между точками:', distance)
# Теперь можно вызывать функцию с нужными аргументами не дублируя код
print_distance(point_1, point_2)
print_distance(point_2, point_3)
Расстояние между точками: 8.395825227010146
Расстояние между точками: 1.8516122437486706
Таким образом, мы написали меньше кода для решения задачи, а самое главное, что теперь логика нашего алгоритма определена только в одном месте, в функции. А если нужно будет внести какие-то изменения в алгоритм определения расстояния, то это будет достаточно сделать только в одном месте.
Определение функции#
Давайте подробнее разберемся в синтаксисе определения пользовательских функций. В общем случае мы его можем описать следующим образом:
def Имя функции(Параметры функции, ...):
Блок Инструкций/Тело функции
Имя функции аналогично имени переменной. Фактически можно сказать, что мы определяем переменную, которая будет указывать на объект функции. Здесь стоит дополнить только, что для названий функций принято использовать глаголы, чтобы из названия было понятно, что делает данная функция.
Параметры функции определяются как имена переменных, которые пока что не существуют, но которые будут использоваться внутри нашей функции. Параметров можно определить сколько угодно, перечисляя их через запятую.
Тело функции - это такой же блок инструкций, как и блоки в управляющих конструкциях. Значит, что внутри него можно использовать любые конструкции, соблюдая нужные уровни отступов.
Посмотрим еще раз на функцию print_distance:
# Определяем функцию print_distance. После определения
# к ней можно обращаться как к обычной переменной
def print_distance(start_point, finish_point):
# Внутри функции к параметрам обращаемся как к обычным переменным.
# Реальные значения им будут присваиваться в момент вызова функции
x1, y1 = start_point
x2, y2 = finish_point
distance = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
print('Расстояние между точками:', distance)
# Вне функции переменные start_point и finish_point не существуют,
# попытка их вызвать будет приводить к ошибке.
# Подробнее этот момент будет рассмотрен далее в теме области видимости
Параметры функции#
Остановимся чуть подробнее на параметрах функции. Их можно разбить на две группы:
required - обязательно нужно передать при вызове
optional - необязательно передавать при вызове, у них есть значение по умолчанию
Важной особенностью является то, что обязательные параметры всегда определяются перед необязательными
Посмотрим пример:
# Сделаем finish_point необязательным параметром,
# задав ему значение по умолчанию.
def print_distance(start_point, finish_point = (0, 0)):
x1, y1 = start_point
x2, y2 = finish_point
distance = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
print('Расстояние между точками:', distance)
# И тех, и других параметров может быть любое количество.
# Главное - соблюдать очередность их определения.
def print_distance_by_ordinates(x1, y1, x2=0, y2=0):
distance = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
print('Расстояние между точками:', distance)
Теперь функцию print_distance можно вызывать, передавая ей 1 или 2 параметра:
print_distance(point_1, point_2) # В finish_point передается значение point_2,
# именно оно будет использоваться в функции
Расстояние между точками: 8.395825227010146
print_distance(point_1) # в finish_point не передается значение,
# по умолчанию будет использоваться (0, 0)
Расстояние между точками: 67.17392508353521
Аргументы функции#
Значения, которые мы передаем функции в качестве параметров при вызове, называются аргументами функции. Аргументы также можно разбить на две группы:
Позиционные (positional) - присваиваются параметрам по позиции. При вызове функции в каком порядке были определены параметры, в таком порядке им присваиваются значения.
Именованные (keywords) - присваиваются параметрам за счет явного указания имени нужного параметра. Так, можно нарушать порядок аргументов при вызове.
Также важной особенностью является то, что позиционные аргументы всегда идут перед именованными.
Посмотрим пример:
print_distance(point_1, point_2)
При вызове нигде не указаны имена параметров, поэтому аргументы point_1 и point_2 являются позиционными. Так, значение point_1 по позиции присваивается параметру start_point, значение point_2 по позиции присваивается параметру finish_point. Причем неважно, являются параметры обязательными или нет.
print_distance_by_ordinates(0, 1, y2=2, x2=3)
При вызове функции у первых двух аргументов не указано, каким параметрам они будут присваиваться, поэтому они будут позиционными. Соответственно x1=0 и y1=1. Для последних двух параметров явно указано, что y2=2 и x2=3 - это именованные аргументы, они присваиваются вне зависимости от позиции.
Области видимости#
Когда мы разобрались, как определять функции и их параметры, как передавать в них аргументы, осталось разобраться, какие переменные можно использовать внутри функций и к каким не получится обратиться извне. За это отвечают области видимости (scope) переменных. Каждый раз, когда в скрипте мы используем слово/имя, не являющееся операндом или частью управляющей конструкции, то интерпретатор пытается найти место, где это слово было определено как имя переменной, функции или как другой объект. Такая проверка проходит поэтапно и выполняется поочередно для нескольких пространств имен - областей видимости:
L (Local) - Локальная область видимости, область внутри функции. Когда имя используется внутри функции, то интерпретатор пытается найти, было ли оно определено в этой функции. Если да, то дальше поиск не идет и все остальные места, где это имя определялось, игнорируются. Если нет, то поиск продолжается дальше вне этой функции.
E (Enclosing) - Охватывающая область видимости. Если функция вложена в другую функцию, то внешняя функция будет охватывающей областью для внутренней функции. Если имя не было найдено в локальной области, то поиск продолжается здесь - в охватывающей, если она имеется. Если имя всё ещё не найдено, то поиск дальше продолжается вне функции, в следующей области видимости.
G (Global) - Глобальная. Поиск имени в скрипте, вне каких-либо функций.
B (Built-in) - Встроенная. Область видимости, в которой находятся различные встроенные функции Python, например
print. Самостоятельно мы в ней никакие имена не определяем.
Рассмотрим, как это работает на примерах:
x = 'global x' # X определена вне функций, она находится в глобальной области
y = 'global y' # Y определена вне функций, она находится в глобальной области
def example():
y = 'local y' # Y определена в функции, она находится в локальной области
print(x) # Обращаясь к X, мы сначала ищем её в локальной области, если её нет, продолжаем искать в глобальной
print(y) # С Y происходит то же самое, но мы находим её в локальной области и игнорируем остальные значения
example()
global x
local y
# Если мы попробуем обратиться к Y из глобальной области, то и искать мы начнем сразу из неё
# То есть мы не будем знать, что переменная Y как-то определялась внутри функции
print(y)
global y
Похожим образом это работает со вложенными функциями:
x = 'x'
def example_outer():
y = 'y'
# Здесь мы можем обратиться к X и Y. Z здесь не определена
# X берется из глобальной
# Y берется из локальной (для этой функции)
def example_inner():
z = 'z'
# Здесь мы можем обратиться к X, Y, Z
# X берется из глобальной
# Y берется из охватывающей
# Z берется из локальной (для этой функции)
# Здесь мы можем обратиться только к X. Y и Z здесь не определены
# X берется из глобальной
Обобщая простыми словами описанное выше, мы можем сформулировать два основных правила:
Изнутри функций видны переменные, которые были определены внутри неё и снаружи
Снаружи функций не видны никакие переменные, определенные в ней
Изменение глобальных переменных из функций
Если нам всё же требуется изменить значение переменной внутри функции, чтобы эти изменения остались в глобальной области видимости, необходимо явно указывать, что это переменная глобальная или не локальная. В большинстве задач данная возможность нам не пригодится и скорее приведет к ошибкам, чем к нужному решению. При необходимости с тем, как это реализуется можно ознакомиться в документации: