Python 3. Функции

Разберём функции Python 3. Узнаем зачем они нужны, как их создавать и выполнять. Познакомимся с рекурсией, замыканиями и декораторами.















Функции в Python 3




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




Функции в Python создаются с помощью оператора def, после которого идёт название функции, круглые скобки и двоеточие. После этого, в блоке ниже описывается функциональность данной функции, такой блок называют телом функции.




Вот пример простейшей функции, которая просто выводит некий текст в консоль:




def my_func():
    print('hello')




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




my_func()

### результат выполнения
hello




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




def my_func():
    a = 2
    b = 3
    return(a+b)




Давайте вызовем функцию написанную выше:




my_func()




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




print(my_func())

### результат выполнения
5




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




Области видимости переменных




Локальные переменные




Возьмём предыдущую функцию:




def my_func():
    a = 2
    b = 3
    return(a+b)




В ней есть две переменные: a и b. Эти переменные являются локальными для функции my_func. Это означает что в глобальном коде эти переменные не видны. Чтобы убедиться в этом, выполним функцию и попробуем получить значение переменной a:




my_func()
print(a)

### результат выполнения
NameError: name 'a' is not defined




Локальные переменные создаются в момент выполнения функции и исчезают в момент завершения её работы. И они никогда не видны в глобальной области видимости. Это позволяет создавать несколько функций с одинаковыми переменными:




def my_func():
    a = 2
    print(a)

def my_finc2():
    a = 3
    print(a)

my_func()
my_finc2()

print(a)

### результат выполнения
2
3
NameError: name 'a' is not defined




То есть первая функция вывела значение своей локальной переменной a = 2. Вторая функция вывела значение своей переменной a = 3. А из основного кода (из модуля) значение переменной a не смогло вывестись, так как там такой переменной нет.




Вложенные функции и поиск переменных




Функции в Python допускают вложенность, то есть в одной функции можно описать другую функцию. Это позволяет делать множество действий описанных ниже, но сейчас мы рассматриваем поиск переменных.




Давайте представим, что у нас есть функция, а внутри этой функции другая функция. И в обоих функциях, и в глобальном коде (модуле) есть одноимённые переменные, например:




a = 'глобальная переменная a'
b = 'глобальная переменная b'
с = 'глобальная переменная c'
def my_func():
    a = 'локальная переменная my_func a'
    b = 'локальная переменная my_func b'
    c = 'локальная переменная my_func c'
    def my_inner():
        a = 'локальная переменная my_inner a'
        b = 'локальная переменная my_inner b'
        c = 'локальная переменная my_inner c'




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




Давайте изменим предыдущий код функции, чтобы функция my_func() выполняла функцию my_inner(), а функция my_inner() выводила значение переменной — ‘a‘:




a = 'глобальная переменная a'
b = 'глобальная переменная b'
с = 'глобальная переменная c'
def my_func():
    a = 'локальная переменная my_func a'
    b = 'локальная переменная my_func b'
    c = 'локальная переменная my_func c'
    def my_inner():
        a = 'локальная переменная my_inner a'
        b = 'локальная переменная my_inner b'
        c = 'локальная переменная my_inner c'
        print(a)
    my_inner()

my_func()

### результат выполнения
локальная переменная my_inner a




Теперь удалим переменную ‘a‘ из функции my_inner() и повторим:




a = 'глобальная переменная a'
b = 'глобальная переменная b'
с = 'глобальная переменная c'
def my_func():
    a = 'локальная переменная my_func a'
    b = 'локальная переменная my_func b'
    c = 'локальная переменная my_func c'
    def my_inner():
        b = 'локальная переменная my_inner b'
        c = 'локальная переменная my_inner c'
        print(a)
    my_inner()

my_func()

### результат выполнения
локальная переменная my_func a




А теперь удалим переменную ‘a‘ из функции my_func():




a = 'глобальная переменная a'
b = 'глобальная переменная b'
с = 'глобальная переменная c'
def my_func():
    b = 'локальная переменная my_func b'
    c = 'локальная переменная my_func c'
    def my_inner():
        b = 'локальная переменная my_inner b'
        c = 'локальная переменная my_inner c'
        print(a)
    my_inner()

my_func()

### результат выполнения
глобальная переменная a




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




Не локальные переменные




