11  Функции.

11.1 Определение

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

  • определить (define);

  • вызвать (call).

Причем, прежде чем вызывать функцию, её нужно обязательно определить, иначе интерпретатор не сможет найти необходимые инструкции. Синтаксис определения представлен в Listing 11.1

Listing 11.1: Определение функции
def function_name(param_1, param_2):
    pass
  1. Определение функции начинается с ключевого слова def. Такую строку называют ещё сигнатурой функции. Затем после пробела идет имя функции, служащий идентификатором, по которому интерпретатор отличает функции друг от друга, так же как и переменные. Затем в круглых скобках через запятую перечисляются имена параметров - входных данных, внешних переменных, от которых зависит поведение функции. Параметров может быть от 0 до бесконечности. Количество параметров задается программистом. Заканчивается сигнатура двоеточием.
  2. С новой строки и дополнительного отступа начинается тело функции. Тело функции заканчивается тогда, когда вы начнете писать код на том же уровне отступа, что и сигнатуру функции (ключевое слово def). Функция может быть сколько угодно большой, но настоятельно рекомендую так не укрупнять. Функция должна быть компактной и гибкой. Для определения пустых функции, которым вы не придумали функционал, но которые должны быть определены, можно использовать ключевое слово pass.

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

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

Задача
  • Техническо задание (ТЗ): найти минимальное значение в списке чисел.

  • Входные данные: список из чисел

  • Выходные данные: значение из списка, являющиеся минимальным

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

  • Внештатные ситуации:

    • Входной параметр не является списком

    • Входной параметр является пустым списком

    • Входной параметр содержит не только числовые данные.

1from numbers import Number

2def get_min(input_list: list) -> Number:
    """
    Функция для поиска минимального значения в массиве чисел   
    Parameters
    ----------
    input_list : list
        Стандартный список из чисел принадлежащих встроенным типам (int, float. complex) ненулевого размера
    
    Returns
    -------
    Number
        минимальное значение в списке
    
    Raises
    ------
    AssertionError
        Входной параметр не является списком
        Входной параметр является пустым списком    
        Входной параметр содержит не только числовые данные.
3    """

4    assert isinstance(input_list, list), "Входной параметр не является списком"
    assert len(input_list) > 0, "Входной параметр является пустым списком"

5    current_min = input_list[0]
    for i in input_list: 
6        assert isinstance(i, (int, float, complex)) and not isinstance(i, bool), "Входной параметр содержит не только числовые данные"
        if i < current_min: 
            current_min = i 

7    return current_min
1
Из модуля стандартной библиотеки импортируем специальный класс для обобщеного обозначения чисел. В данном примере он необходим только для аннотации типов
2
Сигнатура нашей функции. Кроме обычного содержимого, здесь присутствует так называемая аннотация типов. Аннотация типов (type hints) это специальная конструкция для обозначения типов параметров, которые функция ожидает на вход и типов возвращаемых данных. Это вовсе неозначает, что действительно будут применены переменные именно с этими типами данных! Интерпретатор не отслеживает аннотации типов. Они служат только для документации.
3
Документационная строка (docstring) - описание работы функции, идет после сигнатуры функции в виде многострочной строки. В данном примере выполнена в стиле numpy. Подобные комментарии содержат информацию о работе функции, параметрах и их типах, о возвращаемых значениях и о внештатных ситуациях (о выбрасываемых исключениях, про которые речь пойдет в следующей главе). Такие строки могут собираться в специальные сайты-документации с помощью специальных программ, например, Sphinx.
4
Проверки на корректность входных данных. Оператор assert принимает на вход условие и в случае его не соблюдения генерирует ошибку AssertionError с сообщением, которое можно передать через запятую после условия.
5
Основное тело функции в соответствии с оговоренной выше схемой алгоритма.
6
Проверка каждого элемента на корректность.
7
Возвращаем значение в внешнюю среду. Это наш конечный результат. Оператор return прерывает выполнение функции. Его может не быть в функции, но если оно присутствует, то после ключего слова return должно идти некоторое значение.

Давайте проверим работу нашей функции.

example_list = [5,-4,4556,123,44,-5,0,324]
1min_value = get_min(example_list)
print(min_value)
1
Вызов функции. Возвращаемое значение присвоится переменной min_value. Вызов функции оформляется как написание имени функции и перечислением фактических значений параметров в круглых скобках.
-5

Проверим работу проверок

  1. Входной параметр не является списком
example_list = "meow"
min_value = get_min(example_list)
print(min_value)
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[3], line 2
      1 example_list = "meow"
----> 2 min_value = get_min(example_list)
      3 print(min_value)

Cell In[1], line 24, in get_min(input_list)
      3 def get_min(input_list: list) -> Number: # <2>
      4     """
      5     Функция для поиска минимального значения в массиве чисел   
      6     Parameters
   (...)
     21         Входной параметр содержит не только числовые данные.
     22     """ # <3>
---> 24     assert isinstance(input_list, list), "Входной параметр не является списком" # <4>
     25     assert len(input_list) > 0, "Входной параметр является пустым списком"
     27     current_min = input_list[0] # <5>

