Уроки курса
1 Введение в Python и философия дзен
30 мин
2 Переменные и динамическая типизация
30 мин
3 Базовые типы данных: числа, строки и булевы значения
30 мин
4 Изменяемые и неизменяемые объекты (Mutable vs Immutable)
30 мин
5 Форматирование строк и f-строки
30 мин
6 Углубленная работа со списками
30 мин
7 Кортежи и их особенности
30 мин
8 Словари под капотом
30 мин
9 Множества и математические операции
30 мин
10 Генераторы списков (List Comprehensions)
30 мин
11 Генераторы словарей и множеств
30 мин
12 Встроенные функции для коллекций
30 мин
13 Условные операторы и логические выражения
30 мин
14 Циклы while и управление потоком
30 мин
15 Итерация с циклом for
30 мин
16 Конструкции for...else и while...else
30 мин
17 Функции enumerate и zip
30 мин
18 Создание собственных функций (def)
30 мин
19 Позиционные и именованные аргументы
30 мин
20 Проблема изменяемых аргументов по умолчанию
30 мин
21 Произвольное число аргументов (*args и **kwargs)
30 мин
22 Область видимости переменных (LEGB)
30 мин
23 Анонимные функции (lambda)
30 мин
24 Функции высшего порядка
30 мин
25 Замыкания (Closures)
30 мин
26 Введение в объектно-ориентированное программирование
30 мин
27 Атрибуты классов и экземпляров
30 мин
28 Магический метод __init__
30 мин
29 Методы экземпляра
30 мин
30 Инкапсуляция и сокрытие данных
30 мин
31 Декоратор @property
30 мин
32 Наследование классов
30 мин
33 Переопределение методов и функция super()
30 мин
34 Полиморфизм в Python
30 мин
35 Магические методы строк (__str__ и __repr__)
30 мин
36 Обработка исключений (try-except)
30 мин
37 Блоки else и finally
30 мин
38 Генерация собственных исключений (raise)
30 мин
39 Открытие и чтение файлов
30 мин
40 Запись данных в файлы
30 мин
41 Контекстные менеджеры (with)
30 мин
42 Работа с форматом JSON
30 мин
43 Модули и импорты
30 мин
44 Полезные модули стандартной библиотеки
30 мин
45 Модуль datetime
30 мин
46 Модуль collections
30 мин
47 Виртуальные окружения (venv)
30 мин
48 Установка сторонних пакетов через pip
30 мин
49 Организация структуры Python-проекта
30 мин
50 Финальный проект: создание приложения
30 мин

Создание собственных функций (def)

Понимание базовой структуры функции, передачи параметров и возвращения значений через return.

Прогресс урока: 0%

Добро пожаловать в восемнадцатый урок нашего углубленного курса по программированию на языке Python. Сегодня мы переходим к одной из самых важных, фундаментальных и определяющих концепций в разработке программного обеспечения — созданию собственных пользовательских функций. На уровне Intermediate вы перестаете просто писать линейные скрипты, которые выполняются строго сверху вниз (так называемый спагетти-код), и начинаете проектировать модульную архитектуру вашего приложения. Функции — это базовые строительные блоки этой архитектуры. Представьте себе, что вы строите огромный город или завод. Если бы вы каждый раз заново изобретали кирпич или шестеренку, процесс строительства занял бы десятилетия, а результат был бы нестабильным. Вместо этого вы создаете стандартный, надежный компонент один раз и используете его миллионы раз. Именно эту важнейшую роль выполняют функции в программном коде. В инженерии программного обеспечения существует критически важный принцип, называемый DRY — Don't Repeat Yourself (Не повторяй себя). Этот основополагающий принцип гласит, что каждая часть знания, бизнес-логики или алгоритма должна иметь единственное, однозначное и авторитетное представление в рамках всей системы. Если вы замечаете, что копируете и вставляете один и тот же блок кода в разные части вашей программы, это первый и самый явный сигнал (в профессиональной среде это называется code smell — запах кода), что вам немедленно необходимо вынести этот фрагмент в отдельную абстракцию, то есть в функцию. Python, как высокоуровневый язык, глубоко уважающий читаемость и элегантность, предоставляет нам мощнейшие и при этом невероятно лаконичные инструменты для работы с абстракциями. Важно понимать: в отличие от старых процедурных языков, в Python функции являются объектами первого класса (first-class citizens). Это означает, что функции обладают абсолютно теми же правами, что и любые другие типы данных (числа, строки, списки): вы можете присваивать функции переменным, передавать их в качестве аргументов в другие функции, возвращать одни функции из других функций и даже хранить их в структурах данных, таких как списки или словари. Это открывает двери к функциональной парадигме программирования, которую мы начнем постепенно затрагивать. Сегодня мы сфокусируемся на базовой анатомии: ключевом слове def, правилах именования по стандарту PEP 8, потоке выполнения и том, как интерпретатор (обычно CPython) обрабатывает ваш код при встрече с функцией. Мы детально разберем, как именно выделяется память под объект функции и почему само по себе определение функции (ее написание в коде) не приводит к выполнению инструкций внутри нее. Погружаясь в эти нюансы, вы начнете мыслить как Software Engineer.

