04. Функции. Рекурсия.


Функция

Мы уже изучили множество встроенных функций - print(), input(), abs(), list(), range(), map(), zip(), sorted() и другие. Как мы знаем, функция (в том числе метод) возможно принимает на вход какие-то аргументы, по заданным инструкциям выполняет какой-то код при её вызове и возможно возвращает какие-то объекты.

Сегодня мы научимся сами создавать свои функции!

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

Кроме того, это позволяет избежать повторения и делает код многоразовым.

Как выглядит конструкция создания собственной функции:

def funcname(arguments): <------ аргументы необязательны (если вы этого хотите)
    ''' описание функции ''' <-- можно добавить документацию (например, если функция сложная)
    БЛОК КОДА
    return something <---------- функция может вернуть какой-то объект (если необходимо)
In [ ]:
def hi():
    print('Hello!')

hi()

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

Команда `return`

На самом деле функция всегда возвращает какое-то значение, просто когда она возвращает None мы говорим, что она возвращает "ничего". Это происходит если команда return совсем не пишется в инструкции или если она написана без сопутствующего возвращаемого значения.

In [ ]:
def foo():
    print('Nothing to return...')
#     return

type(foo())
In [ ]:
def sqrt(n, b=2):
    return n ** (1 / b)

sqrt(2), sqrt(2, 3)
In [ ]:
def sum_prod(x, y):
    return x + y, x * y # возвращает кортеж 

a, b = sum_prod(2, 5) # множественное присваивание
a, b

Команда `pass`

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

In [ ]:
def foo():
    pass # без команды pass (или без выполняемого кода), будет ругаться, что блока кода нет.
foo()

Аргументы

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

Фиксированные

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

In [ ]:
def hi(name):
    print('Hello,', name)
In [ ]:
hi('Mark')
In [ ]:
hi('Sonya', 'Mitya')

Дефолтные

Помните, как некоторые аргументы функции print() имеют дефолтные значения? Точно так же можно прописывать для собственной функции некоторые значения параметров по умолчанию.

In [ ]:
def hi(name='User', phrase='How are you?'):
    print('Hello,', name)
    print(phrase)
hi()
In [ ]:
hi('Monica')
In [ ]:
hi('Phoebe', 'What a beautiful day!')

Именованные

Когда мы вызываем функцию с некоторыми значениями, эти значения присваиваются аргументам в соответствии с их положением. Например, при вызове функции hi('Phoebe'), Питон считает что значение 'Phoebe' относится к первому аргументы, т.е. name = 'Phoebe'. А в случае hi('Phoebe', 'What a beautiful day!') второе значение относится ко второму аргументу, т.е. к phrase. Но мы можем и по-другому вызвать нашу функцию:

In [ ]:
hi(name='Phoebe', phrase="What's up?")
hi(phrase="What's up?", name='Phoebe')
hi('Phoebe', phrase="What's up?")
In [ ]:
hi(name='Phoebe', "What's up?") # но НЕ МОЖЕМ вот так! 
                                # оно и понятно, уже неочевидно 
                                # какой по счету аргумент имеется в виду (особенно если их больше двух)

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

Произвольные

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

В таком случае необходимо воспользоваться оператором "звездочка" (возможно, вы уже знакомились с ней на семинарах), которая собирает набор значений в переменную типа кортеж.

In [ ]:
def print_sum(*values):
    print(values)
    print(type(values))
    print(sum(values))

print_sum(1, 2, 3, 4, 5)

Несколько примеров:

In [ ]:
def f(a, b, *vals):
    print(a, b, vals)

# f(1) # работать не будет
f(1, 2)
f(1, 2, 3)
f(1, 2, 3, 4)
In [ ]:
def f(a, *vals, c):
    print(a, vals, c)

# f(1, 2) # работать не будет
f(1, 2, c=1) # c стало обязательно именованным при вызове

Переменные

С переменными тоже все не так просто. Есть разница, между переменной, которую вы создали вне функции и внутри функции. И не всегда можно так просто изменить переменную "извне" внутри функции.

Глобальные

Если переменная (как обычно) задана вне функций, то она называется глобальной. Ею можно воспользоваться в любом месте программы - как внутри функций, так и вне.

In [ ]:
x = 'global'

def f():
    print('x inside:', x)

f()
print('x outside:', x)

Причем неважно, задана ли переменная до или после создания функции. Главное чтобы она была задана до вызова функции, т.к. Питон при вызове функции начинает искать переменную pi в глобальном пространстве.

In [ ]:
def circle(r):
    return 2 * pi * r

pi = 3.14159265359
print(pi)
circle(1)

Локальные

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

In [ ]:
def f(arg):
    y = 'local ' * arg
    print(y)

f(2)

# y # будет ошибка, т.к. такая переменная неизвестна
# arg # как и эта тоже

Взаимодействие глобальных и локальных

  1. Использование одного и того же имени переменной для глобальной и локальной - никакой проблемы. Локальная живет внутри, глобальная остается какой была (т.е. не меняется)