А если мы хотим из вложенной функции изменить переменную в родительской функции? Для этого нужно использовать оператор nonlocal. Давайте попробуем изменить переменную ‘a‘ в функции my_func() из вложенной функции my_inner(). Дополнительно выведу значение переменной из глобального кода, который видит только свои глобальные переменные.




a = 'глобальная переменная a'
b = 'глобальная переменная b'
с = 'глобальная переменная c'
def my_func():
    a = 'локальная переменная my_func a'
    b = 'локальная переменная my_func b'
    c = 'локальная переменная my_func c'
    def my_inner():
        nonlocal a
        a = 'изменённая переменная a'
    my_inner()
    print(a)

my_func() 
print(a) # глобальный код видит только глобальные переменные

### результат выполнения
изменённая переменная a
глобальная переменная a




То есть, с помощью оператора nonlocal мы указали что функция my_inner() обращается не к локальной а к родительской переменной ‘a‘.




Сама функция my_func() вначале инициализирует переменные. Затем выполняет вложенную функцию my_inner(), которая изменяет переменную у родительской функции. Затем функция my_func() выводит значение переменной ‘a‘.




Дополнительно, в глобальном коде после вызова функции my_func(), выводится значение глобальной переменной ‘a‘, чтобы убедиться что оно не поменялось.




Глобальные переменные




А теперь давайте из вложенной функции my_inner() изменим глобальную переменную ‘a‘. Для этого используется оператор global, работает он схожем образом с оператором nonlocal, но указывает что обращаться мы будем не к родительской функции, а к глобальной переменной.




a = 'глобальная переменная a'
b = 'глобальная переменная b'
с = 'глобальная переменная c'
def my_func():
    a = 'локальная переменная my_func a'
    b = 'локальная переменная my_func b'
    c = 'локальная переменная my_func c'
    def my_inner():
        global a
        a = 'изменённая переменная a'
    my_inner()
    print(a)

my_func() 
print(a) # глобальный код вдит только глобальные переменные

### результат выполнения
локальная переменная my_func a
изменённая переменная a




Здесь из функции my_inner(), с помощью оператора global, изменяется глобальная переменная ‘a‘. А локальная переменная ‘a‘ функции my_func(), не изменяется.









Передача параметров в функцию




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




Вот пример функции с аргументами:




def my_func(a, b):
    print(a + b)




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




my_func(2, 1)
my_func(3, 5)

### результат выполнения
3
8




Здесь ‘a‘ и ‘b‘ — это аргументы функции. Ну а числа, с которыми мы вызывали функцию (2, 1) и (3, 5) — называют параметрами.




Значения аргументов по умолчанию




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




def my_func(a=1, b=1):
    print(a + b)

my_func()
my_func(3, 5)

### результат выполнения
2
8




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




Неопределённое множество параметров




Если мы не знаем сколько параметров будет передаваться в функцию, то в качестве аргумента можно использовать *args. При этом внутри функции Python локальная переменная args будет кортежем переданных параметров:




def my_func(*args):
    print(args)      # кортеж переданных объектов
    print(sum(args)) # сумма объектов в кортеже
    for i in args:   # в цикле пробегаемся по элементам кортежа
        print(i)

my_func(3, 5, 7)

### результат выполнения
(3, 5, 7)
15
3
5
7




Именованные параметры




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




def my_func(a, b, c):
    print(a, b, c)

my_func(a=15, c=4, b=8) # позиционные параметры

### результат выполнения
15 8 4




Неопределённое множество именованных параметров




Если мы не знаем, сколько будет передано в функцию именованных переменных, то нужно использовать агрумент **kwargs. При этом внутри функции Python можно обратится к двум разным объектам:




  • kwargs — словарь переданных позиционных параметров, в виде ключ => значение;



  • *kwargs — только ключи словаря.




def my_func(**kwargs):
    print(kwargs)  # словарь позиционных параметров
    print(*kwargs) # только ключи словаря
    

my_func(a=15, c=4, b=8) # позиционные параметры

### результат выполнения
{'a': 15, 'c': 4, 'b': 8}
a c b




А так можно пробежаться по словарю переданных параметров:




def my_func(**kwargs):
    for i in kwargs:
        print(i, '=>', kwargs[i])

my_func(a=15, c=4, b=8) # позиционные параметры

### результат выполнения
a => 15
c => 4
b => 8









Лямбда функции (выражения)




Лямбда функции или их ещё называют выражения — это мини функции Python, которые записываются в одну строку и для них не создаётся имени. Выражения всегда возвращают результат, и при этом не нужно использовать return.