python
# Проблема: дублирование кода (нарушение DRY)
print("--- Отчет пользователя ---")
print("Имя: Иван")
print("Статус: Активен")
print("-------------------------")

print("--- Отчет пользователя ---")
print("Имя: Анна")
print("Статус: Заблокирован")
print("-------------------------")

# Решение: использование функции
def print_user_report(name, status):
    print("--- Отчет пользователя ---")
    print(f"Имя: {name}")
    print(f"Статус: {status}")
    print("-------------------------")

print_user_report("Иван", "Активен")
print_user_report("Анна", "Заблокирован")

Давайте детально разберем синтаксис и механику работы представленного выше кода, чтобы понять, что происходит под капотом интерпретатора Python. Определение функции всегда и без исключений начинается с зарезервированного ключевого слова def (сокращение от define — определить). После пробела следует имя функции. В Python существует жесткий стандарт оформления кода, называемый PEP 8. Согласно этому стандарту, имена функций должны состоять исключительно из строчных букв, а слова должны разделяться символом подчеркивания (этот стиль называется snake_case или змеиный_регистр). Имя функции должно быть глаголом или глагольной фразой, так как функция всегда выполняет какое-то действие. Например, calculate_total, get_user_data, print_report — это отличные имена. А вот calculation или data — плохие имена для функций, так как они звучат как существительные (переменные). После имени функции обязательно ставятся круглые скобки. Внутри этих скобок могут указываться параметры (мы подробно обсудим их разницу с аргументами в следующем модуле). Строка определения функции завершается двоеточием, которое в Python всегда сигнализирует о начале нового блока кода с отступом. Все, что написано под def с отступом (обычно это 4 пробела), является телом функции. Крайне важно осознать концепцию потока выполнения (execution flow). Когда интерпретатор Python читает ваш файл скрипта сверху вниз и встречает ключевое слово def, он не выполняет код внутри тела функции. Вместо этого он совершает несколько внутренних операций: он компилирует тело функции во внутренний байт-код, создает в оперативной памяти специальный объект типа function, и затем связывает имя, которое вы дали функции, с адресом этого объекта в памяти. То есть, на этапе определения функция просто сохраняется как план действий или чертеж. Чтобы чертеж превратился в реальное здание, функцию необходимо вызвать. Вызов функции происходит путем написания ее имени с обязательными круглыми скобками в конце (и передачей необходимых данных внутрь скобок). Именно в этот момент поток выполнения программы совершает прыжок из текущего места прямо в тело функции, построчно выполняет все инструкции внутри, а затем совершает обратный прыжок в ту точку, откуда функция была вызвана, чтобы продолжить выполнение остального кода. Если вы забудете поставить круглые скобки при вызове (напишете просто print_user_report без скобок), функция не выполнится; вместо этого Python просто вернет вам ссылку на объект функции в памяти. Это одна из самых частых ошибок новичков, переходящих от синтаксиса к реальной архитектуре.

Что произойдет, когда интерпретатор Python при выполнении скрипта (чтении файла сверху вниз) впервые встретит конструкцию 'def my_function():'?

Как называется стиль написания идентификаторов (имен функций и переменных), рекомендуемый стандартом PEP 8, при котором все буквы строчные, а слова разделяются подчеркиваниями (например: calculate_total_sum)?

Переходим к фундаментально важной терминологии, которую путают 90% начинающих (и даже некоторые разработчики среднего уровня): разница между Параметрами и Аргументами. В повседневной речи программистов эти слова часто используются как синонимы, но с академической и технической точки зрения между ними существует строгая граница. Параметры — это переменные, которые вы указываете внутри круглых скобок при создании (определении) функции. Они выступают в роли заполнителей или пустых контейнеров, ожидающих получения реальных данных. Когда вы пишете def multiply(a, b):, имена 'a' и 'b' являются параметрами. Они определяют интерфейс вашей функции, сообщая миру: «Чтобы я могла работать, мне нужны два куска данных». С другой стороны, Аргументы — это фактические, реальные значения, данные или объекты, которые вы передаете в функцию в момент ее вызова. Когда вы пишете multiply(5, 10), числа 5 и 10 являются аргументами. В момент вызова происходит процесс, называемый связыванием (binding) или передачей. Python берет первый аргумент (5) и присваивает его первому параметру (a). Затем берет второй аргумент (10) и присваивает его второму параметру (b). Важно понимать, как именно Python передает эти данные. В отличие от языков C++ или Java, где есть строгая передача по значению (создается копия данных) или передача по ссылке (передается адрес переменной), в Python используется механизм, который технически называется «вызов по совместному использованию» (call by object reference). Это означает, что в функцию передается ссылка на сам объект в памяти, а не его копия. Если вы передаете в функцию неизменяемый объект (например, число, строку или кортеж) и пытаетесь его изменить внутри функции, Python просто создаст новый объект в локальной области видимости, не затрагивая оригинал. Однако, если вы передаете изменяемый объект (например, список или словарь), любые изменения этого объекта внутри функции (например, добавление элемента через .append) отразятся на оригинальном объекте за пределами функции, так как и параметр внутри функции, и переменная снаружи указывают на один и тот же адрес в оперативной памяти. Это критически важное поведение, непонимание которого приводит к трудноуловимым багам на этапе отладки сложных систем.

