2.7. Функции

2.7.1. Общее представление

Вы уже сталкивались со стандартными функциями: abs, sqrt, да даже print и input — это все функции. По сути, функция — это отдельный фрагмент кода, который вы можете вызывать из более-менее любого места своей программы.

Поговорим для примера про функцию взятия модуля abs. Если вам в программе надо взять модуль какого-то числа, вы, конечно, можете просто написать честный if. Конкретно, пусть вам надо вычислить значение выражения \(abs(x)\), и записать его в переменную y. Вы можете написать вот так:

if x < 0:
   y = -x
else:
   y = x

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

y = abs(x)

Более того, вы можете аргументом функции abs передавать любое сложное выражение, например, если надо, писать abs(2 - 3 * x), и результат вычисления функции вы тоже можете не просто сохранять в переменную, а использовать его как надо, например, вы можете написать print(10 + 137 * abs(2 - 3 * x)). Ясно, что писать все это через ifы было бы сложнее.

Функция abs — она стандартная, т.е. питон автоматически ее знает, она встроена в язык. Но можно писать и свои функции, и в этой теме мы про это и поговорим.

2.7.2. Как объявлять функции

Давайте напишем функцию, которая вычисляет знак числа, т.е. которая будет равна -1, если число отрицательно, 0, если число равно нулю, и 1, если число положительно. Пишется это так:

def sign(a):
   if a < 0:
      return -1
   elif a > 0:
      return 1
   else:
      return 0

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

Давайте разберем, что тут написано. Сначала идет служебное слово def, которое собственно и обозначает, что это определение функции. Дальше идет имя функции (в нашем случае sign) — именно это имя мы будем в дальнейшем использовать, когда нам надо будет вызывать функцию. Имена, конечно, можно использовать какие угодно в разумных пределах, аналогично именам переменных. Дальше в скобках указывается то, что называется списком аргументов, про него еще поговорим ниже, а со следующей строки с отступом идет тело функции — собственно команды, которые надо выполнить.

Как это будет работать? После того, как мы определили такую функцию, дальше в основной программе мы можем написать, например

y = sign(2)

это значит, что надо вызвать функцию sign, передав ей аргументом число 2 (аналогично тому, как запись y = abs(x) обозначает, что надо вызвать функцию abs от числа x). По такой строчке происходит следующее: запускается код функции (начиная со строки if a < 0), при этом в переменную a внутри функции записывается значение 2, потому что именно оно было указано при вызове функции (в записи sign(2)).

Соответственно, функция выполняет проверку, правда ли, что a < 0, но поскольку в a записано 2, то проверка не срабатывает. Поэтому функция дальше проверяет, правда ли, что a > 0, на этот раз проверка срабатывает, и выполняется команда return 1.

Тут вы видите новую, незнакомую команду return. Это специальная команда, которая используется только в функциях. Она обозначает: прекратить выполнение функции, вернуться (return) в то место, откуда функция была вызвана, при этом в качестве результата функции считать то значение, которое указано после return, т.е. в нашем случае 1.

Поэтому по этой команде функция завершится, выполнение программы вернется обратно на строку y = sign(2), при этом значением функции будет считаться 1, поэтому получится, что в переменную y будет записано число 1.

Аналогично, функцию abs, про которую мы говорили выше, если бы ее не было стандартной, можно было бы написать так:

def abs(x):
   if x < 0:
      x = -x
   return x

Попробуйте это осознать.

2.7.3. Аргументы функции

То, что написано внутри скобок, как при объявлении функции, так и при ее вызове, называется аргументами (еще говорят параметры, это синонимы). То есть когда мы написали def sign(a): — мы объявили функцию sign, которая принимает один аргумент a. Когда мы потом пишем y = sign(2), мы вызываем эту функцию, передавая ей аргумент 2. (На самом деле это, конечно, два разных смысла одного слова. Есть даже специальные термины для этого: формальные и фактические аргументы. Но мы не будем сейчас углубляться в терминологию, тем более что в реальной жизни и то, и то называется просто аргументами.)