Для их создания используется оператор lambda, дальше записываются параметры (через запятую), затем после двоеточия записывается выражение (что с этими параметрами нужно сделать).




Пример кода с lambda выражением, при этом сохраняем выражение в переменной:




# вначале создаём переменную как выражение
# затем используем эту переменную
my_sum = lambda x, y: x+y
print(my_sum(3, 5))

### результат выполнения
8




Пример кода без сохранения lambda выражения в переменной:




print((lambda x, y: x+y)(3, 5))

### результат выполнения
8




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




Замыкания




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




Замыкание — это когда вложенная функция использует переменную из родительской. При этом родительская функция возвращает дочернюю функцию, но без её вызова (без скобочек return inner_func а не return inner_func()).




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




Вот пример замыкания:




def main_func(value):
    name = value
    def inner_func():
        print('Привет мой друг', name)
    return inner_func

b = main_func('Миша')
b()
c = main_func('Саша')
c()

### результат выполнения
Привет мой друг Миша
Привет мой друг Саша




И так, у нас есть родительская функция main_func() и дочерняя inner_func(). Родительская возвращает дочернюю (return inner_func). Дочерняя использует переменную родительской функции — name. Значит замыкание мы сделали.




Затем, в основном коде я делаю экземпляры функции: b = main_func(‘Миша’) и c = main_func(‘Саша’) и выполняю их.




То есть функция a — это функция inner_func() в которой предопределена переменная name = ‘Миша’. И подобная функция b — в которой предопределена переменная name = ‘Саша’.




Это самый простой пример, второй пример чуть посложнее:




def adder(value):
    def inner(a):
        return value + a
    return inner
a3 = adder(3)
a4 = adder(4)
print(a3(1))
print(a4(3))

### результат выполнения
4
7




Здесь у нас есть родительская функция main_func(value), и дочерняя inner_func(a). Дочерняя использует переменную от родительской — value. И родительская функция возвращает дочернюю — return inner.




Создаём экземпляры функции — a3 = adder(3) и a4 = adder(4). При этом функция a3() — это функция inner(a) с предопределённым value = 3. А функция a4() — это функция inner(a) с предопределенным value = 4.




Затем мы можем пользоваться этими функциями.




Очень хорошее видео по замыканиям, можно посмотреть здесь.




Декораторы




Чтобы усвоить эту тему нужно хорошо понять замыкания, так как декоратор это почти замыкание. Как и в замыкании, здесь присутствует вложенная функция. А родительская возвращает вложенную функцию без её вызова (без скобочек). И во вложенной функции используется переменная из родительской функции. Но в декораторе эта общая переменная — является функцией, которую мы будем декорировать.




Декораторы нужны, чтобы в вашей функции добавилась новая функциональность.




Вот пример:




# Декоратор
def header(func):
    def inner():
        print('<h1>')
        func()
        print('</h1>')
    return inner

# Функция
def say():
     print('hello world')

# Вызов функции
say()

### результат выполнения
hello world




Вначале мы создали функцию-декоратор header(func). Он выполняет вложенную функцию inner(), которая печатает html тег <h1>, затем выполняет функцию func(), а затем печатает закрывающий html тег </h1>.




А функция say() — просто выводит на консоль hello world. Здесь мы написали декоратор, но не использовали его.




Теперь навесим декоратор на функцию say(). Для этого просто перед созданием функции say() напишем символ ‘@’ и имя функции декоратора (@header):




# Декоратор
def header(func):
    def inner():
        print('<h1>')
        func()
        print('</h1>')
    return inner

# Навесили декоратор на функцию
@header
def say():
     print('hello world')

# Вызов функции
say()

### результат выполнения
<h1>
hello world
</h1>




Таким образом мы изменили функциональность функции say(). Эта функция теперь ссылается на вложенную функцию inner() в функции header(func).




А чтобы функция могла принимать аргументы мы должны прокинуть эти аргументы во вложенную функцию с помощью *args и **kwargs:




# Декоратор
def header(func):
    def inner(*args, **kwargs):
        print('<h1>')
        func(*args, **kwargs)
        print('</h1>')
    return inner

# Навесили декоратор на функцию
@header
def say(n):
     print(n)

# Вызов функции
say('hello')

### результат выполнения
<h1>
hello
</h1>




Таким образом у нас есть просто функция say(), которая выводит в терминал то, что ей передают. И мы можем её задекорировать, декоратором @header, который мы написали ранее. Тогда функция say() начнёт обрамлять вывод html тегами <h1> и </h1>.