python
# a и b - это ПАРАМЕТРЫ (определяются при создании)
def add_numbers(a, b):
    print(f"Получены данные: a={a}, b={b}")
    result = a + b
    print(f"Сумма: {result}")

# 15 и 20 - это АРГУМЕНТЫ (передаются при вызове)
add_numbers(15, 20)

# x и y - переменные, значения которых станут аргументами
x = 100
y = 50
add_numbers(x, y)

Давайте углубимся в механизмы передачи аргументов, начав с самого базового и распространенного типа — Позиционных аргументов (Positional Arguments). По умолчанию, когда вы вызываете функцию и передаете в нее серию значений через запятую, Python распределяет эти значения по параметрам строго на основе их позиции (порядка следования). Первый переданный аргумент становится значением первого параметра, второй аргумент — второго параметра, и так далее. Этот механизм требует абсолютной точности от программиста. Во-первых, порядок имеет критическое значение для бизнес-логики. Если у вас есть функция def register_user(name, age): и вы вызовете ее как register_user(25, "Алексей"), программа технически не сломается на этапе передачи (так как Python имеет динамическую типизацию), но семантика будет нарушена: имя пользователя станет числом 25, а возраст — строкой «Алексей», что неизбежно приведет к ошибкам в других частях программы (например, при попытке сохранить это в базу данных или произвести математические расчеты с возрастом). Во-вторых, количество переданных позиционных аргументов должно в точности совпадать с количеством параметров, заявленных в сигнатуре функции. Эта характеристика функции (количество ожидаемых аргументов) называется ее арностью (arity). Если функция ожидает два параметра, а вы передадите только один, интерпретатор Python немедленно остановит выполнение программы и выбросит исключение TypeError: function missing 1 required positional argument. Точно так же, если вы попытаетесь передать три аргумента функции, ожидающей два, вы получите TypeError: function takes 2 positional arguments but 3 were given. Эта строгая проверка арности является проявлением философии Python: «Явное лучше, чем неявное» (Zen of Python). В отличие от JavaScript, который может молча подставить undefined для недостающих аргументов или проигнорировать лишние, Python заставляет вас сразу исправить ошибку, не допуская непредсказуемого поведения системы в скрытом режиме.

В чем заключается главное правило передачи позиционных аргументов в функцию в Python?

Какое исключение (ошибку) выбросит Python, если в функцию, требующую 2 позиционных параметра, передать 3 аргумента? (Укажите только название типа ошибки, например ValueError)

Задание

Практическое задание: Исправление ошибки позиционных аргументов. Представьте, что вы проводите код-ревью.

  • Прочитайте исходный код: def send_email(to, subject, body): ...
  • Найдите ошибочный вызов: send_email('Привет', 'admin@mail.com', 'Текст')
  • Поймите, что нарушен порядок аргументов. Письмо отправится на адрес 'Привет'.
  • Перепишите вызов, соблюдая правильный порядок: to, затем subject, затем body.
10 баллов

Чтобы решить проблему жесткой привязки к порядку позиционных аргументов и улучшить читаемость кода, в Python существует мощный механизм Именованных аргументов (Keyword Arguments). При вызове функции вы можете явно указать, какому параметру предназначается то или иное значение, используя синтаксис имя_параметра=значение прямо внутри круглых скобок. Когда вы используете именованные аргументы, порядок их передачи больше не имеет абсолютно никакого значения, так как интерпретатор сопоставляет значения не по их позиции, а по их именам. Вернемся к нашему примеру: def register_user(name, age):. Вы можете безопасно вызвать ее так: register_user(age=25, name="Алексей"). Python корректно поймет вашу интенцию. Использование именованных аргументов — это признак хорошего тона (Best Practice) в сообществе Python, особенно для функций, которые принимают три и более параметра или параметры одного типа данных (например, несколько булевых флагов). Это делает вызов функции самодокументируемым. Читая вызов create_window(800, 600, True, False), другой разработчик (или вы сами через месяц) не поймет, что означают True и False. Но вызов create_window(width=800, height=600, resizable=True, fullscreen=False) становится кристально ясным без необходимости заглядывать в определение функции. Однако существует одно непреложное синтаксическое правило языка Python, нарушение которого приводит к немедленной ошибке SyntaxError. Если в одном вызове функции вы смешиваете позиционные и именованные аргументы, все позиционные аргументы обязаны располагаться до первого именованного аргумента. Вызов register_user("Алексей", age=25) абсолютно корректен (сначала позиционный name, затем именованный age). А вот вызов register_user(name="Алексей", 25) приведет к ошибке SyntaxError: positional argument follows keyword argument. Компилятор Python спроектирован так, чтобы избегать двусмысленности. Как только он встречает именованный аргумент, он прекращает отсчет позиций, и любые последующие аргументы без имен ставят его в тупик, так как он больше не знает, к какому индексу их привязать.