AssertionError: Входной параметр не является списком
  1. Входной параметр является пустым списком
example_list = []
min_value = get_min(example_list)
print(min_value)
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[4], line 2
      1 example_list = []
----> 2 min_value = get_min(example_list)
      3 print(min_value)

Cell In[1], line 25, in get_min(input_list)
      4 """
      5 Функция для поиска минимального значения в массиве чисел   
      6 Parameters
   (...)
     21     Входной параметр содержит не только числовые данные.
     22 """ # <3>
     24 assert isinstance(input_list, list), "Входной параметр не является списком" # <4>
---> 25 assert len(input_list) > 0, "Входной параметр является пустым списком"
     27 current_min = input_list[0] # <5>
     28 for i in input_list: 

AssertionError: Входной параметр является пустым списком
  1. Входной параметр содержит не только числовые данные.
example_list = [5,-4,"4556",123,44,-5,0,324]
min_value = get_min(example_list)assert
print(min_value)
  Cell In[5], line 2
    min_value = get_min(example_list)assert
                                     ^
SyntaxError: invalid syntax

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

print(get_min.__doc__)

    Функция для поиска минимального значения в массиве чисел   
    Parameters
    ----------
    input_list : list
        Стандартный список из чисел принадлежащих встроенным типам (int, float. complex) ненулевого размера
    
    Returns
    -------
    Number
        минимальное значение в списке
    
    Raises
    ------
    AssertionError
        Входной параметр не является списком
        Входной параметр является пустым списком    
        Входной параметр содержит не только числовые данные.
    

Документация должна дополнять рассказ о том, что делает код, а не делать это вместо кода!

11.2 Аргументы функции

Функции могут содержать аргументы двух типов:

  • Позиционные

  • Аргументы-ключевые слова

У позиционных аргументов значения копируются в соответствующие параметры согласно порядку следования.

def example(employee, animal, food):
    print(f"Работник = {employee}, Животное = {animal}, Еда = {food}")

example("Кот","Сухой корм","Александр")
Работник = Кот, Животное = Сухой корм, Еда = Александр

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

def example(employee, animal, food):
    print(f"Работник = {employee}, Животное = {animal}, Еда = {food}")

example(animal = "Кот",food = "Сухой корм",employee = "Александр")
Работник = Александр, Животное = Кот, Еда = Сухой корм

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

def example(employee, animal, food = "Cухой корм"):
    print(f"Работник = {employee}, Животное = {animal}, Еда = {food}")

example("Александр", animal = "Кот")
Работник = Александр, Животное = Кот, Еда = Cухой корм
Important

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

Аршументов может быть сколь угодное количество, чтобы не прописывать их всех существуют специальные аргументы args (для позиционных аргументов) и kwargs (для аргументов - ключевых слов). args - содержит кортеж передаваемых значений, а kwargs - словарь, в котором ключи - имена аргументов. Проиллюстрирую использование примерами из книги Любановича.

def print_more(required1, required2, *args):
    print('Need this one:', required1)
    print('Need this one too:', required2)
    print('All the rest:', args)
    
print_more('cap', 'gloves', 'scarf', 'monocle', 'mustache wax')
Need this one: cap
Need this one too: gloves
All the rest: ('scarf', 'monocle', 'mustache wax')
def print_kwargs(**kwargs):
    print('Keyword arguments:', kwargs)

print_kwargs(wine='merlot', entree='mutton', dessert='macaroon')
Keyword arguments: {'wine': 'merlot', 'entree': 'mutton', 'dessert': 'macaroon'}

11.3 Особые случаи

11.3.1 Рекурсивные функции

Функция может сожержать внутри себя вызовы других функций. Следствием из этого является, что функция может вызывать себя внутри своего тела. Такое решение может быть удобно при генерации последовательностей, т.е. когда следующий результат циклического алгоритма зависит от предыдущего. При этом в памяти формируется такая конструкция как стек вызовов. Стек это такая структура, которая работает по приципу LIFO (Last In First Out). Т.е результат будет считаться с последнего вызова к самому первому. Если тяжело представить стек, то представьте такую детскую игрушку, как Ханойская башня. Классический пример рекурсивной функции - расчет факториала.

1def factorial(n: int) -> int:
2    assert n >= 0, "Факториал не определен"
3    if n == 0 or n == 1:
        return 1
    else: 
4        return n * factorial(n-1)

print(factorial(6))
1
Сигнатура нашей функции: ожидаем на вход целое число, вернем целое.
2
Проверка на валидность аргумента. Факториал определен только для положительных целых чисел.
3
Базовый нерекурсивный случай, к которому должен прийти рекурсивный случай через стек и на котором рекурсия прервется
4
Рекурсивный случай. Вызываем саму функцию с уменьшенным аргументом, чтобы функция в итоге пришла к нерекурсивному случаю.
720
Важные моменты
  1. Рекурсивная функция обязательно должна содержать базовый, нерекурсивный случай и рекурсивная функция должна к нему прийти за умеренное количество шагов, иначе произойдет переполнения стека (StackOverflowError) - специальной области оперативной памяти.
  2. Любую рекурсивную функцию можно реализовать через нерекурсивную. Она может быстрее работать, но её написать буджет сложнее и она будет менее читаемая, чем рекурсивная.

