Модуль 3: Функции
Функции — фундаментальный строительный блок в Python и основа модульного программирования. Они позволяют разбить код на логические части, повысить его читаемость и переиспользуемость.
В этом модуле вы изучите:
- Как создавать и вызывать функции
- Работу с параметрами и аргументами разных типов
- Использование компактных lambda-функций
- Применение рекурсии для решения сложных задач
Освоив функции, вы сможете писать более структурированный, поддерживаемый и элегантный код. Это необходимый навык для любого Python-разработчика, который станет основой для изучения более продвинутых концепций в будущих модулях.
3.1 Создание функций
Функции — это блоки кода, выполняющие определённую задачу. Они позволяют структурировать программу, делают код более читаемым и помогают избежать повторений. В Python функции определяются с помощью ключевого слова def
.
Синтаксис функций
def имя_функции(параметр1, параметр2, ...):
"""Документация (docstring)"""
# тело функции
# код, выполняющий нужные действия
return результат # необязательно
Основные компоненты функции
- Ключевое слово def — указывает на начало определения функции
- Имя функции — должно следовать соглашениям Python (snake_case)
- Параметры — переменные, которые получает функция (необязательны)
- Двоеточие : — обязательный символ, завершающий заголовок функции
- Docstring — строка документации (рекомендуется, но не обязательна)
- Тело функции — код с отступом, выполняющий действия функции
- Оператор return — возвращает значение из функции (если требуется)
Простой пример функции
def приветствие():
print("Привет, мир!")
# Вызов функции
приветствие() # Выведет: Привет, мир!
Функция с параметром
def персональное_приветствие(имя):
print(f"Привет, {имя}!")
# Вызов функции с аргументом
персональное_приветствие("Анна") # Выведет: Привет, Анна!
Функция, возвращающая значение
def сумма(a, b):
"""Функция складывает два числа и возвращает результат."""
return a + b
# Сохранение результата функции в переменной
результат = сумма(5, 3)
print(результат) # Выведет: 8
# Прямое использование возвращаемого значения
print(сумма(10, 20)) # Выведет: 30
Оператор return
Оператор return
имеет две важные функции:
- Возвращает значение из функции
- Немедленно завершает выполнение функции
def проверить_возраст(возраст):
if возраст < 18:
return "Доступ запрещён"
# Этот код выполнится только если возраст >= 18
return "Доступ разрешён"
Функция может возвращать несколько значений в виде кортежа:
def разделить_и_остаток(a, b):
"""Возвращает результат деления и остаток."""
частное = a // b
остаток = a % b
return частное, остаток
результат = разделить_и_остаток(20, 7)
print(результат) # Выведет: (2, 6)
# Распаковка кортежа
частное, остаток = разделить_и_остаток(20, 7)
print(f"Частное: {частное}, Остаток: {остаток}") # Выведет: Частное: 2, Остаток: 6
Советы по написанию функций
- Называйте функции понятными именами, отражающими их назначение
- Функция должна выполнять только одну конкретную задачу
- Старайтесь делать функции короткими (до 20-25 строк)
- Всегда добавляйте документацию (docstring) к важным функциям
- Функции без оператора
return
возвращаютNone
Области видимости (Scopes)
Каждая функция создаёт собственную область видимости для переменных:
# Глобальная переменная
x = 10
def функция():
# Локальная переменная
y = 5
print(f"Внутри функции: x = {x}, y = {y}")
функция() # Выведет: Внутри функции: x = 10, y = 5
print(f"Вне функции: x = {x}")
# print(y) # Ошибка! y не определена вне функции
Изменение глобальных переменных
Используйте ключевое слово global
для изменения глобальных переменных:
счётчик = 0
def увеличить_счётчик():
global счётчик
счётчик += 1
return счётчик
print(увеличить_счётчик()) # Выведет: 1
print(увеличить_счётчик()) # Выведет: 2
print(счётчик) # Выведет: 2
Когда использовать функции?
- Код повторяется несколько раз в программе
- Логически обособленные блоки кода
- Когда нужно абстрагировать сложную логику
- Для улучшения читаемости программы
- Для тестирования отдельных частей программы
3.2 Параметры и аргументы
Функции становятся по-настоящему мощными, когда они могут получать и обрабатывать данные. Для этого используются параметры и аргументы.
Важно понимать разницу:
- Параметры — переменные, указанные при определении функции
- Аргументы — значения, которые передаются функции при её вызове
Виды параметров
1. Обязательные параметры
def степень(основание, показатель):
return основание ** показатель
# Оба аргумента обязательны
результат = степень(2, 3) # 2³ = 8
2. Параметры по умолчанию
Параметры могут иметь значения по умолчанию, которые используются, если аргумент не передан:
def степень(основание, показатель=2):
"""Возводит основание в указанную степень. По умолчанию - квадрат."""
return основание ** показатель
# Используем значение по умолчанию для показателя (2)
квадрат = степень(4) # 4² = 16
# Указываем оба аргумента
куб = степень(4, 3) # 4³ = 64
Важные правила для параметров по умолчанию:
- Все параметры по умолчанию должны идти после обязательных
- Значения по умолчанию вычисляются только один раз при определении функции
# НЕПРАВИЛЬНО:
# def функция(параметр1=10, параметр2): # SyntaxError
# pass
# ПРАВИЛЬНО:
def функция(параметр1, параметр2=10):
pass
Будьте осторожны с изменяемыми значениями по умолчанию:
# Проблема: список создается один раз при определении функции
def добавить_элемент(элемент, список=[]):
список.append(элемент)
return список
print(добавить_элемент(1)) # [1]
print(добавить_элемент(2)) # [1, 2] - список сохраняется между вызовами!
# Правильный способ:
def добавить_элемент_правильно(элемент, список=None):
if список is None:
список = []
список.append(элемент)
return список
print(добавить_элемент_правильно(1)) # [1]
print(добавить_элемент_правильно(2)) # [2] - каждый раз новый список
3. Позиционные и именованные аргументы
Python позволяет передавать аргументы двумя способами:
def приветствие(имя, сообщение):
return f"{сообщение}, {имя}!"
# Позиционные аргументы (порядок важен)
print(приветствие("Иван", "Добрый день")) # "Добрый день, Иван!"
# Именованные аргументы (порядок не важен)
print(приветствие(сообщение="Привет", имя="Мария")) # "Привет, Мария!"
# Комбинирование (позиционные должны идти первыми)
print(приветствие("Алексей", сообщение="Здравствуйте")) # "Здравствуйте, Алексей!"
4. Произвольное количество аргументов (*args)
Для работы с переменным числом позиционных аргументов используется специальный параметр со звёздочкой:
def сумма_всех(*числа):
"""Суммирует произвольное количество чисел."""
результат = 0
for число in числа:
результат += число
return результат
print(сумма_всех(1, 2)) # 3
print(сумма_всех(1, 2, 3, 4, 5)) # 15
# *args собирает все позиционные аргументы в кортеж
def информация_о_пользователе(имя, *увлечения):
print(f"Имя: {имя}")
if увлечения:
print(f"Увлечения: {', '.join(увлечения)}")
else:
print("Увлечения не указаны")
информация_о_пользователе("Елена", "чтение", "танцы", "программирование")
# Выведет:
# Имя: Елена
# Увлечения: чтение, танцы, программирование
5. Произвольные именованные аргументы (**kwargs)
Для работы с произвольным числом именованных аргументов используется параметр с двумя звёздочками:
def профиль_пользователя(**данные):
"""Создаёт словарь с данными пользователя."""
print("Информация о пользователе:")
for ключ, значение in данные.items():
print(f" {ключ}: {значение}")
профиль_пользователя(имя="Дмитрий", возраст=28, город="Москва")
# Выведет:
# Информация о пользователе:
# имя: Дмитрий
# возраст: 28
# город: Москва
6. Комбинирование разных типов параметров
Python позволяет комбинировать все типы параметров, но в определённом порядке:
def универсальная_функция(
обязательный, # Обязательный параметр
со_значением_по_умолчанию=10, # Параметр со значением по умолчанию
*args, # Переменное число позиционных аргументов
**kwargs # Переменное число именованных аргументов
):
print(f"Обязательный: {обязательный}")
print(f"По умолчанию: {со_значением_по_умолчанию}")
print(f"*args: {args}")
print(f"**kwargs: {kwargs}")
универсальная_функция(
"Обязательное значение",
20,
1, 2, 3,
имя="Анна", город="Санкт-Петербург"
)
# Выведет:
# Обязательный: Обязательное значение
# По умолчанию: 20
# *args: (1, 2, 3)
# **kwargs: {'имя': 'Анна', 'город': 'Санкт-Петербург'}
7. Позиционные и именованные аргументы в Python 3.8+
В современных версиях Python можно более точно указывать, как должны передаваться аргументы:
# Только позиционные аргументы (до /)
# Только именованные аргументы (после *)
def функция(только_позиционный, /, позиционный_или_именованный, *, только_именованный):
print(f"Позиционный: {только_позиционный}")
print(f"Любой: {позиционный_или_именованный}")
print(f"Именованный: {только_именованный}")
# Правильный вызов:
функция(10, 20, только_именованный=30)
функция(10, позиционный_или_именованный=20, только_именованный=30)
# Ошибки:
# функция(только_позиционный=10, 20, только_именованный=30) # Нельзя первый аргумент по имени
# функция(10, 20, 30) # Нельзя последний аргумент по позиции
Полезные советы
- Используйте параметры по умолчанию для необязательных аргументов
- Избегайте изменяемых типов (список, словарь) в качестве значений по умолчанию
- Предпочитайте именованные аргументы для функций с множеством параметров
- Не злоупотребляйте *args и **kwargs — они снижают читаемость кода
- При работе с позиционными/именованными аргументами, добавьте подробную документацию
Распаковка аргументов
Python позволяет "распаковывать" последовательности и словари в аргументы функции:
def сумма(a, b, c):
return a + b + c
# Распаковка списка/кортежа в позиционные аргументы
значения = [1, 2, 3]
print(сумма(*значения)) # 6
# Распаковка словаря в именованные аргументы
значения_словарь = {'a': 10, 'b': 20, 'c': 30}
print(сумма(**значения_словарь)) # 60
3.3 Lambda-функции
Lambda-функции (анонимные функции) — это небольшие, одноразовые функции, которые можно создавать "на лету" без определения через def
. Они особенно полезны, когда нужна простая функция для короткой операции.
Синтаксис lambda-функций
lambda аргументы: выражение
Ключевые особенности lambda-функций:
- Содержат только одно выражение (не могут содержать блоки кода)
- Результат выражения автоматически возвращается
- Могут принимать любое количество аргументов
- Создаются в месте использования
Простые примеры
# Lambda-функция, возводящая число в квадрат
квадрат = lambda x: x ** 2
print(квадрат(5)) # 25
# Функция с несколькими аргументами
сумма = lambda a, b: a + b
print(сумма(3, 5)) # 8
# Аргументы по умолчанию
приветствие = lambda имя, сообщение="Привет": f"{сообщение}, {имя}!"
print(приветствие("Мария")) # "Привет, Мария!"
print(приветствие("Антон", "Здравствуйте")) # "Здравствуйте, Антон!"
Практическое применение lambda-функций
Lambda-функции чаще всего используются в комбинации с такими функциями, как sorted()
, filter()
, map()
и в функциональном программировании.
1. Сортировка с помощью lambda
# Список кортежей (имя, возраст)
пользователи = [
("Иван", 30),
("Мария", 25),
("Алексей", 35),
("Екатерина", 28)
]
# Сортировка по возрасту
по_возрасту = sorted(пользователи, key=lambda user: user[1])
print(по_возрасту)
# [('Мария', 25), ('Екатерина', 28), ('Иван', 30), ('Алексей', 35)]
# Сортировка по длине имени
по_длине_имени = sorted(пользователи, key=lambda user: len(user[0]))
print(по_длине_имени)
# [('Иван', 30), ('Мария', 25), ('Алексей', 35), ('Екатерина', 28)]
2. Фильтрация с помощью filter()
числа = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Фильтрация четных чисел
четные = list(filter(lambda x: x % 2 == 0, числа))
print(четные) # [2, 4, 6, 8, 10]
# Эквивалент с использованием генератора списка
четные_генератор = [x for x in числа if x % 2 == 0]
print(четные_генератор) # [2, 4, 6, 8, 10]
# Фильтрация словарей
товары = [
{"название": "Ноутбук", "цена": 70000},
{"название": "Смартфон", "цена": 30000},
{"название": "Наушники", "цена": 5000},
{"название": "Планшет", "цена": 45000}
]
дорогие_товары = list(filter(lambda товар: товар["цена"] > 40000, товары))
print(дорогие_товары)
# [{'название': 'Ноутбук', 'цена': 70000}, {'название': 'Планшет', 'цена': 45000}]
3. Преобразование с помощью map()
числа = [1, 2, 3, 4, 5]
# Возведение всех чисел в квадрат
квадраты = list(map(lambda x: x ** 2, числа))
print(квадраты) # [1, 4, 9, 16, 25]
# Эквивалент с использованием генератора списка
квадраты_генератор = [x ** 2 for x in числа]
print(квадраты_генератор) # [1, 4, 9, 16, 25]
# Преобразование списка словарей
товары = [
{"название": "Ноутбук", "цена": 70000},
{"название": "Смартфон", "цена": 30000},
{"название": "Наушники", "цена": 5000}
]
# Добавление скидки 10%
со_скидкой = list(map(
lambda товар: {
"название": товар["название"],
"цена": товар["цена"],
"цена_со_скидкой": товар["цена"] * 0.9
},
товары
))
print(со_скидкой)
# [{'название': 'Ноутбук', 'цена': 70000, 'цена_со_скидкой': 63000.0}, ...]
4. Комбинирование функций высшего порядка
числа = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Найти сумму квадратов четных чисел
результат = sum(map(lambda x: x ** 2, filter(lambda x: x % 2 == 0, числа)))
print(результат) # 220 (= 4 + 16 + 36 + 64 + 100)
# То же самое с генератором
результат_генератор = sum(x ** 2 for x in числа if x % 2 == 0)
print(результат_генератор) # 220
5. Использование в функциональном программировании
from functools import reduce
числа = [1, 2, 3, 4, 5]
# Произведение всех чисел с помощью reduce
произведение = reduce(lambda x, y: x * y, числа)
print(произведение) # 120 (= 1 * 2 * 3 * 4 * 5)
# Более сложный пример: нахождение максимального элемента
максимум = reduce(lambda x, y: x if x > y else y, числа)
print(максимум) # 5
Когда использовать lambda-функции
- Для простых операций, когда объявление полноценной функции избыточно
- При передаче функции как аргумента другой функции (например, в
sorted
,map
,filter
) - Для быстрой обработки данных "на лету"
Ограничения lambda-функций
- Могут содержать только одно выражение (нельзя использовать несколько строк кода)
- Не поддерживают операторы присваивания, условий, циклов внутри себя
- Отсутствие имени затрудняет отладку
- Сложные lambda могут снижать читаемость кода
Сравнение lambda и обычных функций
# Обычная функция
def квадрат_обычная(x):
return x ** 2
# Lambda-функция
квадрат_лямбда = lambda x: x ** 2
# Обе функции работают одинаково
print(квадрат_обычная(5)) # 25
print(квадрат_лямбда(5)) # 25
Для простых случаев lambda-функции могут быть короче и компактнее, но для сложной логики предпочтительнее обычные функции:
# Более сложная логика — лучше использовать обычную функцию
def анализ_числа(x):
"""Анализирует число и возвращает информацию о нем."""
результат = []
if x % 2 == 0:
результат.append("четное")
else:
результат.append("нечетное")
if x > 0:
результат.append("положительное")
elif x < 0:
результат.append("отрицательное")
else:
результат.append("ноль")
return ", ".join(результат)
# Это было бы очень сложно и неудобно реализовать как lambda-функцию
print(анализ_числа(5)) # нечетное, положительное
print(анализ_числа(-2)) # четное, отрицательное
print(анализ_числа(0)) # четное, ноль
3.4 Рекурсия
Рекурсия — это метод решения задач, при котором функция вызывает сама себя. Это мощная техника, которая позволяет элегантно решать многие сложные проблемы, особенно те, которые можно разбить на подзадачи того же типа.
Основные компоненты рекурсивной функции
- Базовый случай — условие, при котором рекурсия останавливается
- Рекурсивный случай — функция вызывает сама себя с измененными аргументами
Важно!
Каждая рекурсивная функция должна иметь как минимум один базовый случай, иначе она будет выполняться бесконечно (или до переполнения стека).
Простой пример: факториал
Факториал числа n (обозначается n!) — это произведение всех положительных целых чисел от 1 до n.
def факториал(n):
"""Вычисляет факториал числа n рекурсивно."""
# Базовый случай
if n == 0 or n == 1:
return 1
# Рекурсивный случай
return n * факториал(n - 1)
print(факториал(5)) # 120 (= 5 * 4 * 3 * 2 * 1)
Разберем выполнение функции факториал(5)
:
факториал(5)
→5 * факториал(4)
факториал(4)
→4 * факториал(3)
факториал(3)
→3 * факториал(2)
факториал(2)
→2 * факториал(1)
факториал(1)
→1
(базовый случай)- Теперь вычисления идут в обратном порядке:
2 * 1 = 2
,3 * 2 = 6
,4 * 6 = 24
,5 * 24 = 120
Числа Фибоначчи
Последовательность Фибоначчи — это ряд чисел, где каждое следующее число равно сумме двух предыдущих: 0, 1, 1, 2, 3, 5, 8, 13, 21, ...
def фибоначчи(n):
"""Возвращает n-ое число Фибоначчи."""
# Базовые случаи
if n == 0:
return 0
if n == 1:
return 1
# Рекурсивный случай
return фибоначчи(n - 1) + фибоначчи(n - 2)
# Вывод первых 10 чисел Фибоначчи
for i in range(10):
print(f"фибоначчи({i}) = {фибоначчи(i)}")
# фибоначчи(0) = 0
# фибоначчи(1) = 1
# фибоначчи(2) = 1
# фибоначчи(3) = 2
# фибоначчи(4) = 3
# фибоначчи(5) = 5
# фибоначчи(6) = 8
# фибоначчи(7) = 13
# фибоначчи(8) = 21
# фибоначчи(9) = 34
Проблемы с наивной реализацией Фибоначчи
Наивная рекурсивная реализация чисел Фибоначчи крайне неэффективна, так как выполняет много повторных вычислений. Например, при вычислении фибоначчи(5)
функция фибоначчи(2)
вызывается трижды!
Оптимизация рекурсии: мемоизация
Мемоизация — это техника оптимизации, при которой мы сохраняем результаты уже выполненных вычислений, чтобы не повторять их.
def фибоначчи_мемо(n, память={}):
"""Оптимизированная версия функции Фибоначчи с мемоизацией."""
# Проверяем, не вычисляли ли мы уже это значение
if n in память:
return память[n]
# Базовые случаи
if n == 0:
результат = 0
elif n == 1:
результат = 1
# Рекурсивный случай
else:
результат = фибоначчи_мемо(n - 1, память) + фибоначчи_мемо(n - 2, память)
# Сохраняем результат в памяти
память[n] = результат
return результат
# Теперь вычисление больших чисел Фибоначчи стало гораздо быстрее
print(фибоначчи_мемо(50)) # 12586269025
Примечание о значении по умолчанию
В данном случае мы намеренно используем изменяемый объект (словарь) как значение по умолчанию, так как нам нужно сохранять результаты между вызовами функции. Это один из редких случаев, когда такой подход оправдан.
Рекурсивный обход структур данных
Рекурсия особенно полезна при работе с вложенными структурами данных, такими как деревья или вложенные списки.
# Обход вложенного списка
def сумма_вложенных(список):
"""Вычисляет сумму всех чисел во вложенном списке любой глубины."""
общая_сумма = 0
for элемент in список:
if isinstance(элемент, list):
# Если элемент - список, рекурсивно вызываем функцию
общая_сумма += сумма_вложенных(элемент)
else:
# Если элемент - число, добавляем его к сумме
общая_сумма += элемент
return общая_сумма
# Пример вложенного списка
вложенный_список = [1, 2, [3, 4, [5, 6]], 7, [8, [9]]]
print(сумма_вложенных(вложенный_список)) # 45 (= 1+2+3+4+5+6+7+8+9)
Обход файловой системы
Рекурсия часто используется для обхода иерархических структур, таких как файловая система.
import os
def найти_файлы(директория, расширение):
"""Рекурсивно находит все файлы с указанным расширением в директории и поддиректориях."""
найденные_файлы = []
# Получаем список всех файлов и папок в текущей директории
for имя in os.listdir(директория):
полный_путь = os.path.join(директория, имя)
# Если это директория, рекурсивно ищем в ней
if os.path.isdir(полный_путь):
найденные_файлы.extend(найти_файлы(полный_путь, расширение))
# Если это файл с нужным расширением, добавляем его
elif имя.endswith(расширение):
найденные_файлы.append(полный_путь)
return найденные_файлы
# Пример использования
# python_файлы = найти_файлы("/путь/к/директории", ".py")
# print(f"Найдено {len(python_файлы)} Python файлов")
Рекурсия vs Итерация
Многие рекурсивные функции можно переписать с использованием циклов (итеративно). Сравним рекурсивную и итеративную версии функции факториала:
# Рекурсивная версия
def факториал_рекурсивный(n):
if n == 0 or n == 1:
return 1
return n * факториал_рекурсивный(n - 1)
# Итеративная версия
def факториал_итеративный(n):
результат = 1
for i in range(2, n + 1):
результат *= i
return результат
print(факториал_рекурсивный(5)) # 120
print(факториал_итеративный(5)) # 120
Преимущества и недостатки рекурсии
Преимущества:
- Код часто получается более элегантным и читаемым
- Естественно подходит для задач, которые уже имеют рекурсивную структуру
- Упрощает работу со сложными структурами данных (деревья, графы)
Недостатки:
- Повышенное потребление памяти из-за создания стековых кадров
- Риск переполнения стека (Python ограничивает глубину рекурсии)
- Часто менее эффективна, чем итеративные решения
- Может быть сложнее отлаживать
Ограничение глубины рекурсии в Python
Python имеет встроенное ограничение на глубину рекурсии для предотвращения переполнения стека. По умолчанию это значение равно 1000, но его можно изменить:
import sys
# Узнать текущий лимит рекурсии
print(sys.getrecursionlimit()) # Обычно 1000
# Установить новый лимит (используйте с осторожностью!)
sys.setrecursionlimit(2000)
print(sys.getrecursionlimit()) # 2000
Советы по использованию рекурсии
- Всегда определяйте базовый случай, чтобы избежать бесконечной рекурсии
- Используйте мемоизацию для оптимизации повторяющихся вычислений
- Рассмотрите итеративное решение для задач с глубокой рекурсией
- Избегайте изменения глобальных переменных внутри рекурсивных функций
- Для очень сложных рекурсивных алгоритмов рассмотрите "хвостовую рекурсию"
Хвостовая рекурсия
Хвостовая рекурсия — это особый вид рекурсии, при котором рекурсивный вызов является последней операцией в функции. Некоторые языки программирования оптимизируют хвостовую рекурсию, но Python, к сожалению, этого не делает.
# Обычная рекурсия (не хвостовая)
def факториал(n):
if n == 0 or n == 1:
return 1
return n * факториал(n - 1) # Нужно выполнить умножение после возврата из рекурсии
# Хвостовая рекурсия
def факториал_хвостовой(n, аккумулятор=1):
if n == 0 or n == 1:
return аккумулятор
return факториал_хвостовой(n - 1, n * аккумулятор) # Последняя операция - рекурсивный вызов
print(факториал(5)) # 120
print(факториал_хвостовой(5)) # 120
В Python хвостовая рекурсия не оптимизируется автоматически, но такой стиль программирования все равно может быть полезен для ясности кода.
Поздравляем!
Вы прошли модуль "Функции" в Python. Теперь вы знаете, как создавать и использовать функции для организации и повторного использования кода!