python
def configure_server(ip, port, is_secure, protocol):
    print(f"Connecting to {ip}:{port}")
    print(f"Secure: {is_secure}, Protocol: {protocol}")

# Плохо читаемый позиционный вызов (что значит True? что значит 'tcp'?)
configure_server("192.168.1.1", 8080, True, "tcp")

# Отличный вызов с именованными аргументами (порядок изменен, но все работает)
configure_server(port=8080, protocol="tcp", ip="192.168.1.1", is_secure=True)

# Смешанный вызов (позиционные ДО именованных - это ПРАВИЛЬНО)
configure_server("10.0.0.1", 443, protocol="udp", is_secure=True)

Мы рассмотрели ситуации, когда все параметры функции являются обязательными. Но что, если некоторые параметры в большинстве случаев имеют одно и то же предсказуемое значение, и мы не хотим заставлять программиста каждый раз передавать его вручную? Здесь на сцену выходят Параметры по умолчанию (Default Parameters). На этапе определения функции вы можете присвоить параметру базовое значение, используя знак равенства прямо в сигнатуре функции, например: def connect(timeout=10):. Благодаря этому, параметр становится опциональным (необязательным) при вызове функции. Если разработчик вызовет функцию без аргумента, интерпретатор Python автоматически подставит значение по умолчанию (в нашем случае 10). Если же разработчик передаст свое значение, например connect(timeout=30), это новое значение переопределит (перезапишет) значение по умолчанию. Использование параметров по умолчанию делает ваши функции гибкими и удобными, позволяя им работать 'из коробки' для типичных сценариев, сохраняя при этом возможность тонкой настройки для нестандартных ситуаций. Однако, как и в случае со смешиванием аргументов при вызове, при создании сигнатуры функции существует строгое правило порядка: параметры без значений по умолчанию (обязательные) всегда должны располагаться ДО параметров со значениями по умолчанию (необязательных). Сигнатура def greet(msg, name="Гость"): корректна. Сигнатура def greet(name="Гость", msg): вызовет ошибку SyntaxError: non-default argument follows default argument. Это логично: если бы Python разрешил такую конструкцию, при вызове greet("Привет") было бы абсолютно непонятно, чему предназначается строка 'Привет' — она должна заменить 'Гость' или стать обязательным сообщением 'msg'? Во избежание такой фатальной двусмысленности язык запрещает подобный синтаксис на уровне компиляции.

Какая из следующих сигнатур (определений) функции написана синтаксически КОРРЕКТНО в Python?

Какая синтаксическая ошибка возникнет, если при ВЫЗОВЕ функции позиционный аргумент указать после именованного? (Напишите первое слово названия ошибки, например Value)

Сейчас мы переходим к одной из самых известных, коварных и опасных ловушек в языке Python, на которую попадаются абсолютно все Junior-разработчики и которая часто встречается в вопросах на технических собеседованиях. Эта концепция называется 'The Mutable Default Argument Trap' (Ловушка изменяемого аргумента по умолчанию). Мы рассматриваем эту тему на основе архивных материалов нашего курса, где был приведен диалог между Junior и Senior разработчиком. Суть проблемы заключается в следующем: что произойдет, если в качестве значения по умолчанию вы используете изменяемый объект (Mutable), например, пустой список [] или пустой словарь {}? Начинающий разработчик пишет функцию: def add_student(student_name, student_list=[]): student_list.append(student_name); return student_list. Логика кажется кристально ясной: если список не передан, функция должна создать новый пустой список, добавить в него имя и вернуть. Но при тестировании происходит катастрофа. При первом вызове add_student("Иван") возвращается ['Иван']. При втором независимом вызове add_student("Мария") для совершенно другой группы студентов, функция возвращает ['Иван', 'Мария']! Студенты из разных вызовов начинают накапливаться в одном и том же списке. Почему это происходит? Разгадка кроется во внутренних механизмах интерпретатора CPython. Значения по умолчанию вычисляются и создаются в памяти строго один раз — в момент определения функции (когда интерпретатор читает ключевое слово def), а не каждый раз при ее вызове. Этот пустой список создается в памяти один раз и сохраняется в скрытом атрибуте объекта функции __defaults__. При каждом вызове функции без второго аргумента, Python использует один и тот же участок памяти. Поскольку списки изменяемы, метод .append модифицирует этот 'встроенный' список. Чтобы навсегда решить эту проблему, Senior-разработчики используют стандартный паттерн проектирования: в качестве значения по умолчанию указывается неизменяемый объект None. А внутри самой функции пишется проверка: if student_list is None: student_list = []. Этот паттерн гарантирует, что при каждом вызове функции без аргумента будет создаваться абсолютно новый, независимый пустой список в новой области памяти. Запомните это правило на всю жизнь: никогда не используйте списки, словари или множества в качестве параметров по умолчанию.

python
# ОПАСНЫЙ АНТИПАТТЕРН (Никогда так не делайте)
def add_item_bad(item, my_list=[]):
    my_list.append(item)
    return my_list

