Рекурсивные и лямбда-функции

В Python функцию можно вызывать саму из себя. Это называется рекурсией. В качестве примера рекурсивной функции я приведу вычисление числа в степени n (n – целое число).

def pow(x, n):
    if n == 0:
        return 1
    else:
        return x*pow(x, n-1)

И, далее, можно ее вызвать так:

print( pow(2, 3) )

Теперь подробнее разберемся как она работает. Для начала заметим, что

то есть, для вычисления значения на текущем n-м шаге достаточно взять значение на предыдущем n-1-м шаге и умножить его на x. Эта формула, по сути, отражает принцип рекурсии. В нашем случае она будет выглядеть так:

x*pow(x,n-1)

Далее, запуск функции осуществляется с аргументами 2 и 3: pow(2, 3). Она помещается в стек вызова функций, в котором хранится порядок вызова различных функций. Далее, выполняется тело функции. Проверяется первое условие. Оно оказывается ложным, так как 3 == 0 дает false. Поэтому идет переход на else и прежде чем выполнить оператор return, снова вызывается та же функция pow(2, 2).

Выполнение функции pow(2, 3) останавливается, в стек помещается вторая функция pow(2, 2) и она запускается. Снова проверяется первое условие, оно ложно, переходим по else к оператору return и опять вызываем функцию pow(2, 1).

Здесь снова все повторяется, в стек помещается очередной вызов функции и по условию вызывается следующая функция pow(2, 0).

Теперь первое условие становится истинным и функция pow(2,0) возвращает значение 1 и рекурсия не идет дальше вглубь – она останавливается. Функция pow(2.0) завершается, она удаляется из стека вызовов и управление передается функции pow(2, 1). Но она частично уже выполнена. Поэтому, берется значение 1 от pow(2, 0), результат умножается на x=2 и величина 2 возвращается функцией pow(2, 1).

Функция pow(2,1) также удаляется из стека, управление переходит к вышестоящей функции pow(2,2) и здесь мы уже имеем результат умножения x*x, то есть, 2*2 = 4. Далее, возвращаемся к самой верхней функции pow(2,3), здесь 2*4=8 и этот результат становится результатом вычисления рекурсивной функции.

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

Для начала вспомним, что когда мы говорили о кортежах, то указывали на такую работу оператора присваивания:

x,y = (1,2)

Здесь x и y принимают значения 1 и 2 соответственно. Но когда этих значений было больше, чем переменных:

x,y = (1,2,3,4)

то возникала ошибка. Так вот, смотрите, если поставить оператор *, например, перед y, то ошибки не возникнет:

x,*y = (1,2,3,4)

и переменная x = 1, а y = [2, 3, 4]. То есть, во вторую переменную будут записаны все оставшиеся значения в виде списка. Или же, можно сделать так:

*x,y = (1,2,3,4)

Тогда первые три значения будут помещены в x:

x = [1, 2, 3]

а последнее в y: y = 4. То же самое работает и со списками:

x,*y = [1,"a",True,4]

и со строками:

*x, y, z = "Hello world!"

И вообще с любыми перечисляемыми типами данных. То есть, оператор * упаковывает оставшиеся значения в список. Правда, мы не можем упаковывать уже упакованные данные, например, так:

*y = 1,2,3

произойдет ошибка, но вот так:

x, *y = 1,2,3

будет работать.

Этот же оператор может выполнять и обратную операцию – распаковывать списки в набор данных. Смотрите, предположим, мы описываем, что счетчик в цикле for должен пробегать диапазон от -5 до 5 с шагом 1. Как вы уже знаете, это делается так:

for x in range(-5,6):
    print(x, end=" ")

Далее, мы хотим представить этот диапазон с помощью списка:

a = [-5, 6]

и передать его функции range. Если записать просто a:

for x in range(a):

То возникнет ошибка, т.к. функция ожидает числа, а не список. Но, записав * перед переменной a, произойдет распаковка списка в набор из двух чисел: -5 и 6:

for x in range(*a):

и программа сработает без ошибок. Тогда как узнать: когда этот оператор распаковывает данные, а когда запаковывает? Очень просто:

  1. Если выполняется присвоение данных переменной с оператором *, то данные упаковываются. Например, *y, x = 1,2,3.
  2. Если выполняется передача списка с указанием оператора *, то происходит его распаковка. Например, range(*[-5,6]).

Этот оператор можно использовать для объявления функций с произвольным числом аргументов:

def myFunc(*args):
    print(args)

и вызовем ее:

myFunc()
myFunc(1)
myFunc(1, 2)
myFunc(1, 3, "hello")

Смотрите, у нас args становится кортежем с переданными значениями при вызове функции. Значит, мы спокойно можем перебрать все значения кортежа, например, в цикле for:

def myFunc(*args):
    for arg in args:
        print(arg)
 
myFunc(1, 3, "hello")

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

def mySum(*args):
    S = 0
    for arg in args:
        S += arg
    return S
 
print( mySum(1,2,3,4,5) )

А что если мы хотим передавать функции неограниченное количество именованных аргументов? Вот так:

myFunc(arg1=1, arg2=2, arg3=3)

В этом случае аргумент у функции следует записать с двумя звездочками:

def myFunc(**kwargs):
    print(kwargs)

И при ее вызове мы видим, что kwargs – это словарь. Как перебирать элементы словаря мы уже знаем, например, можно сделать так:

def myFunc(**kwargs):
    for name, value in kwargs.items():
        print(name, value)

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

Но, если мы теперь попытаемся вызвать функцию вот так:

myFunc(1,2,3, arg1=1, arg2=2, arg3=3)

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

def myFunc(*args, **kwargs):
    print(args)
    print(kwargs)

Причем, вот эти неименованные аргументы должны всегда следовать перед именованными. Или же можно сделать так:

def myFunc(*args, sep="sep", end="end", **kwargs):
    print(args)
    print(kwargs)
    print(sep, end)

или так:

def myFunc(x, y, *args, sep="sep", end="end", **kwargs):
    print(args)
    print(kwargs)
    print(x, y)
    print(sep, end)

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

Анонимные или лямбда-функции

В Python существуют так называемые анонимные функции (они же лямбда-функции). Что это такое? Давайте для начала рассмотрим такой пример:

def showElements(lst, func):
    for x in lst:
        if func(x):
            print(x)
 
def __odd(x):
    return True if x%2 != 0 else False
 
a = [1,2,3,4,5,6,7]
showElements(a, __odd)

У нас имеется функция showElements, которая отображает из списка lst только те элементы, для которых функция func возвращает значение True. Далее, мы создаем вспомогательную функцию __odd, которая возвращает True для нечетных значений. И идет вызов showElements для списка a и селектора в виде функции __odd.

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

lambda arg1, arg2, …: выражение

Например, так:

r = lambda a,b: a+b

и, затем, вызвать ее:

print( r(1,2) )

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

В нашем случае мы можем записать такую функцию сразу в качестве второго аргумента:

showElements(a, lambda x: True if x%2==0 else False)

Видите, как это удобно, понятно и наглядно. Мы сразу видим как будет выбирать значения функция showElements.

Если анонимная функция не принимает никаких аргументов, то она записывается так:

p = lambda : "hello world"
p()

Вот такие интересные и полезные возможности в объявлении функций существуют в языке Python.

Задания для самоподготовки

1. Написать рекурсивную функцию для вычисления факториала числа n:

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

arg1, arg2, …, argN

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

Видео по теме