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


Функция

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

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

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

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

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

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

hi()
Hello!

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

Команда return

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

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

type(foo())
Nothing to return...
Out[38]:
NoneType
In [39]:
def sqrt(n, b=2):
    return n ** (1 / b)

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

a, b = sum_prod(2, 5) # множественное присваивание
a, b
Out[40]:
(7, 10)

Команда pass

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

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

Аргументы

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

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

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

In [42]:
def hi(name):
    print('Hello,', name)
In [43]:
hi('Mark')
Hello, Mark
In [44]:
hi('Sonya', 'Mitya')
--------------------------------------------------------------------
TypeError                          Traceback (most recent call last)
<ipython-input-44-ec8569e50ef5> in <module>
----> 1 hi('Sonya', 'Mitya')

TypeError: hi() takes 1 positional argument but 2 were given

Дефолтные

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

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

Именованные

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

In [48]:
hi(name='Phoebe', phrase="What's up?")
hi(phrase="What's up?", name='Phoebe')
hi('Phoebe', phrase="What's up?")
Hello, Phoebe
What's up?
Hello, Phoebe
What's up?
Hello, Phoebe
What's up?
In [49]:
hi(name='Phoebe', "What's up?") # но НЕ МОЖЕМ вот так! 
                                # оно и понятно, уже неочевидно 
                                # какой по счету аргумент имеется в виду (особенно если их больше двух)
  File "<ipython-input-49-da2d69d767e3>", line 1
    hi(name='Phoebe', "What's up?") # но НЕ МОЖЕМ вот так!
                     ^
SyntaxError: positional argument follows keyword argument

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

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

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

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

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

print_sum(1, 2, 3, 4, 5)
(1, 2, 3, 4, 5)
<class 'tuple'>
15

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

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

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

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

Переменные

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

Глобальные

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

In [53]:
x = 'global'

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

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

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

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

pi = 3.14159265359
print(pi)
circle(1)
3.14159265359
Out[54]:
6.28318530718

Локальные

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

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

f(2)

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

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

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

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

In [58]:
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 outside: global
--------------------------------------------------------------------
UnboundLocalError                  Traceback (most recent call last)
<ipython-input-58-249b54102f5d> in <module>
      6 
      7 print('x outside:', x)
----> 8 f()
      9 print('x outside after call:', x)

<ipython-input-58-249b54102f5d> in f()
      2 def f():
      3 #     global x
----> 4     x = x * 2
      5     print('x inside:', x)
      6 

UnboundLocalError: local variable 'x' referenced before assignment

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

In [60]:
q = q * 2
--------------------------------------------------------------------
NameError                          Traceback (most recent call last)
<ipython-input-60-12632fbaee87> in <module>
----> 1 q = q * 2

NameError: name 'q' is not defined
  1. Если мы создадим функцию внутри функции, то она будет жить только внутри большей функции. При этом переменная, которая создалась локально (т.е. внутри большей функции) не будет видна внутренней функции и необходимо (так же как с глобальными переменными) указать откуда переменная.

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

In [61]:
# # # 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)
2) local
4) innerlocal
5) local

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

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

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

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

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

In [62]:
(lambda x: x**2)(3)
Out[62]:
9

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

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

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

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

In [64]:
(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)
Out[64]:
(6, 6, 6, 6)

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

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

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

In [65]:
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))
Out[65]:
[1, 9, 49]

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

In [66]:
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) 
Out[66]:
[12, 5, 3, 1, -1, -10]
In [67]:
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)
Out[67]:
[(1, -1), (2, -10), (3, 2), (4, -9)]

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

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

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

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

Рекурсия

Что такое рекурсия? Это процесс определения чего-нибудь в терминах самого себя. Например, рекурсивные последовательности задаются первыми несколькими элементами и формулой элемента 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 [72]:
def factorial(n):
    if n == 1: # конец рекурсии
        return 1
    else:
        return n * factorial(n - 1) # формула рекурсии

factorial(5)
Out[72]:
120