print(add_item_bad("Яблоко"))  # ['Яблоко']
print(add_item_bad("Банан"))   # ['Яблоко', 'Банан'] - Баг! Состояние сохранилось!

# ПРАВИЛЬНЫЙ ПОДХОД (Best Practice)
def add_item_good(item, my_list=None):
    if my_list is None:
        my_list = []  # Создаем новый список при каждом вызове
    my_list.append(item)
    return my_list

print(add_item_good("Яблоко"))  # ['Яблоко']
print(add_item_good("Банан"))   # ['Банан'] - Работает идеально!

После того, как функция приняла параметры, произвела вычисления и получила полезный результат, этот результат необходимо передать обратно в ту часть программы, которая эту функцию вызвала. Для этой цели служит ключевое слово return (вернуть). Понимание разницы между функциями print() и return — это огромный шаг в эволюции программиста. Начинающие разработчики часто пытаются выводить результат работы функции на экран с помощью print(). Однако print() просто выводит текст в системную консоль (stdout) для человека. Внутренней программе от этого вывода нет никакой пользы, данные не сохраняются и не могут быть использованы для дальнейших вычислений. Если вы попытаетесь присвоить результат функции, которая использует только print, переменной (например, result = my_func()), в переменной окажется пустота (объект None). Ключевое слово return решает эту задачу. Оно работает как курьер, который берет вычисленное значение и физически переносит его обратно в точку вызова. Как только интерпретатор встречает инструкцию return, происходят два важнейших события. Во-первых, функция выплевывает указанное значение наружу, где его можно сохранить в переменную, передать в другую функцию или использовать в математическом выражении. Во-вторых, и это критически важно, инструкция return немедленно и окончательно завершает работу функции. Поток выполнения мгновенно покидает тело функции. Любой код, который написан строками ниже инструкции return на том же уровне отступа, физически никогда не будет выполнен интерпретатором. В компьютерных науках такой недостижимый код официально называется 'мертвым кодом' (dead code). Среды разработки, такие как PyCharm или VS Code, часто подсвечивают такие строки серым цветом, предупреждая, что логика нарушена. Использование return позволяет функциям стать полноценными математическими выражениями, которые можно комбинировать друг с другом в сложные цепочки обработки данных.

В чем заключается фундаментальная разница между использованием print() и return внутри функции?

Какой тип объекта рекомендуется использовать в качестве значения по умолчанию для списка или словаря, чтобы избежать ловушки Mutable Default Argument? (Соблюдайте регистр)

Задание

Практическое задание: Преобразование мертвых функций в полезные (вычисляемые).

  • Возьмите функцию: def calculate_area(radius): print(3.14 * radius ** 2)
  • Попытайтесь умножить результат на 2: double_area = calculate_area(5) * 2. Вы получите ошибку TypeError, так как умножаете NoneType на int.
  • Замените 'print' на 'return': def calculate_area(radius): return 3.14 * radius ** 2
  • Теперь функция возвращает число, и математические операции с ее результатом будут работать корректно.
10 баллов

Мы выяснили, что return возвращает значения, но возникает закономерный архитектурный вопрос: что происходит с функцией, в которой программист вообще не написал ключевое слово return, или написал пустой return без указания возвращаемого объекта? В некоторых языках программирования (например, в строгом C++) функция, заявленная как возвращающая значение, вызовет ошибку компиляции, если вы забудете инструкцию возврата. Python, будучи прагматичным языком, обрабатывает эту ситуацию изящнее. Если интерпретатор доходит до последней строчки кода внутри тела функции и не встречает явного return, он самостоятельно и неявно добавляет в конец функции инструкцию return None. Аналогично, если вы пишете просто слово return (например, для досрочного выхода из функции), Python возвращает объект None. Что такое None? Это специальный встроенный тип данных (NoneType), паттерн Singleton, который существует в памяти Python в единственном экземпляре. Он буквально переводится как 'ничто' или 'отсутствие значения'. Эта концепция аналогична null в Java или JavaScript, но с важным отличием: None — это реальный объект. Поскольку None существует только один во всей памяти запущенной программы, сравнение переменных с этим объектом следует проводить не с помощью оператора равенства (==), а с помощью оператора идентичности (is). Оператор is проверяет не равенство значений, а совпадение адресов ячеек памяти. По стандарту PEP 8, проверка должна выглядеть так: if result is None:. Понимание того, что функции без return неявно отдают None, спасает от часов мучительной отладки. Например, когда вы используете встроенный метод списка my_list.sort(), этот метод сортирует оригинальный список на месте (in-place) и не имеет явного return. Если вы напишете new_list = my_list.sort(), вы ожидаете получить отсортированный список, но переменная new_list получит значение None, что приведет к краху программы при попытке итерироваться по этой переменной в будущем. Всегда сверяйтесь с документацией, чтобы знать, возвращает функция новые данные или модифицирует их на месте и возвращает None.

python
def process_data(data):
    # Функция что-то делает, но нет слова return
    cleaned_data = data.strip().lower()

result = process_data("  HELLO  ")
print(result)  # Выведет: None
print(type(result))  # Выведет: <class 'NoneType'>