Нерекурсивная реализация факториала

def factorial(n: int) -> int: 
    assert n >= 0, "Факториал не определен"  
    f = 1
    for i in range(1,n+1):
        f *= i
    return f

print(factorial(6))
720

11.3.2 Анонимные функции

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

example_list = list(range(1,10))
square = list(map(lambda x: x**2, example_list))
print(square)
[1, 4, 9, 16, 25, 36, 49, 64, 81]

В данном случае с помощью функции map мы применили к каждому элементу анонимную функцию, принимающую один параметр и возводящую его в квадрат. Синтаксис lambda выражений очень простой:

lambda param_name: action_with_parametr

11.3.3 Генераторы

В Python генератор — это объект, который предназначен для создания последовательностей. С его помощью вы можете проитерировать потенциально огромные последовательности без необходимости создания и сохранения всей последовательности в память сразу. Генераторы часто становятся источником данных для итераторов. Как вы помните, мы уже использовали один из них, range(). Каждый раз, когда вы итерируете через генератор, он отслеживает, где он находился во время последнего вызова, и возвращает следующее значение. Это отличает его от обычной функции, которая не помнит о предыдущих вызовах и всегда начинает работу с первой строки и в неизменном состоянии. Если вы хотите создать потенциально большую последовательность и ее код слишком велик для того, чтобы создать включение генератора, напишите функцию генератора. Это обычная функция, но она возвращает значение с помощью выражения yield, а не return. Напишем собственную функцию range():

def my_range(first=0, last=10, step=1):
    number = first
    while number < last:
        yield number
        number += step

for i in my_range(0,10,2):
    print(i)
0
2
4
6
8

11.3.4 Внутренние функции

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

def outer(a, b):
    def inner(c, d):
        return c + d
    return inner(a, b)

print(outer(5,7))
12

11.3.5 Замыкания (closure)

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

Для создания замыкания в Python нам понадобятся три компонента:

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

  2. Локальные переменные внешней функции: Это переменные, определенные внутри внешней функции. Python сохраняет эти переменные, позволяя использовать их в замыкании, даже после того, как внешняя функция завершила свою работу.

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

1def outer_func():
2    name = "username"
3    def inner_func():
        print(f"Hello, {name}!")
    return inner_func

print(outer_func()) # .inner_func at ...>

greeter = outer_func()

greeter() # Hello, username!
1
Внешняя функция
2
Локальные переменные внешней функции
3
Вложенная (внутренняя) функция
<function outer_func.<locals>.inner_func at 0x710e623e4310>
Hello, username!

11.3.6 Декораторы

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

1def decorator_function(func):
    def wrapper():
        print('Функция-обёртка!')
        print('Оборачиваемая функция: {}'.format(func))
        print('Выполняем обёрнутую функцию...')
        func()
        print('Выходим из обёртки')
    return wrapper

2@decorator_function
def hello_world():
    print('Hello world!')

hello_world()
1
Определение декоратора
2
Применение декоратора к функции
Функция-обёртка!
Оборачиваемая функция: <function hello_world at 0x710e623e4430>
Выполняем обёрнутую функцию...
Hello world!
Выходим из обёртки

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

Замыкания

  • Фабричные функции

  • Функции обратного вызова

  • Генерация иерархических конструкций

  • Мемоизация

  • Метод инкапсуляции реализации

Декораторы

  • расширение функционала функции

    • журналирование

    • обеспечение контроля доступа и аутентификации,

    • инструментарий и функции управления временем,

    • ограничение скорости,

    • кэширование и многое другое

11.4 Подведение итогов

  1. Функции - мощный инструмент организации кода программы
  2. Функции должны быть документированы. Хорошо написанный код может быть сам по себе хорошей документацией, но также следует применять документационные строки и аннотации типов.
  3. Функции могут получать от 0 до бесконечного числа параметров и возвращать значения.
  4. Функции могут генерировать последовательности, вызывать сами себя, и быть определены внутри других функций.

11.5 Упражнения

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

  1. Напишите функцию, которая будет возвращать индекс минимального элемента в списке. Если их несколько, то вернет все минимальные положения.
  2. Напишите функцию, которая возвращает отсортированный методом пузырька список. Для продвинутых: попробуйте написать декоратор, который будет отслеживать выполнение этой функции.
  3. Напишите функцию-генератор случайной нуклеотидной последовательности заданной пользователем длины.
  4. Реализуйте в виде функции игру “Угадай число”. Пользователю дается 10 попыток угадать заданное случайное число в диапазоне от 1 до 100. При каждой попытке программа должна оповещать игрока, загаданное число больше, меньше или равно введенного.

Дополнительные упражнения

https://rosalind.info/problems/list-view/?location=algorithmic-heights

  • Fibbonacci Numbers

  • Degree Array

  • Insertion Sort

https://rosalind.info/problems/list-view/

  • Counting DNA Nucleotides

  • Transcribing DNA into RNA

  • Complementing a Strand of DNA

  • Rabbits and Recurrence Relations