Поговорим подробнее про это. Аргументы функции — это по сути специальные переменные, которые будут видны только внутри этой функции, и которые должны быть заданы извне при вызове этой функции. Написав def sign(a):, мы указали, что внутри функции появится переменная a, начальное значение которой задается извне. Важно то, что это отдельная специальная переменная (говорят локальная переменная, про это еще будет ниже), никак не связанная с переменной a в основной программе (более того, в основной программе переменной a может вообще не быть).

У функции может быть сколько угодно аргументов; их имена, естественно, должны быть корректными именами переменных. Например, вы можете написать def foo(bar, buz, bee): — у этой функции три аргумента.

Соответственно, при вызове функции вы должны указать значения для всех аргументов. Как вы уже прекрасно знаете, это делается перечислением значений для аргументов в скобках после имени функции; если аргументов больше одного, то аргументы разделяются запятыми. При вызове функции в качестве аргументов можно использовать любые выражения, например, можно писать sign(2 + 3 * x) (и тогда в функции получится a = 2 + 3 * x), или foo(2 + 3 * x, 2 - 3 * x, 3 * x) (это чисто пример, конечно). Более того, в выражениях, конечно, можно использовать и другие, или даже те же самые функции, например, sign(2 + 3 * abs(3 - sign(x))).

Если при вызове функции вы указали слишком много или слишком мало аргументов, это, конечно, будет ошибкой.

Аргументов может и не быть, тогда и при объявлении, и при вызове функции надо просто ставить пустые скобки:

def abc():
    ...

...
x = abc()

Аргументы не обязаны быть числами; они могут принимать любые значения, которые могут принимать переменные (массивы, строки и т.д.). Естественно, при этом вам надо, чтобы трактова аргумента внутри функции и при ее вызове была одинаковой: если функция ожидает, что ей в качестве аргумента будет передан массив, а вы передали число, то скорее всего ничего хорошего не произойдет. Функция попробует выполнить свой код, но скорее всего где-то просто наткнется на ошибку. (Это, конечно, относится не только к типам аргументов, но и к аргументам в целом. Конечно, у каждого аргумента, как и у каждой переменной в программе, должен быть какой-то смысл, какое-то назначение, и если вы передали значение, которое не соответствует этому смыслу, то ничего хорошего скорее всего не выйдет…)

В простейших случаях аргументы функции оказываются «отвязаны» от внешних переменных; если вы пишете sign(x), то аргумент a внутри функции sign не будет связан никак с переменной x в основной программе (только значение x скопируется в a). Если функция будет менять значение a, то значение x меняться не будет. Но при передаче в функцию массивов и других сложных объектов будут наблюдаться те же спецэффекты, что и при обычном копировании массива. Если вы пишете:

def foo(a):
    a[1] = 10
    ...

...
x = [1, 2, 3]
foo(x)

то и переменная x основной программы, и аргумент a в функции будут указывать на один и тот же массив, и изменения в a будут видны в x. (И это полностью аналогично обычному копированию массивов: a = x.)

Примечание

На самом деле, то, что описано выше — это простейший вариант задания аргументов; питон поддерживает и более хитрые варианты (например, изложенным выше способом вы не можете создать функции типа print, у которых количество аргументов неизвестно заранее, и которые, более того, умеют принимать именованные аргументы типа sep=' '). Но про эти продвинутые варианты мы сейчас говорить не будем.

2.7.4. Локальные переменные

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

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

Примечание

Точнее, поскольку в питоне нет специального синтаксиса для объявления переменных, то различие глобальных и локальных переменных довольно тонкое и на первый взгляд неочевидное. Правило такое: если в функции вы что-то присваиваете переменной, то эта переменная считается локальной (и не будет связана с одноименной глобальной, если такая есть); если же вы ничего не присваиваете, а только как-то по-другому упоминаете переменную, то будет считаться, что вы хотите работать с глобальной переменной. В целом будьте готовы к разным неожиданностям здесь.

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

Пример:

a = 30
c = 40
z = 100

def do_something(x):
    a = x + 10
    b = a - 20
    return b + z

