Функции#

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

Про определение функций в python можно подробно прочитать в документации по ссылке.

Объявление функции (function definition) состоит из заголовка и тела.

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

  • Далее приводится тело функции, инструкции в котором приводятся с постоянным отступом вправо относительно заголовка функции.

  • Первой строкой в теле функции допускается указывать документирующую строку.

def имя_функции(параметр1, ..., параметрN):
    "Документирующая строка."
    Первая инструкция в теле функции
    ...
    Последняя инструкция в теле функции

Инструкция снаружи тела функции    

Когда интерпретатор встречает определение функции, он создаёт вызываемый объект-функцию и связывает этот объект с указанным при объявлении именем. Это имя также функции сохраняется в атрибут __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 имя_функции():
    ...
    return expression

т.е. ключевое слово return и некоторое выражение expression, то вызывающему коду возвращается результат вычисления expression. Именно такая ситуация и встречается в определенной выше функции add: в качестве expression выступает выражение x + y.

Если же после ключевого слова return ничего не стоит, т.е. встречается конструкция вида

def имя_функции():
    ...
    return 

то вызывающему коду возвращается значение None.

Если же функция заканчивается без ключевого слова return, то после исполнения последней инструкции тела функции вызывающему коду возвращается значение None. Т.е. следующие три определения функции эквиваленты.

def имя_функции():
    print("Hello, world!")
def имя_функции():
    print("Hello, world!")
    return None
def имя_функции():
    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]

Возвращение из функции. Замыкание#

Функция может возвращать функцию из себя. Распространенное применение такого приема — декораторы (см. “Декораторы”). Одна из основных причин такой распространенности — возможность реализовать шаблон замыкание (closure).

Рассмотрим самый простой пример. Реализуем функцию, которая генерирует функцию, запоминающую момент своего создания.

from datetime import datetime
from time import sleep

def make_remembering_function():
    time_of_creation = datetime.now()
    def remembering_function():
        print(f"I was created at {time_of_creation}")
    return remembering_function

f1 = make_remembering_function()
sleep(1)
f2 = make_remembering_function()

for f in (f1, f2):
    f()
I was created at 2022-10-04 15:15:34.360450
I was created at 2022-10-04 15:15:35.372219

Разберем, как этот пример работает. Обратим внимание на функцию make_remembering_function.

def make_remembering_function():
    ...
    def remembering_function():
        ...
    return remembering_function

Для начала обратим внимание на то, что внутри тела этой функции объявляется другая функция remembering_function и она же возвращается в качестве результата. При каждом вызове функции make_remembering_function интерпретатор заново встречает объявление функции remembering_function, создаёт функциональный объект и связывает его с именем remembering_function в пространстве локальных имен функции make_remembering_function (каждому вызову одной и той же функции соответствует своё уникальное пространство локальных имен).

Теперь обратим внимание, что созданная функция remembering_function обращается к переменной time_of_creation, которая в этой функции не объявляется.

...
time_of_creation = datetime.now()
def remembering_function():
    print(f"I was created at {time_of_creation}")
...

Такие переменные считаются свободными переменными и к списку таковых даже можно получить доступ напрямую.

f1.__code__.co_freevars
('time_of_creation',)

Связывание свободных переменных с объектов происходит во время вызова функции. Поиск происходит в приоритете

  1. nonlocals — пространство локальных имен замыкающей функции (в данному примере пространство локальных имен функции make_remembering_function);

  2. globals — глобальное пространство имен модуля, в котором объявлена эта функция;

  3. builtins — пространство встроенных имен.

Искусственно произвести процесс разрешения можно средствами модуля inspect, который предназначен для инспекции python объектов. Метод inspect.getclosurevars разрешает все имена, которые используются в функции, но не объявлены в самой функции, с объектами.

import inspect

print(inspect.getclosurevars(f1))
print(inspect.getclosurevars(f2))
ClosureVars(nonlocals={'time_of_creation': datetime.datetime(2022, 10, 4, 15, 15, 34, 360450)}, globals={}, builtins={'print': <built-in function print>}, unbound=set())
ClosureVars(nonlocals={'time_of_creation': datetime.datetime(2022, 10, 4, 15, 15, 35, 372219)}, globals={}, builtins={'print': <built-in function print>}, unbound=set())

В данном примере имя time_of_creation обнаружено в пространстве имен замыкающей функции, а print в пространстве имен встроенных функций.

Рассмотрим несколько другой пример.

def make_greeter_function(name):
    def greet():
        print(f"Hi, {name}!")
    return greet


ivan = make_greeter_function("Ivan")
alex = make_greeter_function("Alex")

for f in (ivan, alex):
    f()
Hi, Ivan!
Hi, Alex!

Этот пример работает, основываясь на тех же механизмах, что и предыдущей, так как параметры функции — локальные переменные функции. В данном примере name — локальная переменная функции make_greeter_function, и она же является переменной из локального пространства имен замыкающей функции greet (иными словами она nonlocal для функции greet).

Опциональные и обязательные параметры функции#

Как и в 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 [42]
    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 значение value в качестве параметра с именем x, то используется следующий синтаксис.

f(...,x=value ,...)

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

  1. передав оба аргумента позиционно;

  2. передав первый аргумент позиционно, а второй аргумент по имени;

  3. передав оба аргумента по имени в том же порядке, в котором они перечисленны в определении функции;

  4. передав оба аргумента по имени в противоположном порядке, чем порядок при котором они перечислены в определении функции.

def f(param1, param2):
    print(f"First parameter: {param1}. Second parameter: {param2}")

f(0, 1)                 # 1
f(0, param2=1)          # 2
f(param1=0, param2=1)   # 3
f(param2=1, param1=0)   # 4
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)
  File "C:\Users\qujim\AppData\Local\Temp/ipykernel_11924/1298529622.py", line 1
    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 =)