Лекция 7. Позиционные и именованные аргументы. Рекурсия

Позиционные и именованные аргументы

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

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

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

def f(a, b):
    print(a, b)

x = f(1, 2)  # Сопоставление по порядку
x = f(b=2, a=1)  # Сопоставление по имени

Очевидно, что во втором случае, хоть значения аргументов идут и в обратном порядке, но параметр a всё равно получит значение 1, а b получит значение 2.

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

Различные случаи можно рассмотреть в примере ниже:

def g(a, b, c = 100, d = 500):  # с и d -- необязательные аргументы
                                # со значениями по умолчанию.
    print(a, b, c, d)

x = g(1)  # Ошибка, мало аргументов.
x = g(1, 2)          # 1 2 100 500
x = g(1, 2, 11)      # 1 2 11 500
x = g(1, 2, 11, 20)  # 1 2 11 20

x = g(1, 2, d=20)        # 1 2 100 20
x = g(1, 2, c=11)        # 1 2 11 500
x = g(1, 2, d=20, c=11)  # 1 2 11 20

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

Функции с произвольным числом аргументов

Если функция должна принимать переменное число параметров, как, например, max, min или print, можно автоматически упаковать много аргументов в один параметр:

def h(a, b, *rest):
    # В переменной rest будет содержаться переменная с кортежем
    # (неизменяемым списком) с аргументами после a и b.
    print(a, b, rest)

x = h()               # ошибка
x = h(1)              # ошибка
x = h(1, 2)           # 1 2 ()         (пустой кортеж)
x = h(1, 2, 3)        # 1 2 (3,)       (кортеж из одного элемента)
x = h(1, 2, 3, 4, 5)  # 1 2 (3, 4, 5)  (кортеж из трёх элементов)

Рекурсия

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

Для начала начнём с простейшего случая рекурсии и вычисления факториала по рекуррентной формуле:

\[0! = 1\]\[n! = (n-1)! * n\]

На Python рекурсивная функция вычисления факториала выглядит так:

def fact(n):
    if n == 0:
        return 0
    return n * fact(n - 1)

Рассмотрим, как будет производиться вычисление значения fact(4):

_images/fact_stack.png

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

def рекурсивная_функция(...):
    if терминальное_условие:
        терминальная ветвь
        return значение
    рекурсивная ветвь

Рассмотрим ещё один математический пример, вычисление чисел Фибоначчи. Так же как и факториал, последовательность чисел Фибоначчи может быть задана в рекурсивном виде:

\[a_0 = 0\]\[a_1 = 1\]\[a_{n} = a_{n-1} + a_{n-2}\]

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

Подобную рекуррентную последовательность можно почти дословно переписать с помощью рекурсивной функции:

def fib(n):
    if n == 0:
        return 0
    if n == 1:
        return 1
    return fib(n - 1) + fib(n - 2)

Как видно, у этой функции есть две терминальные ветви, при n == 0 или n == 1, и рекурсивная ветвь, которая всегда делает два рекурсивных вызова.

При вычислении fib(4) происходит рекурсивный обход следующего дерева:

_images/fib_tree.png

Этот процесс происходит с использованием стека вызовов следующим образом:

_images/fib_stack.png

Как видно, рекурсия справляется с задачей обхода дерева, но конкретная задача вычисления чисел Фибоначчи решается крайне неэффективно, так как есть много повторяющихся поддеревьев.

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

cache = [0, 1] + [-1] * 98    # Список из 100 чисел: 0, 1, затем 98 нулей.
def fib(n):
    if cache[n] >= 0:
        return cache[n]
    cache[n] = fib(n - 1) + fib(n - 2)
    return cache[n]