In [ ]:
x = 'global'
def f():
    x = 'local'
    print('x inside:', x)
    
print('x outside:', x)
f()
print('x outside after call:', x)
  1. Изменение глобальной переменной внутри функции - как видим в примере выше, глобальная переменная не изменилась. Чтобы переписать её значение, необходимо воспользоваться командой global.
In [ ]:
x = 'global'
def f():
    global x
    x = 'local'
    print('x inside:', x)
    
print('x outside:', x)
f()
print('x outside after call:', x)

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

In [ ]:
x = 'global'
def f():
#     global x
    x = x * 2
    print('x inside:', x)
    
print('x outside:', x)
f()
print('x outside after call:', x)

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

In [ ]:
q = q * 2
  1. Если мы создадим функцию внутри функции, то она будет жить только внутри большей функции. При этом переменная, которая создалась локально (т.е. внутри большей функции) не будет видна внутренней функции и необходимо (так же как с глобальными переменными) указать откуда переменная.

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

In [ ]:
# # # x = 'global'
def g():
# # # #    global x
# # #    print('1)', x)
    x = 'local'
    print('2)', x)
    def h():
# #         nonlocal x
#         print('3)', x)
        x = 'innerlocal'
        print('4)', x)
    h()
    print('5)', x)

g()
# # # # # #h()
# # # print('6)', x)

Лямбда-функции

Снова, снова, снова. Питонисты любят краткость (иногда излишнюю). Поэтому функции, если это необходимо, можно задавать в одну строку. Есть некоторые внутренние различия между созданием функции с помощью def и lambda, но мы не станем в это вдаваться подробно. В целом рекомендация такая - не увлекайтесь lambda, не используйте её без особой надобности. Как правило, лямбда-выражение используют как аргумент функций схлжих с map (её первый аргумент это функция, которую применяют к каждому элементу последовательности).

Синтаксис лямбда-выражения такой:

lambda arg1, arg2, ..., argn : code(args)

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

In [ ]:
(lambda x: x**2)(3)

Но часто выражению дают имя функции:

In [ ]:
sqr = lambda x: x**2
sqr(3)

Как и в обычных функциях, лямбда поддерживает позиционные/именованные/дефолтные/произвольные аргументы:

In [ ]:
(lambda x, y, z: x + y + z)(1, 2, 3),\
(lambda x, y, z=3: x + y + z)(1, 2),\
(lambda x, y, z=3: x + y + z)(1, y=2),\
(lambda *args: sum(args))(1,2,3)

Использование функций

Здесь подразумевается функции как и созданные с помощью def, так и с помощью lambda. Далее покажем "приемлемые" случаи использования лямбда-выражения, но помните, что вместо лямбды всегда можно поставить любую другую функцию.

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

In [ ]:
points = [(1, 2), (2, 5), (3, 10)]
f = lambda x: x[0]**2 - 2 * x[0] * x[1] + x[1]**2
list(map(f, points))

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

In [ ]:
spisok = [1, 12, -10, 5, 3, -1]
poryadok = lambda x: -x

# poryadok(spisok) : -1 -12 10 -5 -3 1 => -12 -5 -3 -1 1 10 => 12 5 3 1 -1 -10
sorted(spisok, key=poryadok) 
In [ ]:
points = zip([1, 2, 3, 4], [-1, -10, 2, -9])
poryadok = lambda x: x[0] # x[1] # x[0] + x[1] # x[0] * x[1]
sorted(points, key=poryadok)

Точно так же устроены функции min и max:

In [ ]:
a = [1, 2, 3, -1, -2]
s = lambda x: -x
min(a, key=s), max(a, key=s)

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

In [ ]:
list(filter(lambda x: x > 0, (-1, 2, -3, 4, 5, -10)))
In [ ]:
list(filter(lambda x: x, (0, 2, 0, 0, 1, -1, 0, 1, 10, 0, 0, -1)))
In [ ]:
s = '1   2        4 5 '.split(' ')
s, list(filter(lambda x: x, s))

Рекурсия

Что такое рекурсия? Это процесс определения чего-нибудь в терминах самого себя. Например, рекурсивные последовательности задаются первыми несколькими элементами и формулой элемента n в терминах элементов n-1,..., n-k.

С помощью функций в Питоне можно задавать и решать рекурсивные алгоритмы. Чтобы создать рекурсивную функцию (причем, желательно не бесконечную), необходимо:

  1. Чтобы функция возвращала (return) саму себя (с новым аргументом), т.е. задана формула рекурсии
  2. Чтобы в зависимости от аргумента, возвращалась уже не наша функция, а какое-то значение, т.е. задан конец рекурсии

Разберемся на примере факториала. $$n! = n \cdot (n-1)! = \dots = n \cdot (n-1) \cdot \dots \cdot 2 \cdot 1$$

In [ ]:
def factorial(n):
    if n == 1: # конец рекурсии
        return 1
    else:
        return n * factorial(n - 1) # формула рекурсии

factorial(5)