Также, можете посмотреть видео по декораторам — здесь.




Рекурсия




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




Допустим у нас есть следующая рекурсивная функция — rec(x), которая:




  1. выводит значение переменной x;



  2. вызывает саму себя, но с новыми значениями — rec(x+1); — рекурсивная функция



  3. выводит значение переменной x;



  4. и так до тех пор, пока x<4. — условие выхода из рекурсии




Записывается такая рекурсивная функция на языке Python следующим образом:




def rec(x):
    if x<4:
        print(x) # до входа в следующий уровень рекурсии
        rec(x+1) # рекурсивная функция
        print(x) # после выхода из предыдущей функции
rec(1)

### результат выполнения
1
2
3
3
2
1




Ещё раз разберу как работает рекурсия на этом примере:




  1. Мы вызываем функцию rec(1), она проверяет что x<4, и выводит в консоль 1. Сохраняет состояние (x=1).



  2. Затем запускается rec(2). Она проверяет что x<4, и выводит в консоль 2. Сохраняет состояние (x=2).



  3. Затем запускается rec(3). Она проверяет что x<4, и выводит в консоль 3. Сохраняет состояние (x=3).



  4. Затем запускается rec(4). Проверяет что x<4, и так как условие не выполняется то функция завершает свою работу.



  5. Продолжает работать предыдущая функция, она запомнила что x=3, выводит в консоль 3. И завершает свою работу.



  6. Продолжает работать предыдущая функция, она запомнила что x=2, выводит в консоль 2. И завершает свою работу.



  7. Продолжает работать предыдущая функция, она запомнила что x=1, выводит в консоль 1. И завершает свою работу.




Пример рекурсии «нахождение чисел Фибоначчи»




Вот ещё один пример рекурсивной функции. Это поиск чисел Фибоначчи, это такие числа, которые образуются от сложения предыдущих двух чисел, начинается такой список от 0, второе число 1, а дальше начинается сложение чисел. Вот часть чисел Фибоначчи: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 и так далее.




Здесь первое число всегда равно 0, а второе число равно 1. Остальные числа легко высчитываются по формуле (предыдущее число) + (число перед предыдущим). Другими словами формула такая: (n-1) + (n-2).




Код такой функции:




def fib(n):
    if n==1:
        return 0 # первое число фибоначчи всегда 0
    if n==2:
        return 1 # второе число фибоначчи всегда 1
    return fib(n-1) + fib(n-2)
print(fib(5))




Мы ищем пятое число Фибоначчи. Если n=1, то это ноль. Если n=2, то это единица. Любое другое число высчитывается по формуле: fib(n-1) + fib(n-2).




В нашем примере функция работает так:




  1. fib(5) — запомнили что n=5 и вызывали fib(4) + fib(3);

    • fib(4) — запомнили что n=4 и вызвали fib(3) + fib(2);

      • fib(3) — запомнили что n=3 и вызвали fib(2) + fib(1);

        • fib(2) — вернул 1;



        • fib(1) — вернул 0;




      • вернулись к fib(3) и посчитали fib(2) + fib(1) = 1 + 0 = 1. То есть fib(3)=1.




    • вернулись к fib(4) и посчитали fib(3) + fib(2) = 1 + 1 = 2. То есть fib(4)=2.




  2. вернулись к fib(5) и посчитали fib(4) + fib(3) = 2 + 1 = 3. То есть fib(5)=3.




Рекурсия всегда выглядит как пробежка по дереву, вначале вглубь, затем обратно.




Но помните, чем больше вложенность, тем медленнее работают рекурсивные функции. Например, если захотите вычислить 40-ое число Фибоначчи (print(fib(40))) — то придётся немного подождать. А если 45-ое — то долго подождать.




Здесь можете посмотреть видео по рекурсии.









Итог




Если вы дошли до конца этой, довольно длинной статьи, то должны были узнать про функции и оператор def, про возвращаемые значения и оператор return. Также мы разобрали области видимости и вложенные функции, и узнали про операторы nonlocal и global. Познакомились с аргументами функции, узнали что они бывают именованные и не именнованые, узнали про аргументы *args и **kwargs. Познакомились с маленькими lambda функциями. А также узнали про замыкания, декораторы и рекурсию.




Другие статьи по Python доступны здесь.



2023-05-11T10:16:09
Python