# Правильная проверка на None по PEP 8:
if result is None:
    print("Функция не вернула никаких полезных данных.")

Одна из самых изящных и мощных особенностей Python, которая вызывает зависть у разработчиков на C++ или Java, — это возможность функции легко и интуитивно возвращать сразу несколько значений. Представьте ситуацию: вам нужно написать функцию геопозиционирования, которая вычисляет координаты устройства и должна вернуть широту и долготу. В старых языках вам пришлось бы создавать специальные громоздкие классы (Data Transfer Objects), использовать массивы или, что еще хуже, передавать в функцию указатели на внешние переменные. В Python все решается элегантно просто: вы можете перечислить возвращаемые переменные через запятую после ключевого слова return, например: return latitude, longitude. Но как это работает под капотом? Неужели Python действительно научился возвращать две вещи одновременно? На самом деле, нет. Архитектура функций в Python по-прежнему позволяет возвращать ровно один и только один объект. Секрет кроется в том, что когда интерпретатор видит значения, разделенные запятыми (без скобок), он автоматически и неявно упаковывает (packing) их в единую структуру данных — Кортеж (Tuple). Кортеж — это неизменяемый аналог списка. То есть функция возвращает один-единственный объект-кортеж, внутри которого лежат два значения. Истинная магия Python раскрывается в момент приема этих данных. Используя механизм множественного присваивания или распаковки (unpacking), вы можете перехватить этот кортеж прямо в точке вызова, развернув его в отдельные переменные: lat, lon = get_coordinates(). Интерпретатор сам распакует кортеж и разложит значения по переменным строго по порядку. Этот механизм не только делает код невероятно кратким, но и открывает путь к классическому питоническому трюку — обмену значениями двух переменных без использования временной третьей переменной: a, b = b, a. За кулисами здесь сначала создается кортеж (b, a), а затем он немедленно распаковывается обратно в a и b.

Какой тип данных фактически возвращает функция в Python, если в инструкции return указано несколько значений через запятую (например: return a, b)?

Какой оператор рекомендуется использовать по стандарту PEP 8 для проверки того, что переменная содержит значение None? (Укажите одно слово, например == или is)

Архитектура функций — это не только правильное использование параметров, но и обеспечение чистоты и читаемости кода внутри самой функции. На уровне Intermediate вы должны познакомиться с профессиональным паттерном программирования, известным как Guard Clauses (Защитные утверждения) или Ранний возврат (Early Return). Начинающие программисты часто строят логику функций с использованием глубоко вложенных конструкций if-else. Например, функция проверки прав пользователя: if user_is_active: if user_has_permissions: if data_is_valid: return process_data() else: return 'Ошибка данных' else: return 'Нет прав' else: return 'Заблокирован'. Это приводит к появлению анти-паттерна, который в шутку называют 'Arrow Anti-pattern' (Анти-паттерн Стрела), когда код смещается далеко вправо из-за отступов, становясь абсолютно нечитаемым. Мозг человека плохо справляется с отслеживанием вложенных контекстов (когнитивная нагрузка). Паттерн Раннего возврата инвертирует эту логику. Вместо того, чтобы вкладывать успешные сценарии друг в друга, мы сначала пишем серию проверок на негативные сценарии в самом начале функции (устанавливаем 'охрану'). Если негативное условие срабатывает, мы немедленно используем return, чтобы выбросить ошибку и прервать выполнение. Код преображается: if not user_is_active: return 'Заблокирован'. Затем на том же базовом уровне отступа: if not user_has_permissions: return 'Нет прав'. И так далее. В самом конце функции, без каких-либо отступов вправо, остается чистый, успешный сценарий выполнения: return process_data(). Такой подход кардинально снижает цикломатическую сложность (метрику запутанности кода). Вы устраняете необходимость в блоках else, код читается линейно сверху вниз как простой список правил, и сразу понятно, какие предварительные условия нужны для успешной работы главной бизнес-логики.

python
# ПЛОХО: Глубокая вложенность (Сложно читать и поддерживать)
def divide_bad(a, b):
    if type(a) == int and type(b) == int:
        if b != 0:
            return a / b
        else:
            return "Ошибка: деление на ноль"
    else:
        return "Ошибка: нужны числа"

# ОТЛИЧНО: Паттерн раннего возврата (Guard Clauses)
def divide_good(a, b):
    # Защитная проверка 1
    if type(a) != int or type(b) != int:
        return "Ошибка: нужны числа"
    
    # Защитная проверка 2
    if b == 0:
        return "Ошибка: деление на ноль"
    
    # Основная логика на нулевом уровне отступа функции
    return a / b

