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
и не задумываетесь о том, что там происходит внутри.
На это же можно посмотреть и с другой стороны: если у вас есть какая-то сложная система (например, тот же автоматический открыватель-закрыватель окна),
вы пишете функцию, которая открывает окно, подавая нужные сигналы на блок управления, и вот как раз эта функция должна будет знать,
как общаться с этим блоком. А в остальной программе уже не думаете, как конкретно открывается окно, а просто вызываете функцию.