Примеры идут в порядке лекций, которые повторяет этот урок: документация функций (docstrings, help(), аннотации типов) → работа со словарями → рекурсия (факториал, бинарный поиск, хвостовая рекурсия, обход вложенных структур) → глубокое копирование. Весь вывод в комментариях получен реальным запуском кода, а не набран по памяти.
Пример 1. Docstring и help()
Функция без документации понятна только тому, кто её написал. Docstring — встроенный способ объяснить, что делает функция, прямо в коде: строка в тройных кавычках сразу после заголовка сохраняется в атрибуте __doc__ и видна через help(), в отличие от обычного комментария #, который ни __doc__, ни help() не видят.
# docstring_example.py
def greet(name: str) -> str:
"""Возвращает строку приветствия.
:param name: Имя пользователя.
:return: Приветственное сообщение.
"""
return f"Hello, {name}!"
print(greet("Alice"))
# Hello, Alice!
print(greet.__doc__)
# Возвращает строку приветствия.
#
# :param name: Имя пользователя.
# :return: Приветственное сообщение.
help(greet)
# Help on function greet in module __main__:
#
# greet(name: str) -> str
# Возвращает строку приветствия.
#
# :param name: Имя пользователя.
# :return: Приветственное сообщение.
Что происходит: тройные кавычки сразу после заголовка функции не выполняются как код — интерпретатор сохраняет их в greet.__doc__. help(greet) идёт дальше: берёт сигнатуру функции вместе с аннотациями типов и её docstring и печатает их в едином человекочитаемом виде — так же, как help() работает для встроенных функций вроде help(sum).
Пример 2. Аннотации типов
Аннотации — единственный способ Python сообщить читателю и IDE, какие типы аргументов и возврата ожидает функция, без отдельной документации. Но это только подсказка, не проверка: значение любого типа всё равно попадёт в функцию без ошибки.
# type_hints_example.py
def add(a: int, b: int) -> int:
"""Возвращает сумму двух целых чисел."""
return a + b
def convert_to_celsius(fahrenheit: float) -> float:
"""Конвертирует температуру из Фаренгейта в Цельсий."""
return (fahrenheit - 32) * 5 / 9
def is_even(number: int) -> bool:
"""Определяет, является ли число чётным."""
return number % 2 == 0
def log_message(message: str) -> None:
"""Выводит сообщение в консоль."""
print(f"LOG: {message}")
print(add(3, 5)) # 8
print(convert_to_celsius(68.0)) # 20.0
print(is_even(4)) # True
log_message("start") # LOG: start
Почему так: аннотации int, float, bool, None хранятся в __annotations__ функции и используются IDE и mypy для статического анализа. Во время выполнения Python их не проверяет: add("3", "5") вернёт строку "35", а не вызовет ошибку.
Пример 3. Инверсия словаря
Словарь отображает ключ в значение только в одну сторону. Если значения уникальны, полезно построить обратное отображение — например, чтобы по коду товара найти его название, имея словарь «название → код».
# invert_dict.py
original_dict = {"a": 1, "b": 2, "c": 3}
inverted_dict = {value: key for key, value in original_dict.items()}
print("Инверсированный словарь:", inverted_dict)
# Инверсированный словарь: {1: 'a', 2: 'b', 3: 'c'}
Что происходит: {value: key for key, value in original_dict.items()} проходит по парам исходного словаря и строит новый, меняя ключ и значение местами. Результат корректен, только если исходные значения уникальны и хешируемы — иначе более поздняя пара молча перезапишет более раннюю с тем же значением.
Пример 4. Замена чисел на слова
Часто нужно заменить значения словаря по правилам из другого словаря-сопоставления, а не по формуле. dict.get(value, value) делает это безопасно: если сопоставления нет, значение остаётся как есть, а не превращается в None.
# numbers_to_words.py
number_to_word = {1: "один", 2: "два", 3: "три"}
data = {"x": 1, "y": 2, "z": 3}
translated = {
key: number_to_word.get(value, value)
for key, value in data.items()
}
print(translated)
# {'x': 'один', 'y': 'два', 'z': 'три'}
Почему так: number_to_word.get(value, value) ищет value как ключ в словаре сопоставлений; если не находит — возвращает второй аргумент, то есть само value. Это надёжнее, чем number_to_word[value], который упал бы с KeyError на любом числе вне словаря.
Пример 5. Факториал
Факториал — классический пример рекурсии с одним базовым случаем (0! = 1! = 1) и одним рекурсивным (n! = n × (n-1)!). Аргумент n - 1 в каждом вызове приближается к базовому случаю — без этого получилась бы бесконечная рекурсия.
# factorial.py
def factorial(n: int) -> int:
"""Возвращает факториал неотрицательного целого числа."""
if n == 0 or n == 1:
return 1
return n * factorial(n - 1)
print(factorial(5)) # 120
Что происходит: каждый вызов factorial(n) откладывает умножение и вызывает factorial(n - 1), пока не дойдёт до n == 1. После этого стек вызовов разворачивается снизу вверх: 1, затем 2 × 1, затем 3 × 2, и так до 5 × 24 = 120.
Пример 6. Бинарный поиск
Бинарный поиск находит элемент в отсортированном списке за O(log n) вместо O(n) у линейного перебора — на каждом шаге отсекается половина диапазона поиска.
# binary_search.py
from typing import Optional
def binary_search(
arr: list[int],
target: int,
left: int,
right: int
) -> Optional[int]:
"""Ищет target в отсортированном списке arr. Возвращает индекс или None."""
if left > right:
return None
mid = (left + right) // 2
if arr[mid] == target:
return mid
if arr[mid] < target:
return binary_search(arr, target, mid + 1, right)
return binary_search(arr, target, left, mid - 1)
array = [1, 3, 5, 7, 9, 11, 13]
print(binary_search(array, 5, 0, len(array) - 1)) # 2
print(binary_search(array, 13, 0, len(array) - 1)) # 6
print(binary_search(array, 8, 0, len(array) - 1)) # None
Почему так: на каждом рекурсивном вызове диапазон [left, right] сужается вдвое: если arr[mid] меньше target, интересующая половина — правая, иначе — левая. Базовый случай left > right означает, что диапазон опустел и элемента в массиве нет, поэтому функция возвращает None.
Пример 7. Хвостовая рекурсия и итерация
Здесь важно показать, что «хвостовая» форма рекурсии (рекурсивный вызов — последняя операция функции) не даёт в Python выигрыша по сравнению с обычной рекурсией — в отличие от языков вроде Scheme, где такую рекурсию оптимизирует компилятор.
# tail_vs_iterative.py
def factorial_tail(n: int, accumulator: int = 1) -> int:
"""Хвостовая рекурсия (не оптимизируется в Python)."""
if n == 0 or n == 1:
return accumulator
return factorial_tail(n - 1, n * accumulator)
def factorial_iterative(n: int) -> int:
"""Итеративный факториал."""
accumulator = 1
while n > 1:
accumulator *= n
n -= 1
return accumulator
print(factorial_tail(5)) # 120
print(factorial_iterative(5)) # 120
Что происходит: оба варианта дают одинаковый результат, но накладные расходы разные. factorial_tail всё равно создаёт n кадров стека, потому что Python не убирает кадр до фактического возврата из рекурсии — оптимизации хвостовых вызовов (TCO) в CPython нет. factorial_iterative работает в одном кадре и с постоянной памятью, поэтому именно её стоит использовать при больших n.
Пример 8. Собственный аналог deepcopy
Здесь видно, что deepcopy() — не магия, а рекурсивный обход структуры: списки, кортежи, множества и словари копируются поэлементно, а неизменяемые простые типы (числа, строки) возвращаются как есть, потому что их и так нельзя изменить на месте.
# custom_deepcopy.py
def deep_copy(data):
"""Рекурсивно копирует списки, кортежи, множества и словари."""
if isinstance(data, list):
return [deep_copy(item) for item in data]
if isinstance(data, dict):
return {key: deep_copy(value) for key, value in data.items()}
if isinstance(data, set):
return {deep_copy(item) for item in data}
if isinstance(data, tuple):
return tuple(deep_copy(item) for item in data)
return data
original = [[1, 2], (4, [5, 6], {7, 8}), {"a": 9, "b": [10, 11]}]
copied = deep_copy(original)
original[1][1][0] = 0
print("Оригинал:", original)
# Оригинал: [[1, 2], (4, [0, 6], {8, 7}), {'a': 9, 'b': [10, 11]}]
print("Копия: ", copied)
# Копия: [[1, 2], (4, [5, 6], {8, 7}), {'a': 9, 'b': [10, 11]}]
Почему так: deep_copy проверяет тип через isinstance и для каждого контейнера строит новый объект, рекурсивно копируя элементы. Когда original[1][1][0] меняется (вложенный список внутри кортежа), это не задевает copied, потому что кортеж внутри copied ссылается на отдельно созданный список. Множество при печати может показывать элементы в другом порядке, чем при создании ({8, 7} вместо {7, 8}) — это особенность хранения set в CPython, а не ошибка кода.
Пример 9. Guard clause и что происходит без базового случая
Рекурсия без базового случая — не абстрактная опасность, а реальная ошибка (см. типичные ошибки, ошибка 1). А для отрицательного n у факториала базового случая нет вообще — это математически не определено, и корректный код должен явно сообщить об этом через исключение, а не тихо уйти в бесконечную рекурсию. Задание 3 в разделе заданий просит реализовать именно guard clause для отрицательных чисел.
# factorial_guard.py
def factorial_safe(n: int) -> int:
"""Возвращает факториал неотрицательного целого числа.
:raises ValueError: если n отрицательное.
"""
if n < 0:
raise ValueError("n must be >= 0")
if n == 0 or n == 1:
return 1
return n * factorial_safe(n - 1)
def factorial_broken(n: int) -> int:
"""Без базового случая — рекурсия никогда не остановится."""
return n * factorial_broken(n - 1)
print(factorial_safe(5)) # 120
try:
factorial_safe(-3)
except ValueError as error:
print("ValueError:", error)
# ValueError: n must be >= 0
try:
factorial_broken(5)
except RecursionError as error:
print("RecursionError:", error)
# RecursionError: maximum recursion depth exceeded
Что происходит: factorial_safe отдельно проверяет n < 0 и поднимает ValueError — вызывающий код сразу получает понятную ошибку вместо неверного результата или зависания. factorial_broken — та же ошибка, что в разделе типичных ошибок: у него вообще нет условия остановки, поэтому Python рано или поздно поднимает RecursionError, когда стек вызовов достигает предела глубины.
sys.getrecursionlimit() и sys.setrecursionlimit() — за рамками материалов этого урока. Сверьтесь с docs.python.org: sys.getrecursionlimit.
Пример 10. Сумма цифр числа (рекурсия по разрядам)
Рекурсия — не только про списки и деревья, но и про числа: если число больше одной цифры, его можно разбить на «последнюю цифру» и «всё остальное» через % 10 и // 10 и вызвать себя от остатка. Задание 5 просит реализовать именно эту функцию.
# sum_digits.py
def sum_digits(n: int) -> int:
"""Возвращает сумму цифр неотрицательного целого числа."""
if n < 10:
return n
return n % 10 + sum_digits(n // 10)
print(sum_digits(43197)) # 24
Почему так: n % 10 отделяет последнюю цифру, а n // 10 отбрасывает её, уменьшая число на один разряд с каждым вызовом — это и есть движение к базовому случаю n < 10 (однозначное число, сумма цифр которого равна ему самому). Для 43197 стек разворачивается как 7 + (9 + (1 + (3 + 4))) = 24.
Пример 11. Сумма чисел во вложенных списках
Списки могут быть вложены на произвольную глубину, и заранее неизвестно, сколько уровней нужно обойти циклом — рекурсия решает это естественно: если элемент сам список, вызываем себя от него, иначе прибавляем число. Задание 6 просит реализовать sum_nested именно для такого случая.
# sum_nested.py
def sum_nested(numbers: list) -> int:
"""Рекурсивно суммирует числа во вложенных списках."""
total = 0
for item in numbers:
if isinstance(item, list):
total += sum_nested(item)
else:
total += item
return total
nested_numbers = [1, [2, 3], [4, [5, 6]], 7]
print(sum_nested(nested_numbers)) # 28
Что происходит: функция проходит по элементам numbers и проверяет каждый через isinstance(item, list): для вложенного списка она рекурсивно вызывает sum_nested(item) и прибавляет результат, для числа — прибавляет само число. Глубина вложенности значения не имеет: [4, [5, 6]] разворачивается в 4 + (5 + 6), а весь список — в 1 + (2 + 3) + (4 + (5 + 6)) + 7 = 28.
Как запустить в VS Code
- Создайте файл, например
summary_12_examples.py. - Скопируйте код одного из примеров выше.
- Откройте терминал в VS Code (Ctrl + `).
- Убедитесь, что активировано виртуальное окружение.
- Запустите:
python summary_12_examples.py