Мы переходим к одному из самых гибких и мощных инструментов функционального программирования в Python, который был упомянут в архивных материалах вашего курса. Представьте, что вы хотите написать математическую функцию для вычисления суммы чисел. Если вы напишете def sum_two(a, b):, она сможет сложить только два числа. Что если пользователь захочет сложить три? Или сто? Вы не можете заранее прописать в сигнатуре функции сто параметров. Для решения этой проблемы используются функции с произвольным количеством позиционных аргументов. В Python этот механизм реализуется с помощью магического оператора-звездочки (asterisk), который ставится перед именем параметра в сигнатуре функции. По негласному стандарту сообщества этот параметр всегда называют *args (от слова arguments), хотя интерпретатору важно только наличие звездочки, а не само слово. Когда вы определяете функцию как def calculate_sum(*args):, звездочка дает интерпретатору следующую команду: 'Собери все лишние позиционные аргументы, которые программист передаст при вызове этой функции, и упакуй их в один единый кортеж (tuple) с именем args'. Таким образом, внутри тела функции переменная args является обычным итерируемым кортежем. Вы можете использовать цикл for item in args: для перебора всех переданных значений, применять к нему встроенные функции вроде len() или sum(), или обращаться по индексам args[0]. Важно понимать архитектурное ограничение: параметр *args может быть только один в сигнатуре функции, и он должен располагаться после всех стандартных обязательных позиционных параметров (если таковые имеются). Например: def log_messages(system_name, *args):. В этом случае первый переданный аргумент уйдет в system_name, а все остальные, сколько бы их ни было, будут собраны в кортеж args. Это делает интерфейс ваших функций невероятно гибким, позволяя создавать инструменты (вроде стандартного print), которые могут принимать от нуля до бесконечности аргументов.

Каким типом данных является переменная args внутри тела функции, если она объявлена в сигнатуре как *args?

Какой специальный символ используется перед параметром функции, чтобы собрать произвольное количество позиционных аргументов? (Введите только сам символ)

Задание

Практическое задание: Использование *args для гибких функций.

  • Создайте функцию def find_max(*args):
  • Если вызвать ее как find_max(5, 10, 3), внутри args станет кортежем (5, 10, 3).
  • Внутри функции проверьте: if not args: return None (чтобы не сломаться при пустом вызове).
  • Используйте встроенную функцию max(args), чтобы найти и вернуть самое большое число в переданной последовательности.
10 баллов

Если *args собирает в кортеж позиционные аргументы, логично предположить, что должен существовать механизм для сбора произвольного количества именованных (keyword) аргументов. И такой механизм действительно есть — это оператор двойной звездочки. В сигнатуре функции он традиционно обозначается как **kwargs (от keyword arguments). Обратимся к заданию 2.1 'Гибкая функция-логгер' из архива курса. В задаче требовалось создать функцию, которая может принимать метаданные в любом количестве и с любыми ключами (например, user="admin", timestamp="12:00", ip="127.0.0.1"). Если вы определяете функцию как def custom_log(message, **kwargs):, интерпретатор Python выполняет следующее: он берет первый позиционный аргумент и отдает его параметру message. А затем он берет абсолютно все именованные аргументы, переданные при вызове, которые не совпали ни с одним явно указанным параметром, и упаковывает их. Но на этот раз он упаковывает их не в кортеж, а в Словарь (Dictionary). Имена переданных аргументов становятся строковыми ключами этого словаря, а переданные значения — значениями словаря. Внутри функции переменная kwargs — это самый обычный словарь, и к нему применимы все словарные методы, такие как .items(), .keys() и .get(). Это основа многих современных веб-фреймворков (например, Django и Flask), где разработчики создают функции-обработчики, способные принимать гибкие HTML-атрибуты или параметры URL-запросов. Вы можете комбинировать все типы параметров в одной функции, создавая универсальные функции-обертки (декораторы, которые мы будем изучать позже). Однако в этом случае вы обязаны строго соблюдать порядок параметров в сигнатуре: сначала идут стандартные позиционные параметры, затем параметр со звездочкой (*args), затем параметры по умолчанию (если нужны, хотя часто они переносятся после args как keyword-only параметры), и в самом конце — параметр с двойной звездочкой (**kwargs). Сигнатура def master_func(a, b, *args, **kwargs): является хрестоматийной для создания функций, способных поглотить абсолютно любой ввод данных без возникновения синтаксических ошибок.

python
# Идеальный пример из архива курса (Задание 2.1)
def custom_log(message, *args, **kwargs):
    print(f"[LOG]: {message}")
    
    if args:
        # args - это кортеж. Распаковываем его для вывода
        print(f"Details: {', '.join(map(str, args))}")
        
    if kwargs:
        # kwargs - это словарь. Итерируемся по ключам и значениям
        print("Meta:")
        for k, v in kwargs.items():
            print(f"  - {k}: {v}")

# Тестовый вызов с позиционными и именованными аргументами
custom_log("System Error", 404, "Not Found", user="admin", timestamp="12:00")

