Functions and recursion - Learn Python 3 - Snakify

Lesson 8
Функции и рекурсия


1. функции

Напомним, что в математике факториал числа n определяется как n! = 1 ⋅ 2 ⋅ ... ⋅ n (как произведение всех целых чисел от 1 до n). Например, 5! = 1 ⋅ 2 ⋅ 3 ⋅ 4 ⋅ 5 = 120. Ясно, что факториал легко вычислить, используя цикл for. Представьте, что нам нужно в нашей программе несколько раз вычислить факториал разных чисел (или в разных местах кода). Конечно, вы можете написать вычисление факториала один раз, а затем с помощью Copy-Paste вставить его туда, где вам это нужно:

# вычислить 3!
res = 1
for i in range(1, 4):
    res *= i
print(res)

# вычислить 5!
res = 1
for i in range(1, 6):
    res *= i
print(res)

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

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

Теперь мы хотим показать вам, как написать функцию factorial() которая принимает один параметр - число и возвращает значение - факториал этого числа.

def factorial(n):
    res = 1
    for i in range(1, n + 1):
        res *= i
    return res

print(factorial(3))
print(factorial(5))

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

Затем идет тело функции. В Python тело должно быть отступом (по Tab или четыре пробела, как всегда). Эта функция вычисляет значение n! и сохраняет его в переменной res . Последней строкой функции является return res , которая выходит из функции и возвращает значение переменной res .

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

Мы хотели бы привести еще один пример. Вот функция max() которая принимает два числа и возвращает максимум из них (на самом деле эта функция уже стала частью синтаксиса Python).

def max(a, b):
    if a > b:
        return a
    else:
        return b

print(max(3, 5))
print(max(5, 3))
print(max(int(input()), int(input())))

Теперь вы можете написать функцию max3() которая принимает три числа и возвращает максимум из них.

def max(a, b):
    if a > b:
        return a
    else:
        return b

def max3(a, b, c):
    return max(max(a, b), c)

print(max3(3, 5, 4))

Встроенная функция max() в Python может принимать различное количество аргументов и возвращать максимум из них. Вот пример того, как можно написать такую ​​функцию.

def max(*a):
    res = a[0]
    for val in a[1:]:
        if val > res:
            res = val
    return res

print(max(3, 5, 4))
Все, переданное этой функции, соберет параметры в один кортеж, называемый a , который обозначен звездочкой.

2. Локальные и глобальные переменные

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

def f():
    print(a)

a = 1
f()

Здесь переменная a установлена ​​в 1, а функция f() печатает это значение, несмотря на то, что когда мы объявляем функцию f эта переменная не инициализируется. Причина в том, что во время вызова функции f() (последняя строка) переменная a уже имеет значение. Вот почему функция f() может отображать ее.

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

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

def f():
    a = 1

f()
print(a)

Мы получаем ошибку NameError: name 'a' is not defined . Такие переменные, объявленные внутри функции, называются локальными . После выхода из функции они становятся недоступными.

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

def f():
    a = 1
    print(a)

a = 0
f()
print(a)

Эта программа напечатает вам числа 1 и 0. Несмотря на то, что значение переменной a измененное внутри функции, вне функции остается неизменным! Это делается для того, чтобы «защитить» глобальные переменные от непреднамеренных изменений функции. Итак, если какая-либо переменная изменяется внутри функции, переменная становится локальной переменной, и ее модификация не изменит глобальную переменную с тем же именем.

Более формально: интерпретатор Python рассматривает переменную локальную для функции, если в коде этой функции есть хотя бы одна команда, которая изменяет значение переменной. Тогда эта переменная также не может быть использована до инициализации. Инструкции, которые изменяют значение переменной - operator = , += и использование переменной в качестве цикла for параметра. Однако, даже если оператор change-variable никогда не выполняется, интерпретатор не может его проверить, и переменная остается локальной. Пример:

def f():
    print(a)
    if False:
        a = 0

a = 1
f()

Произошла ошибка: UnboundLocalError: local variable 'a' referenced before assignment . А именно, в функции f() идентификатор a становится локальной переменной, так как функция содержит команду, которая модифицирует переменную a . Инструкция по модификации никогда не будет выполнена, но интерпретатор не проверяет ее. Поэтому, когда вы пытаетесь распечатать переменную a , вы обращаетесь к неинициализированной локальной переменной.

Если вы хотите, чтобы функция могла изменять какую-либо переменную, вы должны объявить эту переменную внутри функции, используя ключевое слово global :

def f():
    global a
    a = 1
    print(a)

a = 0
f()
print(a)

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

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

Например, предположим, что ваша программа должна вычислить факториал данного номера, который вы хотите сохранить в переменной f. Вот как вы не должны этого делать:

def factorial(n):
    global f
    res = 1
    for i in range(2, n + 1):
        res *= i
    f = res

n = int(input())
factorial(n)
print(f)
# делать другие вещи с переменной f

Это пример плохого кода, потому что трудно использовать другое время. Если завтра вам понадобится другая программа для использования функции «factorial», вы не сможете просто скопировать эту функцию и вставить новую программу. Вам нужно будет убедиться, что эта программа не содержит переменную f .

Гораздо лучше переписать этот пример следующим образом:

# начало фрагмента кода, который можно скопировать из программы в программу
def factorial(n):
    res = 1
    for i in range(2, n + 1):
        res *= i
    return res
# конец фрагмента кода

n = int(input())
f = factorial(n)
print(f)
# делать другие вещи с переменной f

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

return [a, b]

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

n, m = f(a, b)

3. Рекурсия

Как мы видели выше, функция может вызвать другую функцию. Но функции могут также называть себя! Чтобы проиллюстрировать это, рассмотрим пример факторно-вычислительной функции. Хорошо известно, что 0! = 1, 1! = 1. Как рассчитать значение n! для больших n? Если бы нам удалось вычислить значение (n-1) !, то мы легко вычислим n !, так как n! = N⋅ (n-1) !. Но как вычислить (n-1) !? Если мы вычислили (n-2) !, тогда (n-1)! = (N-1) ⋅ (n-2) !. Как рассчитать (n-2) !? Если ... В итоге мы получаем 0 !, что равно 1. Таким образом, для вычисления факториала мы можем использовать значение факториала для меньшего целого числа. Этот расчет можно сделать с помощью Python:

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

print(factorial(5))        

Ситуация, когда функция вызывает себя, называется рекурсией , и такая функция называется рекурсивной.

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

def f():
    return f()
Две наиболее распространенные причины, вызывающие бесконечную рекурсию:
  1. Неправильное условие остановки. Например, если в факториал-калькуляционной программе мы забываем поставить проверку, if n == 0 , то factorial(0) вызовет factorial(-1) , который вызовет factorial(-2) и т. Д.
  2. Рекурсивный вызов с неправильными параметрами. Например, если функция factorial(n) вызывает функцию factorial(n) , мы также получим бесконечную цепочку вызовов.

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