do_something(c)

Что здесь происходит: есть три глобальные переменные a, c и z. В строке do_something(c) вызывается функция do_something, ей в качестве аргумента передается значение переменной c (т.е. 40). Входим в функцию, ее аргумент x получается равным 40. В локальную переменную a записываем x + 10, т.е. 50. (При этом значение глобальной переменной a никак не изменилось.) В локальную переменную b записываем a - 20, т.е. 30 (При этом глобальной переменной b вообще нет, ну и не страшно.) Возвращаем значение b + z, причем b тут имеется в виду локальная (т.к. мы раньше в нее записали 30), а z — глобальная (т.к. такую локальную переменную мы не создавали).

Примечание

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

def do_something(x):
   global a
   a = x + 10

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

2.7.5. Возвращаемое значение

Как мы уже обсуждали, возвращаемое значение — это то, что указывается в команде return, и что потом будет использоваться в качестве значения функции в месте ее вызова (т.е. что будет сохранено в переменную y, если мы, например, пишем y = sign(x)).

Конечно, в команде return можно писать любое выражение, причем это, конечно, не обязательно должно быть число. Аналогично, использовать в месте вызова результат выполнения функции мы можем как угодно, а не только сохранять в переменную, например, написав y = 20 + sign(x) и даже print(a[sign(x)]), если у вас есть массив a.

В частности, мы можем в месте вызова функции никак не использовать возвращаемое значение, написав просто отдельную команду (на отдельной строке) типа

do_something(x)

В таком случае код функции отработает, а результат, указанный в return, будет просто забыт. Это бывает полезно, если функция вам нужна не для простых вычислений (как abs или наша sign), а для каких-то действий, которые эта функция производит. Типичный пример — функция print. Нет никакого смысла писать x = print(y), а запись просто print(y) вполне имеет смысл; вы вызываете print не ради возвращаемого значения, а ради вывода на экран. Соответственно, вы вполне можете и сами писать такие функции.

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

def foo(x):
   print(x + 20)

Тут нет ни одного return, поэтому функция просто доработает до конца своего тела и вернется.

Примечание

На самом деле пустой return, а также завершение функции без return не возвращает ничего, а возвращает специальное значение None.

Вообще, иногда говорят о разделении на функции и процедуры — функциями в этом, узком, смысле слова называют функции, которые возвращают какое-либо значение, а процедурами — то, что не возвращает никакое значение. В некоторых языках (в первую очередь в паскале) это яркое синтаксическое различие: есть два разных служебных слова: procedure и function для объявления процедур и функций, и в принципе эти два термина стараются не путать. В других языках (C++, Java) используется только термин «функция», но для функций, которые не возвращают никакое значение, используется специальный тип такого «возвращаемого» значения — void, — и такие функции ведут себя немного по-другому (их результат в принципе нельзя никуда сохранить, компилятор не позволит), поэтому все-таки небольшая разница между процедурами и функциями есть, пусть даже термин «процедура» не используется.

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

def test(x):
   if x < 0:
      return 10
   if x > 0:
       return

тут если x < 0, то возвращается значение 10, если x > 0, то попадаем на пустой return, а если x == 0, то функция вообще просто дойдет до конца своего тела без return’ов. (И в соответствии со сказанным выше в двух последних случаях на самом деле будет возвращено None.)

Но так делать не надо (ну, за исключением совсем особых случаев). Лучше и понятнее код, в котором у каждой функции есть вполне понятный смысл и назначение; и такие функции или всегда возвращают что-то, или никогда ничего (кроме None не возвращают). Поэтому если вы предполагаете, что возвращаемое значение функции имеет смысл использовать, то пишите явный return со значение во всех возможных ветках, а если нет — то пишите везде пустой return (ну кроме самого конца функции, где его можно не писать.)

При этом бывает так, что в функции, которая обычно что-то возвращает, вам иногда надо вернуть None (например, так нередко делают в функциях поиска какого-нибудь объекта: возвращается или найденный объект, или None). Но тогда пишите явно return None, чтобы было видно, что вы это делаете намеренно.