Создавая функции, вы неизбежно сталкиваетесь с концепцией Области видимости (Scope). Это одна из самых сложных тем для понимания, потому что она невидима глазу. Область видимости определяет, какие переменные доступны интерпретатору для чтения или изменения в конкретный момент выполнения кода. В Python работает строгое правило разрешения имен (поиск переменной в памяти), которое называется аббревиатурой LEGB: Local (Локальная), Enclosing (Охватывающая), Global (Глобальная) и Built-in (Встроенная). Как только вы определяете переменную внутри тела функции (например, result = 100 внутри функции), эта переменная становится Локальной. Она рождается в момент вызова функции и полностью уничтожается сборщиком мусора (Garbage Collector) в ту же микросекунду, когда функция завершает свою работу через return. Вы физически не можете обратиться к переменной result из основной части программы (глобальной области) — программа выдаст ошибку NameError. Функции в Python подобны полупроницаемым зеркалам. Находясь внутри функции (в локальной области), код 'видит' переменные, созданные снаружи в глобальной области видимости. Функция может читать глобальные переменные. Но если вы попытаетесь изменить глобальную переменную внутри функции обычным оператором присваивания (например, counter = counter + 1), Python не изменит глобальную переменную. Вместо этого он создаст новую локальную переменную с точно таким же именем (это называется Shadowing - затенение переменной), которая перекроет доступ к глобальной. Чтобы явно сообщить интерпретатору, что вы хотите модифицировать именно внешнюю глобальную переменную, необходимо использовать ключевое слово global в начале функции: global counter. Однако в современной профессиональной разработке использование ключевого слова global считается серьезным нарушением архитектуры (анти-паттерном), так как функции перестают быть независимыми, изолированными 'черными ящиками' и начинают непредсказуемо изменять глобальное состояние программы, что приводит к багам. Идеальная функция должна опираться только на переданные ей аргументы и отдавать результат только через return.

Какое правило аббревиатуры описывает порядок, в котором интерпретатор Python ищет переменные в областях видимости?

Как называется тип словаря, в который интерпретатор собирает произвольные именованные аргументы внутри функции (переменная с двойной звездочкой)?

Завершая тему создания собственных функций на уровне Intermediate, мы обязаны поговорить о профессиональной культуре написания кода — о документации. Создание блестяще работающей функции теряет всякий смысл, если другие разработчики (или вы сами спустя полгода) не можете понять, что она делает, какие аргументы принимает и что возвращает, без детального разбора исходного кода. Для решения этой проблемы в Python встроен механизм Строк документации (Docstrings), который регулируется стандартом PEP 257. В отличие от обычных комментариев, которые начинаются с символа решетки (#) и игнорируются интерпретатором, Docstring — это многострочная строка, заключенная в тройные двойные кавычки ("""Текст"""), которая располагается строго на первой строке внутри тела функции, сразу под словом def. Уникальность строк документации заключается в том, что интерпретатор Python парсит этот текст во время компиляции и сохраняет его как строку внутри специального магического атрибута объекта функции, который называется __doc__. Благодаря этому свойству, встроенные инструменты языка, такие как функция help(имя_функции), или всплывающие подсказки в современных IDE (PyCharm, VS Code) могут динамически извлекать эту документацию и показывать ее программисту прямо в месте вызова функции, избавляя от необходимости искать исходный код. Качественный Docstring должен отвечать на три вопроса: Краткое описание действия (Глагол), Описание параметров (типы и суть), Описание возвращаемого значения. На уровне Intermediate (и далее в Advanced) эта практика дополняется Type Hints (Аннотациями типов, PEP 484). Хотя Python является динамически типизированным языком, вы можете использовать синтаксис def process(price: float, name: str) -> bool: для визуальной подсказки разработчикам и статическим анализаторам кода (таким как mypy). Аннотации типов не заставляют Python проверять типы на лету (программа не упадет, если вы передадите число вместо строки), но они колоссально повышают надежность архитектуры, позволяя отлавливать логические несоответствия еще на этапе написания кода в редакторе.

python
def calculate_discount(price: float, discount_percent: float) -> float:
    """
    Вычисляет итоговую цену товара после применения скидки.
    
    Аргументы:
        price (float): Исходная стоимость товара (должна быть > 0).
        discount_percent (float): Процент скидки от 0 до 100.
        
    Возвращает:
        float: Итоговая стоимость с учетом скидки.
        Если скидка 100%, возвращает 0.0.
    """
    if price < 0 or not (0 <= discount_percent <= 100):
        raise ValueError("Некорректные входные данные")
        
    discount_amount = price * (discount_percent / 100)
    return price - discount_amount

# Выведет красиво оформленную документацию в консоль
help(calculate_discount)

В каком специальном (магическом) атрибуте объекта функции интерпретатор Python сохраняет строку документации (Docstring)?

Напишите имя встроенной функции Python, которая выводит в консоль документацию (Docstring) любого объекта или функции. (Одно слово)

Задание

Финальное практическое задание (Project-Based Learning). Создайте полноценную профессиональную функцию обработки данных.

  • Создайте функцию `process_users_data`, используя Type Hints: она должна принимать список словарей и возвращать список строк.
  • Напишите правильный PEP 257 Docstring (тройные кавычки), объясняющий параметры и возвращаемое значение.
  • Используйте паттерн 'Early Return' в начале функции: если переданный список пуст (if not users_list:), верните пустой список [], чтобы не тратить ресурсы на цикл.
  • Используйте генератор списков (List Comprehension из архива), чтобы извлечь из словарей имена активных пользователей, и верните этот новый список через return.
10 баллов