До этого мы решали простые задачи. Для их решения достаточно небольшого количесвта кода в 15-20 строчек максимум. Сколько-нибудь сложная, прикладная программа состоит из множества действий, которые необходимо повторять раз за разом, причем в совершенно разных участках кода. Набирать их раз за разом муторно. Если нужно что-то поменять, то менять придется везде. Код превращается в нечитаемую вермешель из дублирующихся фрагментов текста. Поэтому была придумана синтаксическая конструкция, призванная заменять повторяющееся действие одной строчкой. Это функции - фрагмент программного кода, к которому можно обратиться из другого места программы. С функцией можно сделать две вещи:
определить (define);
вызвать (call).
Причем, прежде чем вызывать функцию, её нужно обязательно определить, иначе интерпретатор не сможет найти необходимые инструкции. Синтаксис определения представлен в Listing 11.1
Listing 11.1: Определение функции
def function_name(param_1, param_2):pass
Определение функции начинается с ключевого слова def. Такую строку называют ещё сигнатурой функции. Затем после пробела идет имя функции, служащий идентификатором, по которому интерпретатор отличает функции друг от друга, так же как и переменные. Затем в круглых скобках через запятую перечисляются имена параметров - входных данных, внешних переменных, от которых зависит поведение функции. Параметров может быть от 0 до бесконечности. Количество параметров задается программистом. Заканчивается сигнатура двоеточием.
С новой строки и дополнительного отступа начинается тело функции. Тело функции заканчивается тогда, когда вы начнете писать код на том же уровне отступа, что и сигнатуру функции (ключевое слово def). Функция может быть сколько угодно большой, но настоятельно рекомендую так не укрупнять. Функция должна быть компактной и гибкой. Для определения пустых функции, которым вы не придумали функционал, но которые должны быть определены, можно использовать ключевое слово pass.
Таким образом, функция это некоторое шаблонное действие, подалгоритм, который должен работать с любыми предназначенными для него входными данными.
Кроме приема значений на вход, функция может возвращать значения. например, наша задача найти минимальное значение в списке. Давайте сконструируем решение. Хорошее решение должно состоять из описания входных и выходных данных, описания алгоритма, проверки и документирования внештатных ситуаций. На следующем примере покажу лучшие практики написания и документирования кода.
Задача
Техническо задание (ТЗ): найти минимальное значение в списке чисел.
Входные данные: список из чисел
Выходные данные: значение из списка, являющиеся минимальным
Решение: предположим, что минимальным элементом в списке является нулевой элемент. Тогда мы сравним каждый элемент списка с предполаемым минимальным значением. Если мы обнаружим значение меньше предполагаемого, то тогда заменим его на найденное. После всех сравнений вернем итоговое значение. В графическом представлении алгоритм, можно представить следующим образом:
Внештатные ситуации:
Входной параметр не является списком
Входной параметр является пустым списком
Входной параметр содержит не только числовые данные.
1from numbers import Number2def get_min(input_list: list) -> Number:""" Функция для поиска минимального значения в массиве чисел Parameters ---------- input_list : list Стандартный список из чисел принадлежащих встроенным типам (int, float. complex) ненулевого размера Returns ------- Number минимальное значение в списке Raises ------ AssertionError Входной параметр не является списком Входной параметр является пустым списком Входной параметр содержит не только числовые данные.3 """4assertisinstance(input_list, list), "Входной параметр не является списком"assertlen(input_list) >0, "Входной параметр является пустым списком"5 current_min = input_list[0]for i in input_list: 6assertisinstance(i, (int, float, complex)) andnotisinstance(i, bool), "Входной параметр содержит не только числовые данные"if i < current_min: current_min = i 7return current_min
1
Из модуля стандартной библиотеки импортируем специальный класс для обобщеного обозначения чисел. В данном примере он необходим только для аннотации типов
2
Сигнатура нашей функции. Кроме обычного содержимого, здесь присутствует так называемая аннотация типов. Аннотация типов (type hints) это специальная конструкция для обозначения типов параметров, которые функция ожидает на вход и типов возвращаемых данных. Это вовсе неозначает, что действительно будут применены переменные именно с этими типами данных! Интерпретатор не отслеживает аннотации типов. Они служат только для документации.
3
Документационная строка (docstring) - описание работы функции, идет после сигнатуры функции в виде многострочной строки. В данном примере выполнена в стиле numpy. Подобные комментарии содержат информацию о работе функции, параметрах и их типах, о возвращаемых значениях и о внештатных ситуациях (о выбрасываемых исключениях, про которые речь пойдет в следующей главе). Такие строки могут собираться в специальные сайты-документации с помощью специальных программ, например, Sphinx.
4
Проверки на корректность входных данных. Оператор assert принимает на вход условие и в случае его не соблюдения генерирует ошибку AssertionError с сообщением, которое можно передать через запятую после условия.
5
Основное тело функции в соответствии с оговоренной выше схемой алгоритма.
6
Проверка каждого элемента на корректность.
7
Возвращаем значение в внешнюю среду. Это наш конечный результат. Оператор return прерывает выполнение функции. Его может не быть в функции, но если оно присутствует, то после ключего слова return должно идти некоторое значение.
Вызов функции. Возвращаемое значение присвоится переменной min_value. Вызов функции оформляется как написание имени функции и перечислением фактических значений параметров в круглых скобках.
---------------------------------------------------------------------------AssertionError Traceback (most recent call last)
Cell In[3], line 2 1 example_list ="meow"----> 2 min_value =get_min(example_list) 3print(min_value)
Cell In[1], line 24, in get_min(input_list) 3defget_min(input_list: list) -> Number: # <2> 4""" 5 Функция для поиска минимального значения в массиве чисел 6 Parameters (...) 21 Входной параметр содержит не только числовые данные. 22 """# <3>---> 24assertisinstance(input_list, list), "Входной параметр не является списком"# <4> 25assertlen(input_list) >0, "Входной параметр является пустым списком" 27 current_min = input_list[0] # <5>AssertionError: Входной параметр не является списком
---------------------------------------------------------------------------AssertionError Traceback (most recent call last)
Cell In[4], line 2 1 example_list = []
----> 2 min_value =get_min(example_list) 3print(min_value)
Cell In[1], line 25, in get_min(input_list) 4""" 5Функция для поиска минимального значения в массиве чисел 6Parameters (...) 21 Входной параметр содержит не только числовые данные. 22"""# <3> 24assertisinstance(input_list, list), "Входной параметр не является списком"# <4>---> 25assertlen(input_list) >0, "Входной параметр является пустым списком" 27 current_min = input_list[0] # <5> 28for i in input_list:
AssertionError: Входной параметр является пустым списком
Входной параметр содержит не только числовые данные.
Таким образом, мы показали, что в первом приближении согласно ТЗ наша функция работает корректно. Если мы вдруг забудем, что это за функция, то она маленькая и читаемая. Все переменные имеют говорящие имена. Алгоритм крайне простой и мы всегда можем посмотреть документацию.
print(get_min.__doc__)
Функция для поиска минимального значения в массиве чисел
Parameters
----------
input_list : list
Стандартный список из чисел принадлежащих встроенным типам (int, float. complex) ненулевого размера
Returns
-------
Number
минимальное значение в списке
Raises
------
AssertionError
Входной параметр не является списком
Входной параметр является пустым списком
Входной параметр содержит не только числовые данные.
Документация должна дополнять рассказ о том, что делает код, а не делать это вместо кода!
11.2 Аргументы функции
Функции могут содержать аргументы двух типов:
Позиционные
Аргументы-ключевые слова
У позиционных аргументов значения копируются в соответствующие параметры согласно порядку следования.
Работник = Кот, Животное = Сухой корм, Еда = Александр
Чтобы избежать путаницы с позиционными аргументами, вы можете указать аргументы с помощью имен соответствующих параметров. Порядок следования аргументов в этом случае может быть иным
Если вы вызываете функцию, имеющую как позиционные аргументы, так и аргументы — ключевые слова, то позиционные аргументы необходимо указывать первыми. Вы можете указать значения по умолчанию для параметров. Значения по умолчанию используются в том случае, если вызывающая сторона не предоставила соответствующий аргумент.
Значение аргументов по умолчанию высчитывается, когда функция определяется, а не выполняется. Распространенной ошибкой новичков (и иногда не совсем новичков) является использование изменяемого типа данных вроде списка или словаря в качестве аргумента по умолчанию.
Аршументов может быть сколь угодное количество, чтобы не прописывать их всех существуют специальные аргументы 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')
Функция может сожержать внутри себя вызовы других функций. Следствием из этого является, что функция может вызывать себя внутри своего тела. Такое решение может быть удобно при генерации последовательностей, т.е. когда следующий результат циклического алгоритма зависит от предыдущего. При этом в памяти формируется такая конструкция как стек вызовов. Стек это такая структура, которая работает по приципу LIFO (Last In First Out). Т.е результат будет считаться с последнего вызова к самому первому. Если тяжело представить стек, то представьте такую детскую игрушку, как Ханойская башня. Классический пример рекурсивной функции - расчет факториала.
1def factorial(n: int) ->int:2assert n >=0, "Факториал не определен"3if n ==0or n ==1:return1else: 4return n * factorial(n-1)print(factorial(6))
1
Сигнатура нашей функции: ожидаем на вход целое число, вернем целое.
2
Проверка на валидность аргумента. Факториал определен только для положительных целых чисел.
3
Базовый нерекурсивный случай, к которому должен прийти рекурсивный случай через стек и на котором рекурсия прервется
4
Рекурсивный случай. Вызываем саму функцию с уменьшенным аргументом, чтобы функция в итоге пришла к нерекурсивному случаю.
720
Важные моменты
Рекурсивная функция обязательно должна содержать базовый, нерекурсивный случай и рекурсивная функция должна к нему прийти за умеренное количество шагов, иначе произойдет переполнения стека (StackOverflowError) - специальной области оперативной памяти.
Любую рекурсивную функцию можно реализовать через нерекурсивную. Она может быстрее работать, но её написать буджет сложнее и она будет менее читаемая, чем рекурсивная.
Нерекурсивная реализация факториала
def factorial(n: int) ->int: assert n >=0, "Факториал не определен" f =1for i inrange(1,n+1): f *= ireturn fprint(factorial(6))
720
11.3.2 Анонимные функции
В разных языках существует концепт анонимных функций, существующих для выполнения некоторого действия здесь и сейчас, когда определять отдельную функцию не очень разумно. В Python функционал анонимных функций выполняют лямбда-выражения. Это простые, однострочные функции, работающие как правило над некоторой итерируемой переменной. Например, у вас есть список чисел и вы хотите возвести его в квадрат. Подобную задачу можно решить стандартными средствами питона в одну строчку.
В данном случае с помощью функции 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 = firstwhile number < last:yield number number += stepfor 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 + dreturn inner(a, b)print(outer(5,7))
12
11.3.5 Замыкания (closure)
Замыкание (closure) представляет функцию, которая запоминает свое лексическое окружение даже в том случае, когда она выполняется вне своей области видимости.
Для создания замыкания в Python нам понадобятся три компонента:
Внешняя функция: Это функция, которая содержит другую функцию, называемую внутренней. Внешняя функция может принимать аргументы и определять переменные, к которым внутренняя функция может получить доступ и обновить..
Локальные переменные внешней функции: Это переменные, определенные внутри внешней функции. Python сохраняет эти переменные, позволяя использовать их в замыкании, даже после того, как внешняя функция завершила свою работу.
Вложенная функция: Это функция, определенная внутри внешней функции. Она может получать доступ и обновлять переменные из внешней функции, даже после того, как внешняя функция вернула значение.
1def outer_func():2 name ="username"3def inner_func():print(f"Hello, {name}!")return inner_funcprint(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 Декораторы
Иногда вам нужно модифицировать существующую функцию, не меняя при этом ее исходный код. Зачастую нужно добавить выражение для отладки, чтобы посмотреть, какие аргументы были туда переданы. Декоратор — это функция, которая принимает одну функцию в качестве аргумента и возвращает другую функцию.
Функция-обёртка!
Оборачиваемая функция: <function hello_world at 0x710e623e4430>
Выполняем обёрнутую функцию...
Hello world!
Выходим из обёртки
Тема замыканий и декораторов крайне сложна и оставим её на самостоятельное изучение, так как они выходят за рамки нашего пособия. Отметим, только области примения этих конструкций
Замыкания
Фабричные функции
Функции обратного вызова
Генерация иерархических конструкций
Мемоизация
Метод инкапсуляции реализации
Декораторы
расширение функционала функции
журналирование
обеспечение контроля доступа и аутентификации,
инструментарий и функции управления временем,
ограничение скорости,
кэширование и многое другое
11.4 Подведение итогов
Функции - мощный инструмент организации кода программы
Функции должны быть документированы. Хорошо написанный код может быть сам по себе хорошей документацией, но также следует применять документационные строки и аннотации типов.
Функции могут получать от 0 до бесконечного числа параметров и возвращать значения.
Функции могут генерировать последовательности, вызывать сами себя, и быть определены внутри других функций.
11.5 Упражнения
Для выполнения не используйте встроенные функции или функции из других библиотек. Постарайтесь реализовать сами алгоритмы, а готовые решения используйте для самопроверки.
Напишите функцию, которая будет возвращать индекс минимального элемента в списке. Если их несколько, то вернет все минимальные положения.
Напишите функцию, которая возвращает отсортированный методом пузырька список. Для продвинутых: попробуйте написать декоратор, который будет отслеживать выполнение этой функции.
Реализуйте в виде функции игру “Угадай число”. Пользователю дается 10 попыток угадать заданное случайное число в диапазоне от 1 до 100. При каждой попытке программа должна оповещать игрока, загаданное число больше, меньше или равно введенного.