2.7.6. Зачем нужны функции

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

В простейших ситуациях (с которыми вы и столкнетесь в первую очередь) можно выделить следующие причины, зачем вам нужны функции.

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

Второе — это возможность выделения смысловых блоков программы. Функция в идеале должна быть некоторым законченным фрагментом кода, который выполняет некоторую понятную задачу. И тогда, когда вы эту функцию вызываете, сразу понятно, что происходит. В принципе, это видно даже на примере функции abs: если вы пишете abs(5 - x), сразу понятно, что вы имеете в виду \(|5 - x|\). А если бы вы писали бы через if, то это было бы не очень очевидно, вам пришлось бы потратить несколько секунд на размышления и понимание того, что этот if обозначает просто модуль.

Это еще важнее в более крупных программах, где нужная последовательность действий состоит из нескольких крупных шагов. Пусть, например, вы делаете систему умного дома, и вам надо скачать прогноз погоды из интернета, выделить прогноз осадков в ближайшие 6 часов, и в зависимости от этого открыть или закрыть окно в комнате. Даже если эти шаги нигде не повторяются, зачастую удобно их вынести в отдельные функции, чтобы сразу было видно: тут мы скачиваем данные, тут решаем, открыть или закрыть, а вот тут собственно подаем команды на управляющий блок окна. Если каждый шаг не очень тривиален, то выделение шагов в функции резко повышает понятность и читаемость программ. (Конечно, для этого надо выбрать адекватное название для каждой функции.) Кроме того, вам намного проще будет потом менять программу; если вы захотите поменять принцип, по которому открывается или закрывается окно, вам вообще не придется трогать часть функций. Заодно еще одно удобство — вы можете использовать локальные переменные, и они не будут мешаться друг другу.

Третья причина для использования функций, ну или на самом деле комбинация первой и второй, но заслуживающая отдельного упоминания — это создание параметризуемого кода. То есть пусть у вас есть какая-то операция, какой-то фрагмент кода, который выполняется несколько раз, но каждый раз слегка по-разному. Зачастую вы его тоже можете легко выделить в функцию, а это самое различие передавать просто аргументами функции. Аналогично, если у вас есть какой-то смысловой блок, который тоже может выполняться по-разному (например, окно можно открыть, а можно и закрыть), вы его тоже можете выделить в функцию, сделав параметром указание на то, как именно надо выполнять этот блок (надо конкретно открывать или закрывать окно).

Четвертая причина — это рекурсия. Вообще, понятно, что из функции вы в принципе можете вызывать другие функции (например, вы можете написать функцию foo, которая внутри себя будет использовать функцию abs, если ей надо — почему бы нет?), но также вы из функции можете вызывать её же саму. Это и называется рекурсией. (Естественно, надо делать какое-то ограничение таких вызовов, чтобы не получилась бесконечная рекурсия). Я не будут про это писать подробнее, но если вы все, что было написано выше, уже поняли, то можете обдумать этот абзац отдельно.

Ну и пятая причина, которая на самом деле является вариацией второй причины (про смысловые блоки), но заслуживает отдельного упоминания — это, как говорят, инкапсуляция кода. Функции позволяют вам скрыть всю свою сложность, всю нетривиальность, позволив вам в основной программе не задумываться о том, как функция устроена внутри, а просто вызвать эту функцию. Ярким примером этого принципа являются функции print и input. Вы сейчас, скорее всего, даже теоретически не понимаете, что же такое делают эти функции внутри себя, как так получается, что функция print выводит текст на экран, а input считывает текст с клавиатуры. Но вам это и не важно; вы просто пишете input и не задумываетесь о том, что там происходит внутри. На это же можно посмотреть и с другой стороны: если у вас есть какая-то сложная система (например, тот же автоматический открыватель-закрыватель окна), вы пишете функцию, которая открывает окно, подавая нужные сигналы на блок управления, и вот как раз эта функция должна будет знать, как общаться с этим блоком. А в остальной программе уже не думаете, как конкретно открывается окно, а просто вызываете функцию.