Примеры реализации функций. От различных умений и действий в играх, до добавления товара в корзину – весь этот код реализуется функциями. Наличие их – еще одно отличие низкоуровневых ЯП от высокоуровевых.
В главе 4. Циклы мы описывали несложный алгоритм официанта, который обходит все столы и собирает грязные тарелки со столов.
Между тем, если бы мы решились описывать полную работу официанта, как это было в главе 0 с примером робота, у нас бы появилось множество подзадач, которые нужно было бы выполнить официанту (или роботу, как вам больше нравится).
По сути, в примере про робота мы описывали функции этого робота: небольшие программы, которые робот может выполнять в зависимости от условий в разные моменты времени.
В мире программирования возможность повторной реализации кода крайне важна, ведь если не объединять повторяющиеся участки кода в блоки кода, то поддержка программ и дальнейшее их развитие будет крайне усложнена
Функция – это именованный фрагмент кода, который можно повторно вызывать множество раз по ходу программы. Они полезны при разработке небольших программ и просто необходимы при разработке серьезных проектов: от приложений для ПК до серверов сайтов.
Использование функций — это один из ключевых принципов написания эффективных программ. Вместо того чтобы описывать каждое действие в программе отдельно, как в линейном подходе, мы группируем связанные шаги в логические блоки.
Чтобы лучше понять, как это работает, давайте разберем на примере.
Когда роботу нужно выполнить одни и те же действия несколько раз — например, обслужить несколько посетителей — линейный код становится громоздким и трудным для изменения.
Если добавить новое действие или изменить существующее, придется вносить правки во многих местах программы.
– Чем-то напоминает то, как мы используем переменные. Чтобы не повторять числа или данные, запоминаем их под каким-то именем.
– Всё верно! Только в этот раз мы запоминаем целый участок кода.
В функциональном же подходе достаточно изменить одну функцию, и это изменение сразу отразится во всех местах, где она используется.
Так, на примере, мы создали сразу несколько функций, которые делают подзадачи для робота. После этого код сразу становится более структурированным и читаемым.
Такой метод разработки еще называют "разделяй и влавствуй". Мы разбиваем сложную задачу на более мелкие и автономные подзадачи до тех пор, пока каждая из подзадач не станет максимально автономной и переиспользуемой.
В начале главы на гифках были представлены два примера:
Добавление товара в корзину
Выполнение боевых действий в игре
Ваша задача - попробовать сначала линейно, а потом функционально попробовать представить работу программ в одном из этих случаях. Опишите действия словесно, как в примерах выше.
Подсказка: начать добавление товара можно с того, что вы откроете страницу товара, а действия в игре - что подойдете к противнику.
Итак, мы разобрались с тем, зачем функции вообще нужны. Теперь научимся их писать на python.
Сразу обозначу, что возможностей у функций очень много: от простого вызова до различных декораторов. Поэтому в этой главе мы лишь разберем основы, но крайне подробно, потому что дальше на функциях буту строиться вообще все программы, которые вы будете писать.
Как и любой другой блок в программе, у функций есть ключевое слово для ее начала. За это отвечает оператор def, сообщающий программе, что дальше будет функция.
Однако синтаксис здесь достаточно сильно меняется.
Функции – это именованные участки кода, а значит, чтобы использовать функцию, нужно указать её имя.
Функции могут принимать в себя аргументы (об этом дальше), указываемые в скобках после имени
Обратите внимание: определение функции само по себе эту функцию не выполняет эту функцию. Именно вызов функции запускает выполнение. При этом основная программа приостанавливается до момента завершения функции.
Яркий тому пример – функция input. Вы наверняка к этому моменту заметили, что до тех пор, пока пользователь не введет анные, программа не будет выполняться дальше. С пользовательскими функциями точно также.
Создайте файл users_functions.py
Напишите несколько пользовательских функций:
Функция make_coffee — готовит кофе.
Функция должна запрашивать, какой кофе хочет пользователь.
Потом, выводить текст: “Кофе готовится… Пожалуйста, подождите!”
и через некоторое время — “Кофе <вид кофе> готов!”. Используйте метод time.sleep(). Будет здорово, если каждый кофе будет готовиться разное количество времени.
Функция wash_dishes — моет посуду.
У пользователя нужно спросить, есть ли грязная посуда
Если да, функция должна выводить сообщение: “Начинаю мыть посуду…”. После этого — “Посуду вымыл!”.
Если нет, то вывести текст "Посуду мыть не нужно"
Функция preheat_oven — разогревает духовку. Придумайте собственный алгоритм того, как работает духовка. Возможно, вы хотите, чтобы пользователь указывал температурный режим, прогрев и общее время работы, а можнт просто функция вкл-выкл духовки. На ваше усмотрение.
В основном участке кода вызовите поочередно все функции.
Сами по себе функции, конечно, уже заметно начинают упрощать разработку программ. Но самое важное начинает появляться, когда мы начинаем работать с параметрами функций.
Параметры – это переменные, которые могут принимать значение только в момент вызова функции. Параметры могут использоваться только внутри функции.
Аргументы – это значения, которые передаются внутрь функции и присваиваются параметрам.
Параметры необходимы, если вы хотите создать такую функцию, которая меняет свой результат выполнения в зависимости от принимаемых данных.
При этом аргументы присваиваются по порядку. Так, в примере, значение 2 присвоится параметру-переменной a, а значение 10 присвоится b
Обратите внимание: вызов функции без указания аргументов в этом случае приведет к ошибке. Но это можно предотвратить, если указать значение параметра по умолчанию.
Создайте файл battle.py
Представьте, что в игре есть персонажи с определенными характеристиками: уровень атаки, уровень защиты и здоровье. Вам нужно написать функцию, которая будет рассчитывать урон, наносимый одному персонажу другим, и выводить результат атаки в консоль.
Функция называется attack.
Она принимает три позиционных параметра:
attack_power — сила атаки атакующего персонажа (целое число).
defense_level — уровень защиты защищающегося персонажа (целое число).
health — текущее здоровье защищающегося персонажа (целое число).
Функция должна рассчитать итоговый урон по следующей формуле:
Урон = сила атаки - уровень защиты. Если урон меньше нуля, он приравнивается к нулю.
Затем урон вычитается из здоровья защищающегося персонажа, и результат выводится в консоль:
Если здоровье уходит в минус, считается, что здоровье стало нулевым.
Функция выводит результат боя в консоль.
В случае, если вам требуется присвоить какое-то значение по умолчанию, чтобы, например, не всегда передавать аргументы для функции, а была возможность использовать какие-то стандартные значения, можно указать это значение через знак равенства, в момент определения параметров.
Также, вы можете указывать, какие конкретно параметры вы хотите использовать. Это работает в любом случае, даже если значения по умолчанию у вас нет.
Обратите внимание: если вы указываете и обязательные параметры (без значений по умолчанию) и не обязательные, параметры БЕЗ значений всегда должны идти первыми. И в вызове функции аргументы, которые будут присвоены этим параметрам, должны идти первыми, в указаннов в функции порядке.
Однако, вы можете поменять порядок аргументов, если напрямую укажете, каким параметрам какие аргументы вы присваиваете
Создайте файл battle_2.py. Скопируйте код из предыдущего файла, чтобы модифицировать его.
В этот раз расширим задачу, добавив параметры по умолчанию. Это позволит вызывать функцию attack как с указанными значениями для всех параметров, так и с использованием некоторых параметров по умолчанию.
Теперь наша функция attack будет иметь значения по умолчанию для параметров:
defense_level будет по умолчанию равен 5 (базовый уровень защиты).
health будет по умолчанию равен 50 (начальное здоровье персонажа).
Таким образом, вы можете передавать только attack_power для вызова функции, а значения защиты и здоровья будут использоваться по умолчанию, если не заданы.
При изучении функций вы неизбежно сталкиваетесь с двумя важными понятиями: lifetime (время жизни) и scope (область видимости).
Lifetime – определяет промежуток времени, в течение которого переменная существует в памяти — от момента её создания до уничтожения. В некоторых языках, где возможен прямой доступ к управлению памятью (например, C++), программист обязан контролировать создание и удаление переменных вручную. Однако в Python за это отвечает сборщик мусора (garbage collector), что снимает необходимость в явном управлении памятью. Но если память не освобождается, это может привести к утечкам.
Типичный пример такой ситуации – не закрыли текстовый файл, после завершения работы с ним.
Scope, в свою очередь, определяет области программы, в которых можно использовать переменные. Это ограничение позволяет управлять доступом к данным и предотвращать случайные ошибки, такие как изменение переменных в недопустимых местах программы.
В Python существует две основные области видимости: глобальная (global, для основного тела программы) и локальная (local, для функций и классов).
Переменные, объявленные внутри функции, находятся в локальной области видимости и недоступны за её пределами. Напротив, глобальные переменные, объявленные в основном теле программы, видны внутри локальных областей. Однако их нельзя изменять внутри функций, если у переменной - неизменяемый тип данных (например: int, str, float, bool)
Также существует охватывающая (enclosing, когда внутри функции вызывается другая функция). Такой тип видимости называют замыканием. В этой главе рассматривать этот тип не будем.
Обратите внимание на пример. Внутри функции и в основном теле программы создается переменная с одинаковым именем. однако, переменная в глобальном scope не будет изменена функцией, даже если внутри функции используется то же самое имя.
def foo():
x += 1 # !!! так делать нельзя
print(x + 1)
x = 10
А такая запись вызовет ошибку, потому что тут идет попытка изменить переменную из global scope в local scope. При этом достаточно убрать некорректную строчку - и все заработает.
– А зачем вообще делать одинаковые переменные внутри функции и снаружи? Не проще ли сразу разными именами называть?
– В целом, проще. Но также исопльзовать одинаковые в тех ситуациях, когда переменные по смыслу хранят схожие данные. Например, если вы делаете корзину товаров, у тебя может быть общая переменная total в основной программе и total для подсчета стоимости нескольких товаров одного вида. Смысл очень похож, разница в том, где total вычисляется.
Хотя в программах лучше изолировать логику функций от глобального кода, иногда возникают ситуации, когда требуется изменить глобальную переменную из функции. Для этого используется оператор global, который позволяет указать переменные, к которым необходим доступ.
Однако, старайтесь избегать частого использования global. Функции предназначены для разделения кода и создания более чистой и управляемой логики. Чрезмерное обращение к глобальным переменным может привести к непредвиденным ошибкам, так как изменяемые данные становятся доступными в разных частях программы, что усложняет отладку и может нарушить работу других частей кода.
Так как списки являются объектов, содержащим ссылки на другие данные, и вместе с тем изменяемым типом, существует несколько особенностей при работе функций со списками.
Когда вы передаёте список в функцию, вы передаёте ссылку на этот список, а не его копию. Это значит, что изменения, внесённые в список внутри функции, будут видны и за её пределами. То есть список будет изменён глобально, даже если внутри функции не используется global.
В примере, функция modify_list добавляет элемент 4 в список, и это изменение сохраняется за пределами функции, потому что сам объект списка передаётся по ссылке.
– То есть, чтобы изменять список, мне даже global не нужен?
– Ага. Интересно, что незнание факта управления списками, можно допустить довольно много ошибок, так что надо быть внимательным. Лучше работать с копией списка внутри функции и возвращать копию, но об этом дальше.
Если внутри функции нужно использовать локальную копию списка, но при этом не затрагивать оригинальный список, его следует явно скопировать. Это создаст новый объект, область видимости которого ограничена функцией.
Создайте файл shop_cart.py
Вам нужно создать простую систему управления корзиной в интернет-магазине. Корзина хранится в глобальной переменной cart, и вам нужно написать несколько функций для добавления и удаления товаров из этой корзины. Использовать return нельзя, все изменения должны происходить через глобальный список cart.
Глобальная переменная cart – изначально, пустой список.
Функция add_to_cart(item) – добавляет указанный товар item в глобальную переменную cart.
Функция remove_from_cart(item) – удаляет товар item из глобальной переменной cart, если он там есть.
Функция show_cart() – выводит все товары, находящиеся в корзине.
Вызовите эти функции подряд, с разными значениями. Либо, можете расширить программу и предлагать пользователю добавлять разные товары самостоятельно (по желанию)
Пример работы программы:
Товар 'Ноутбук' добавлен в корзину.
Товар 'Мышь' добавлен в корзину.
Ваша корзина содержит следующие товары:
- Ноутбук
- Мышь
Товар 'Ноутбук' удалён из корзины.
Ваша корзина содержит следующие товары:
- Мышь
Теперь давайте узнаем, как работает оператор возврата данных из функции.
До этого вы писали функции, которые ничего не возвращают: эти функции делали что-то, но их результат нельзя было записать, например, в переменную. Однако, часто возникает ситуация, когда необходимо записать результат функции в какую-то переменную. И для этих ситуация существует return
Когда функция в Python выполняет инструкцию return, она завершает своё выполнение и "возвращает" значение, указанное после return, вызывающему коду. Давайте разберёмся, как это работает пошагово.
Когда Python доходит до инструкции return внутри функции, он сразу же останавливает выполнение этой функции. Все инструкции после return не будут выполняться. Это означает, что return сигнализирует интерпретатору о том, что функция "закончила свою работу".
Чем-то такое поведение похоже на break внутри циклов.
После return можно указать значение, которое мы хотим вернуть из функции. Это может быть любое значение: число, строка, список, объект и даже другая функция! Значение, указанное после return, становится результатом вызова функции.
В примере, когда вы вызываете add(2, 19), интерпретатор доходит до return a + b, вычисляет a + b как 21 и возвращает это значение вызову add. Теперь вызов add(2, 19) можно использовать как значение 7.
Если в функции нет инструкции return с указанием значения, по умолчанию функция вернёт специальное значение None. Это может произойти в двух случаях:
Если return вообще не указан в функции — Python автоматически возвращает None, как результат вызова этой функции.
Если return указан без значения, то Python также возвращает None. Это полезно, когда нужно досрочно завершить выполнение функции, не возвращая конкретного значения.
В Python функция может вернуть сразу несколько значений, разделённых запятой. Когда вы это делаете, Python фактически создаёт кортеж из возвращаемых значений. Этот кортеж можно легко "распаковать" в отдельные переменные, что делает код более читабельным и удобным.
Также можно просто вернуть кортеж как единое целое, не распаковывая его. Тогда результат вызова функции будет выглядеть как кортеж.
Создайте файл calculate.py
Напишите функцию calculate_area_and_perimeter(length, width), которая принимает длину и ширину прямоугольника и возвращает два значения:
Площадь прямоугольника.
Периметр прямоугольника.
Потренируйтесь возвращать сразу два значения из функции и распаковывать их в отдельные переменные.