Функции
Contents
Функции¶
Объявление функций¶
Про определение функций в python
можно подробно прочитать в документации по ссылке.
Объявление функции (function definition
) начинается с ключевого слова def
, за которым должно следовать название (имя) функции и список формальных параметров. Тело функции должно начинаться со следующей строки и отделяться отступом. Следующей строкой после заголовка может идти документирующая строка.
def function_name(param1, param2, ..., paramN):
"documentation string"
...
Когда интерпретатор встречает определение функции, он создаёт вызываемый объект-функцию и связывает этот объект с указанным при объявлении именем. Это имя также функции сохраняется в атрибут __name__
объекта-функции.
def add(x, y):
"""Return the sum of its arguments"""
return x + y
print(type(add))
print(add.__name__)
<class 'function'>
add
В ячейке выше на первой строке мы объявили функцию с именем add
принимающую два аргумента под именами x
и y
.
def add(x, y):
На следующей строке располагается документирующая строка.
def add(x, y):
"""Return the sum of its arguments"""
Документирующая строка сохраняется в атрибут __doc__
объекта функции и также выводится в справке по функции, которую можно сгенерировать встроенной функцией help.
print(add.__doc__)
print("-" * 40)
help(add)
Return the sum of its arguments
----------------------------------------
Help on function add in module __main__:
add(x, y)
Return the sum of its arguments
На последней строке функция возвращает результат вычисления выражения x + y
вызывающему коду.
def add(x, y):
"""Return the sum of its arguments"""
return x + y
Управление потоком управления и возвращение значений из функции¶
При вызове функции поток управления программой передаётся этой функции. Поток управления программой возвращается обратно вызывающему коду, когда исполнение функций завершается, что может произойти по трём причинам:
встречено ключевое слов
return
;достигнут конец функции;
возникло необработанное исключение, которое начало распространяться по стеку вызовов.
Последний случай разбирается позднее, разберем первые два.
Если встречается конструкция вида
def f():
...
return expression
т.е. ключевое слово return
и некоторое выражение expression
, то вызывающему коду возвращается результат вычисления expression
. Именно такая ситуация и встречается в определенной выше функции add
: в качестве expression
выступает выражение x + y
.
Если же после ключевого слова return
ничего не стоит, т.к. встречается конструкция вида
def f():
...
return
то вызывающему коду возвращается значение None
.
Если же функция заканчивается без ключевого слова return
, то после исполнения последней инструкции из функции возвращается значение None
. Т.е. следующие три определения функции эквиваленты.
def f():
print("Hello, world!")
def f():
print("Hello, world!")
return None
def f():
print("Hello, world!")
return
Таким образом функция всегда возвращает какое-то значение, если её исполнение завершилось без возникновения исключений. Это значение явно задаётся выражением справа от ключевого слова return
или возвращается None
, если ключевое слово return
не встретилось при исполнении тела функции или справа от оператора return
ничего не указано.
Функции как объекты первого класса¶
Прочитав объявление функции, интерпретатор создаёт вызываемый объект-функцию типа function
и связывает его с указанным при объявлении функции именем. При этом объекты функционального типа function
ничем не выделяются на фоне объектов всех остальных типов. Как и с любыми другими объектами в python
можно совершать следующие операции и над функциями:
присвоить переменной;
передать в качестве аргумента функции;
возвращать из функции;
В программировании такие объекты с такими свойствами принято называть объектами первого класса и далеко не во всех языках программирования функции являются таковыми. Но в python
это так и давайте продемонстрируем это.
Присваивание переменной¶
Присвоение переменной в python
— связывание с именем. В качестве демонстрации этой возможности свяжем встроенную функцию print с именем p
и вызовем её от этого имени.
p = print
p("Теперь я могу вызывать функцию print от имени p")
Теперь я могу вызывать функцию print от имени p
Передача в качестве аргумента функции¶
Передачу функции в качестве аргумента другой функции мы уже встречали на примере получения справки функцией help
. Разберем пример того, как пользовательская функция может принимать в качестве параметра объект-функцию.
def print_function_name(f):
print(f.__name__)
print_function_name(add)
print_function_name(print_function_name)
add
print_function_name
В ячейке выше определена примитивная функция print_function_name
, которая просто печатает содержимое атрибута __name__
функции. Далее на вход этой функции сначала передаётся определенная ранее функция add
. А потом на вход этой функции передаётся она сама! Да, python
настолько гибкий, что функция может обработать саму себя. Правда сложно представить себе ситуацию, где такая возможность пригодится и будет лучшей из всех возможных альтернатив.
Напишем чуть более содержательную функцию apply
, которая принимает на вход функцию f
и список l
и подменяет каждый элемент l[i]
значением f(l[i])
, т.к. применяет функцию f
к элементам списка l
на месте.
from math import sin, pi
def apply(f, l):
for i in range(len(l)):
l[i] = f(l[i])
return
x = [0, pi/6., pi/4., pi/3., pi/2.]
apply(sin, x)
print(x)
[0.0, 0.49999999999999994, 0.7071067811865476, 0.8660254037844386, 1.0]
На самом деле есть встроенная функция map, которая делает тоже самое, только не на месте.
y = list(map(sin, x))
print(y)
[0.0, 0.47942553860420295, 0.6496369390800625, 0.7617599814162892, 0.8414709848078965]
Возвращение из функции. Замыкание¶
Рассмотрим ситуацию, что мы работаем с функцией \(f\colon \mathbb{R}^2\to \mathbb{R}\) двух переменных. И иногда необходимо фиксировать значение одной из независимых переменных для решения какой-то подзадачи. Иными словами вам, например, необходимо получать функциональный объект \(f_{x_*}\colon \mathbb{R} \to \mathbb{R}\):
Такая ситуация может возникнуть, например, если вам необходимо найти корень функции \(f(x, y)\) по \(y\) при условии, что \(x=x_*\). Если вы умеете получать такой функциональный объект \(f_{x_*}(y)\), то остаётся решить уравнение скалярное уравнение \(f_{x_*}(y)=0\), для решения которых существуют опробованные библиотечные методы (например, этот метод, который будет разбираться позже в лекциях).
Чтобы добиться такого эффекта можно воспользоваться приёмом “замыкание”.
def fix_x(f, x):
def f_with_fixed_x(y):
return f(x, y)
return f_with_fixed_x
Разберем подробнее, как работает эта функция.
Идея функции fix_x
заключается в том, что она принимает на вход функцию двух переменных f
и число x
и фиксирует значение первого аргумента значением x
. Чтобы этого достичь, внутри функции определяется новая функция f_with_fixed_x
. При этом определение этой функции читается интерпретатором не когда создаётся читается определение функции fix_x
, а каждый раз, когда она вызывается. Итого, при каждом вызове функции fix_x
в её пространстве локальных имён создаётся новый функциональный объект с именем f_with_fixed_x
, который затем из функции и возвращается.
Функция f_with_fixed_x
зависит уже от одного параметра y
, а возвращает результат вычисления выражения f(x, y)
. При этом f
и x
отсутствуют в пространстве локальных имен функции f_with_fixed_x
, а значит подтягиваются из пространства локальных имен замыкающей функции fix_x
, созданном при том вызове функции fix_x
, при котором был создан этот функциональный объект f_with_fixed_x
.
Продемонстрировать работоспособность такой идеи.
def f(x, y):
return x ** 2 + y ** 2
for x in range(3):
for y in range(3):
print(f(x, y), end=" ")
print()
0 1 4
1 2 5
4 5 8
Определена функция \(f(x, y)=x^2 + y^2\) и вычислено её значение в точках \([0, 1, 2] \times [0, 1, 2]\) обычным вызовом функции \(f\) внутри двойного цикла по значениям \(x\) и \(y\).
Теперь проделаем это несколько иначе. Внешний цикл по \(x\) оставим как и прежде, а внутри него каждый раз будет создавать функцию \(g(y) = f(x, y)\) и внутри цикла по \(y\) будет вызывать уже её.
for x in range(3):
g = fix_x(f, x)
for y in range(3):
print(g(y), end=" ")
print()
0 1 4
1 2 5
4 5 8
Опциональные и обязательные параметры функции¶
Как и в C/C++
, у параметров функции могут быть значения по умолчанию (default value
). Такие параметры ещё называют опциональными (optional parameter
), т.к. при вызове функции их можно не указывать, а параметры без значений по умолчаний называют обязательными (required parameter
).
def say_hi_n_times(name, n=1):
for _ in range(n):
print(f"Привет, {name}!")
В примере выше объявлена функция say_hi_n_times
с обязательным параметром name
и опциональным параметром n
, у которого значение по умолчанию равняется 1.
Вызвать это функцию можно двумя способами:
указав оба аргумента:
say_hi_n_times("Иван", 3)
Привет, Иван!
Привет, Иван!
Привет, Иван!
указав только обязательный параметр
name
и опустив опциональныйn
:
say_hi_n_times("Иван")
Привет, Иван!
При объявлении функции можно указывать произвольное количество как и обязательных, так и опциональных параметров, но необходимо соблюдать следующее требование: все обязательные параметры должны быть перечислены ранее всех опциональных. Т.е. допустим только следующий синтаксис.
def f(req1, ..., reqN, opt1=v1, ..., optM=vM):
...
Нарушение этого требования — синтаксическая ошибка.
def f(x=0, y):
pass
Input In [13]
def f(x=0, y):
^
SyntaxError: non-default argument follows default argument
Про механизм вычисления значения по умолчанию надо помнить две детали.
Во-первых, значение по умолчанию вычисляется при объявлении функции.
x = "before"
def f(arg=x):
print(arg)
x = "after"
f()
before
Здесь f()
печатает строку “before”, а не “after”, потому что на момент, когда интерпретатор читал объявление функции f
и создавал соответствующий объект, имя x
ссылалось на объект 5
.
Во-вторых, это значение вычисляется всего один раз. Это играет роль, если значение по умолчанию — изменяемый объект.
def f(a, L=[]):
L.append(a)
return L
print(f(1))
print(f(2))
print(f(3))
[1]
[1, 2]
[1, 2, 3]
А здесь один и тот же список разделяется между тремя вызовами функции f
. Этот список создан при объявлении функции и затем используется при всех вызовах функции без второго аргумента.
Передача аргументов в функцию по имени¶
Во всех предыдущих примерах аргументы передавались в функцию позиционно, т.е. то, в какой параметр попадет значение при передаче его в качестве аргумента определялось тем, в какой позиции он был указан при вызове функции. В python
можно передавать аргументы так же и по именам параметров, нарушая при этом порядок, в котором параметры перечислены в заголовке вызываемой функции.
Для этого при вызове функции указывается имя параметра, затем знак “=
”, а затем значение, т.е. если мы хотим передать в функцию f
значение 5
в качестве параметра с именем x
, то используется следующий синтаксис.
f(...,x=5 ,...)
Это сильно увеличивает количество способов вызвать одну и ту же функцию. Например, в случае функции с двумя параметрами существует 4 равнозначных способа её вызвать, в том числе:
передав оба аргумента позиционно;
передав первый аргумент позиционно, а второй аргумент по имени;
передав оба аргумента по имени в том же порядке, в котором они перечисленны в определении функции;
передав параметры в противоположном порядке, чем порядок при котором они перечислены в определении функции.
def f(param1, param2):
print(f"First parameter: {param1}. Second parameter: {param2}")
f(0, 1)
f(0, param2=1)
f(param1=0, param2=1)
f(param2=1, param1=0) # в другом порядке
First parameter: 0. Second parameter: 1
First parameter: 0. Second parameter: 1
First parameter: 0. Second parameter: 1
First parameter: 0. Second parameter: 1
Как и в случае с опциональными параметрами в определении функции, есть ограничение на передачу параметров по имени: необходимо сначала указать все параметры, передаваемые позиционно, а только потом все параметры, передаваемые по именам.
f(param1, ..., paramN, param(N+1)=value1, ..., param(N+M)=value(N+M))
Нарушение этого правила — синтаксическая ошибка.
f(param1=0, 1)
Input In [17]
f(param1=0, 1)
^
SyntaxError: positional argument follows keyword argument
Сугубо позиционные параметры и сугубо именованные параметры¶
Иногда может быть удобно сделать прием каких-то параметров только по позиции, а ряда других параметров только по имени. Допустим следующий синтаксис:
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
----------- ---------- ----------
| | |
| По позиции или по имени |
| - Только по имени
-- Только по позиции
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
print(f"Только по позиции: {pos1} и {pos2}")
print(f"По позиции или по имени: {pos_or_kwd}")
print(f"Только по имени: {kwd1} и {kwd2}")
f(0, 1, 2, kwd1=3, kwd2=4)
print("-"*30)
f(0, 1, pos_or_kwd=2, kwd1=3, kwd2=4)
Только по позиции: 0 и 1
По позиции или по имени: 2
Только по имени: 3 и 4
------------------------------
Только по позиции: 0 и 1
По позиции или по имени: 2
Только по имени: 3 и 4
Примеры некорректных вызовов функции f
.
f(x=0, y=1, pos_or_kwd=2, kwd1=3, kwd2=4)
Первый два параметра должны быть переданы по позиции.
f(0, 1, 2, 3, 4)
Передано 5 позиционных аргумента, а в объявлении всего 3 параметра допускающих передачу по позиции.
Перехват произвольного количества позиционных параметров.¶
Python
допускает синтаксис, которые позволяет перехватывать произвольное количество позиционно переданных аргументов. Для этого при объявлении функции указывается параметр с символом “*
” перед ним. Обычно такому параметру дают имя *args
(сокращение от arguments
). Ниже приведен пример такого объявления.
def f(*args):
print(f"{len(args)=}, {args=}")
Вызывать такую функцию можно передавая произвольное количество параметров.
f()
f(0)
f(0, 1)
f(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
len(args)=0, args=()
len(args)=1, args=(0,)
len(args)=2, args=(0, 1)
len(args)=11, args=(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Все переданные в функцию значения попадают в кортеж, который связывается с именем args
.
def g(*args):
print(type(args))
g()
g(0)
g(0, 1)
<class 'tuple'>
<class 'tuple'>
<class 'tuple'>
Полный синтаксис допускает наличие обычных позиционных параметров и одного перехватывающего.
def f(pos1, ..., posN, *args):
Тогда в перехватывающий параметр попадают все оставшиеся аргументы.
def f(x, *args):
print(f"{x=}, {args=}")
f(0)
f(0, 1)
f(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
x=0, args=()
x=0, args=(1,)
x=0, args=(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Функция print — пример функции, объявленной таким образом. Действительно, мы можем вызывать печатать произвольное количество объектов за один вызов функции print
.
print(0)
print()
print(0, 1)
print(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
0
0 1
0 1 2 3 4 5 6 7 8 9 10
Функции min и max — ещё два примера таких функций. Если в любую из них передать два и более аргумента, то результатом будет минимальное или максимальное значение из всех аргументов.
print(min(0, 1, 3, -1, 2))
print(max(0, 1, 3, -1, 2))
-1
3
Распаковка последовательности по позиционным аргументам функции¶
Допустима в некотором смысле и обратная операция к перехвату всех позиционных аргументов, которая позволяет распаковать последовательность произвольной длины по позиционным аргументам функции. Для этого при передаче распаковываемого аргумента перед ним ставится “*
”. Если f
— вызываемая функция, s
— распаковываемая последовательность, то используется следующий синтаксис.
f(..., *s, ...)
Сравните следующие два вызова функции print
.
r = range(3)
print(r)
print(*r)
range(0, 3)
0 1 2
Тоже самое будет работать с любой последовательностью.
s = "abc"
l = [0, 1, 2]
print(s, sep=", ")
print(*s, sep=", ")
print(l)
print(*l)
abc
a, b, c
[0, 1, 2]
0 1 2
Естественно это может работает и с пользовательскими функциями.
def f(x, y, z):
print(f"{x=}, {y=}, {z=}")
x, y, z = range(3)
f(x, y, z)
f(*range(3))
x=0, y=1, z=2
x=0, y=1, z=2
Перехват произвольного количества переданных по имени аргументов¶
Чтобы перехватить произвольное количество параметров переданных по имени используется схожий синтаксис, но вместо одного символа “*
” перед перехватывающим параметром, ставится сразу два. Такому параметру обычно дают имя **kwargs
(сокращение от key words arguments
).
Ниже приведен пример такого объявления.
def f(**kwargs):
print(f"{len(kwargs)=}, {kwargs=}")
f()
f(x=1, y=2)
f(first_name="Иван", surname="Иванов", age=18)
len(kwargs)=0, kwargs={}
len(kwargs)=2, kwargs={'x': 1, 'y': 2}
len(kwargs)=3, kwargs={'first_name': 'Иван', 'surname': 'Иванов', 'age': 18}
В этом случае все переданные аргументы группируются в словаре. В качестве ключей выступают имена параметров, в качестве значений — переданные аргументы.
def g(**kwargs):
print(type(kwargs))
g()
<class 'dict'>
Естественно можно комбинировать перехват позиционных и именованных параметров.
def f(*args, **kwargs):
print(f"{args=}, {kwargs=}")
f("a", 1, letter="b", digit=2)
args=('a', 1), kwargs={'letter': 'b', 'digit': 2}
Распаковка словарей по аргументам функции¶
Словари тоже допускают распаковку по аргументам функции. В этом случае при вызове функции перед распаковываемым словарем ставится два символ “*
”. Если f
— вызываемая функция, а d
— распаковываемый словарь, то используется следующий синтаксис.
f(..., **d, ...)
def f(x, y):
print(f"{x=}, {y=}")
d = {"x": 1, "y": 2}
f(**d)
x=1, y=2
Естественно можно совмещать одновременную распаковку нескольких нескольких последовательностей и нескольких словарей, если у функции хватит параметров.
l1 = range(3)
l2 = [3, 4, 5]
d1 = {"sep": "->"}
d2 = {"end": " =)"}
print(*l1, *l2, **d1, **d2)
0->1->2->3->4->5 =)