Функции

Объявление функций

Про определение функций в 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

Управление потоком управления и возвращение значений из функции

При вызове функции поток управления программой передаётся этой функции. Поток управления программой возвращается обратно вызывающему коду, когда исполнение функций завершается, что может произойти по трём причинам:

  1. встречено ключевое слов return;

  2. достигнут конец функции;

  3. возникло необработанное исключение, которое начало распространяться по стеку вызовов.

Последний случай разбирается позднее, разберем первые два.

Если встречается конструкция вида

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 можно совершать следующие операции и над функциями:

  1. присвоить переменной;

  2. передать в качестве аргумента функции;

  3. возвращать из функции;

В программировании такие объекты с такими свойствами принято называть объектами первого класса и далеко не во всех языках программирования функции являются таковыми. Но в 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) = f(x_*, y).\]

Такая ситуация может возникнуть, например, если вам необходимо найти корень функции \(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 =)