Произвольное число аргументов (*args и **kwargs)
Написание универсальных функций, способных принимать любое количество параметров.
Добро пожаловать в двадцать первый урок нашего курса, который станет настоящим прорывом в вашем понимании архитектуры программного обеспечения на Python. Сегодня мы переходим от жестко заданных, процедурных функций к созданию невероятно гибких, универсальных и масштабируемых интерфейсов. Тема нашего занятия — произвольное число аргументов, знаменитые конструкции *args и **kwargs. До этого момента мы писали функции, которые имели строгую, неизменную сигнатуру. Если функция def add_two_numbers(a, b): ожидала два аргумента, мы были обязаны передать ровно два. Передача трех аргументов вызывала ошибку TypeError, а передача одного — тоже приводила к аварийному завершению программы. В реальном мире, в промышленной разработке (production-ready code), такая жесткость часто становится огромной проблемой. Представьте, что вы разрабатываете системный логгер, который должен принимать произвольное количество сообщений об ошибках, метрик и метаданных. Или вы пишете математическую библиотеку, где функция вычисления среднего значения должна обрабатывать как два числа, так и выборку из миллиона значений. Как решить эту задачу стандартными средствами? Первое, что приходит в голову начинающему разработчику — использовать списки (lists) или кортежи (tuples) в качестве единственного аргумента. Да, вы можете написать def calculate_average(numbers_list): и передавать туда список. Но это нарушает один из главных принципов Дзена Python — «Красивое лучше, чем уродливое». Пользователю вашей функции придется каждый раз вручную оборачивать свои данные в квадратные скобки. Это лишние действия, лишний синтаксический шум и ухудшение читаемости кода (readability). Python предлагает элегантное, мощное и истинно «питонячье» (Pythonic way) решение этой проблемы — операторы упаковки и распаковки. Мы детально изучим, как один символ звездочки * или два символа ** полностью меняют механику работы интерпретатора, позволяя функции принимать бесконечное количество данных, автоматически структурируя их в кортежи и словари прямо под капотом, без малейших усилий со стороны программиста, вызывающего вашу функцию.
def rigid_sum(a, b):
return a + b
# Это вызовет TypeError: rigid_sum() takes 2 positional arguments but 3 were given
# result = rigid_sum(1, 2, 3)
def list_sum(numbers):
total = 0
for num in numbers:
total += num
return total
# Работает, но требует лишних скобок (создания списка)
result = list_sum([1, 2, 3, 4, 5])
Давайте подробно разберем первый и самый распространенный механизм работы с произвольным количеством позиционных аргументов — конструкцию *args. Сама по себе аббревиатура «args» является сокращением от английского слова «arguments» (аргументы). Важно понимать фундаментальное правило: магия заключается не в слове «args», а исключительно в символе звездочки (*), который ставится перед именем переменной. Звездочка выступает в роли оператора упаковки (packing). Когда интерпретатор Python видит в сигнатуре функции параметр со звездочкой, он понимает следующую инструкцию: «Все позиционные аргументы, которые были переданы в эту функцию и которым не хватило явно названных параметров, должны быть собраны, упакованы и помещены в единую структуру данных». И здесь возникает важнейший вопрос: в какую именно структуру данных они упаковываются? Python выбирает для этого кортеж (tuple). Почему именно кортеж, а не список (list)? Вспомните наши прошлые уроки о разнице между изменяемыми (mutable) и неизменяемыми (immutable) типами данных. Кортеж является неизменяемым. Переданные в функцию аргументы не должны случайным образом изменяться внутри этой функции, если они были собраны механизмом *args. Использование кортежа гарантирует целостность данных, обеспечивает более высокую скорость работы (кортежи быстрее списков благодаря фиксированному размеру в памяти) и защищает разработчика от случайных побочных эффектов. Вы можете назвать этот параметр как угодно: *numbers, *items, *data. Код будет работать абсолютно идентично. Однако, в сообществе Python существует строгий конвенциональный стандарт (зафиксированный в PEP 8), согласно которому принято использовать именно имя *args. Следование этому стандарту критически важно для того, чтобы другие программисты, просматривая ваш код, мгновенно понимали его логику без необходимости вчитываться в детали реализации. Когда вы пишете def my_func(*args):, вы открываете дверь для неограниченного потока данных.
def flexible_sum(*args):
# Внутри функции args - это обычный кортеж (tuple)
print(f"Тип аргумента args: {type(args)}")
print(f"Содержимое args: {args}")
total = 0
for num in args:
total += num
return total
# Мы можем передать любое количество чисел!
print(flexible_sum(1, 2, 3)) # Вернет 6
print(flexible_sum(10, 20)) # Вернет 30
print(flexible_sum(5)) # Вернет 5
print(flexible_sum()) # Вернет 0, args будет пустым кортежем ()
Какой тип данных использует Python для упаковки позиционных аргументов, переданных через оператор * (например, *args)?
Теперь давайте усложним задачу и рассмотрим, как оператор *args взаимодействует со стандартными, жестко заданными позиционными параметрами в одной сигнатуре функции. Это классический паттерн проектирования в Python. Представьте, что вы пишете функцию для отправки электронного письма. Очевидно, что у любого письма обязательно должен быть хотя бы один получатель, тема и, возможно, текст. Но что, если вы хотите иметь возможность прикреплять к письму произвольное количество файлов (вложений)? Здесь на помощь приходит комбинирование обязательных аргументов и *args. Синтаксис выглядит так: def send_email(recipient, subject, *attachments):. В этом случае интерпретатор Python будет действовать по строгим правилам позиционного сопоставления. Первое переданное значение он железобетонно присвоит переменной recipient. Второе переданное значение отправится в переменную subject. А вот всё остальное — третье, четвертое, десятое, сотое значение — будет безжалостно сгребено оператором звездочки, упаковано в единый кортеж и присвоено переменной attachments. Крайне важно понимать порядок следования параметров: обязательные позиционные аргументы всегда должны идти строго до параметра *args. Если вы попытаетесь написать def my_func(*args, mandatory_arg):, вы столкнетесь с серьезными проблемами. Python заберет все позиционные аргументы в args, и для mandatory_arg просто ничего не останется! Интерпретатор выбросит ошибку TypeError: missing 1 required keyword-only argument (мы поговорим о keyword-only аргументах чуть позже, это важный нюанс стандарта Python 3). Поэтому всегда помните правило архитектуры сигнатур: сначала идут конкретные, обязательные параметры, затем, если нужно, параметры со значениями по умолчанию, и только в самом конце позиционного блока ставится 'жадный' сборщик *args, который поглощает весь оставшийся поток данных. Это правило обеспечивает предсказуемость и надежность вашего кода в любых, даже самых сложных сценариях использования.
def process_user_data(user_id, username, *actions):
print(f"Обработка данных пользователя ID: {user_id}")
print(f"Имя пользователя: {username}")
if not actions:
print("Нет дополнительных действий для выполнения.")
return
print("Выполнение действий:")
for index, action in enumerate(actions, 1):
print(f" {index}. {action}")
# Вызов только с обязательными аргументами
process_user_data(101, "Alex_Dev")
print("-" * 30)
# Вызов с дополнительными аргументами
process_user_data(102, "Maria_Pro", "login", "update_profile", "upload_photo")
Если сигнатура функции выглядит как `def test(a, b, *args):`, чему будет равен кортеж args, если мы вызовем функцию так: `test(1, 2)`?
Мы освоили передачу произвольного количества простых, безымянных значений. Но современная разработка требует работы с метаданными, конфигурациями и опциями, которые имеют свои имена. Если вам нужно передать в функцию набор настроек, где каждая настройка имеет ключ и значение (например, color="red", timeout=60, retries=3), использование позиционного *args становится бессмысленным. Вы получите кортеж вида ("red", 60, 3), но потеряете самое главное — информацию о том, к какому параметру относится каждое число! Для решения этой фундаментальной задачи в Python существует вторая магическая конструкция — **kwargs. Аббревиатура расшифровывается как «Keyword Arguments» (именованные аргументы). Оператор двойной звездочки ** работает по схожему принципу с одинарной, но вместо того, чтобы собирать «беспризорные» позиционные аргументы в кортеж, он собирает все «беспризорные» именованные аргументы (те, что передаются с символом равенства: key=value) и упаковывает их в мощнейшую структуру данных Python — словарь (dictionary). Имена переменных становятся ключами словаря (в виде строк), а переданные значения — значениями этого словаря. Это открывает феноменальные возможности для создания гибких конфигураторов, фабрик объектов и систем маршрутизации. Представьте, что вы пишете функцию подключения к базе данных. Базы бывают разные: PostgreSQL требует порт и пароль, SQLite требует только путь к файлу, MongoDB может требовать SSL-сертификаты. Вместо того чтобы прописывать десятки параметров со значениями по умолчанию в сигнатуре одной функции (что приведет к нечитаемому монструозному коду), вы можете просто использовать def connect_to_db(db_type, **kwargs):. Внутри функции вы сможете динамически проверять наличие нужных ключей в словаре kwargs с помощью метода .get() или оператора in, обеспечивая элегантную и невероятно гибкую логику обработки параметров соединения. И снова, как и с *args, имя kwargs является строгим стандартом де-факто, который ожидают увидеть все Python-разработчики мира.
def create_html_tag(tag_name, text, **kwargs):
# kwargs собирает все именованные аргументы в словарь
# Например, если мы передадим class_name="btn", id="submit"
# kwargs будет равен {'class_name': 'btn', 'id': 'submit'}
attributes = ""
for key, value in kwargs.items():
# Формируем строку HTML атрибутов
# Обратите внимание: в Python class - зарезервированное слово,
# поэтому часто используют class_name или cls, заменяя их при рендеринге
html_key = "class" if key == "class_name" else key
attributes += f' {html_key}="{value}"'
return f"<{tag_name}{attributes}>{text}</{tag_name}>"
# Практическое использование:
button = create_html_tag("button", "Отправить", class_name="btn-primary", id="submit-btn", disabled="true")
print(button)
# Вывод: <button class="btn-primary" id="submit-btn" disabled="true">Отправить</button>
Флеш-карточки
Что собирает оператор * (одна звездочка) в сигнатуре функции?
Нажмите, чтобы увидеть ответ
Собирает все неименованные (позиционные) аргументы и упаковывает их в кортеж (tuple).
Нажмите, чтобы вернуться
Что собирает оператор ** (две звездочки) в сигнатуре функции?
Нажмите, чтобы увидеть ответ
Собирает все именованные аргументы (keyword arguments) и упаковывает их в словарь (dict).
Нажмите, чтобы вернуться
Каков правильный порядок аргументов в сложной сигнатуре Python?
Нажмите, чтобы увидеть ответ
1. Обязательные позиционные 2. *args 3. Обязательные именованные (keyword-only) 4. **kwargs
Нажмите, чтобы вернуться
Мы подошли к моменту, где теория встречается с серьезной архитектурой. Что происходит, когда нам нужно использовать ВСЕ типы аргументов одновременно? Это не просто возможно, это стандартный паттерн написания универсальных оберток (wrappers) и декораторов (decorators). В Python существует жесткий, не подлежащий обсуждению, грамматический закон, определяющий порядок следования параметров в сигнатуре функции. Нарушение этого порядка неминуемо приведет к синтаксической ошибке (SyntaxError) еще на этапе компиляции байт-кода, до фактического запуска программы. Вы должны выучить эту последовательность наизусть, словно таблицу умножения. Правильный порядок (Golden Rule of Python Signatures) выглядит следующим образом: 1. Стандартные позиционные аргументы (например, a, b). 2. Стандартные аргументы со значениями по умолчанию (например, c=10). 3. Сборщик позиционных аргументов (*args). 4. Keyword-only аргументы (появились в Python 3, мы их подробно рассмотрим). 5. Сборщик именованных аргументов (**kwargs). Почему порядок именно такой? Представьте себя на месте интерпретатора. Когда вы вызываете функцию func(1, 2, 3, x=4, y=5), интерпретатор идет слева направо. Сначала он обязан раздать позиционные значения. Если он встречает конкретное имя (позиционный аргумент), он отдает значение ему. Затем, когда имена заканчиваются, включается жадный пылесос *args и забирает остатки чисел. После этого начинаются именованные параметры. Конкретные имена получают свои значения, а неизвестные, произвольные ключи всасываются мощным насосом **kwargs. Если бы **kwargs стоял перед *args, возникла бы логическая коллизия: как интерпретатор должен был бы понимать, где заканчиваются словари и начинаются кортежи? Именно поэтому **kwargs всегда, при любых обстоятельствах, является самым последним элементом в определении любой функции. Это фундаментальное ограничение синтаксиса, гарантирующее однозначность парсинга вашего кода.
# Идеальная универсальная сигнатура функции (The Ultimate Signature)
def master_function(a, b, *args, **kwargs):
print(f"Обязательный a: {a}")
print(f"Обязательный b: {b}")
print(f"Дополнительные позиционные args: {args}")
print(f"Дополнительные именованные kwargs: {kwargs}")
print("-" * 40)
# Различные варианты вызова:
# 1. Только обязательные
master_function(10, 20)
# 2. Обязательные + args
master_function(10, 20, 30, 40, 50)
# 3. Обязательные + kwargs
master_function(1, 2, name="Alice", role="Admin")
# 4. Полный комплект!
master_function("First", "Second", "Third", 4.0, True, user="Bob", status=200)
Какая из следующих сигнатур функций синтаксически КОРРЕКТНА в Python?
Прежде чем двигаться дальше, давайте сделаем небольшое отступление и рассмотрим важнейшее нововведение стандарта Python 3 — параметры Keyword-Only (только именованные аргументы). Это знание критически необходимо для написания надежных, защищенных от ошибок API-интерфейсов и библиотек. Представьте функцию создания пользователя: def create_user(username, password, is_admin=False):. Разработчик может вызвать её так: create_user("john", "12345", True). Проблема в том, что аргумент True здесь передается позиционно. Читая код create_user("john", "12345", True) через полгода, вы или ваш коллега можете не понять, что означает это True. Это активация аккаунта? Это статус премиум-пользователя? Это предоставление админских прав? Чтобы избежать таких катастрофических ошибок в безопасности и логике, мы можем принудительно заставить программиста явно указывать имя аргумента при вызове. Для этого в сигнатуре функции используется одиночная звездочка * БЕЗ имени переменной. Выглядит это так: def create_user(username, password, *, is_admin=False):. Эта одиночная звездочка работает как шлагбаум. Она говорит интерпретатору: «Все позиционные аргументы закончились здесь. Любые параметры, идущие после этой звездочки, могут быть переданы ТОЛЬКО по их имени». Если вы теперь попытаетесь вызвать create_user("john", "12345", True), программа упадет с ошибкой TypeError: create_user() takes 2 positional arguments but 3 were given. Единственный легальный способ вызова теперь: create_user("john", "12345", is_admin=True). Это многократно повышает читаемость (явное лучше, чем неявное — вспоминаем Дзен) и безопасность вашего кода. Более того, если в вашей функции уже используется *args, то все параметры, стоящие МЕЖДУ *args и **kwargs, автоматически становятся keyword-only. Например, в def process(*args, delay=0):, параметр delay невозможно передать позиционно, его можно указать только как delay=5. Это изящный способ совместить гибкость и строгий контроль поведения функции.
# Пример использования Keyword-Only аргументов для повышения безопасности
def delete_database(db_name, *, force_drop=False, backup=True):
print(f"Инициация удаления БД: {db_name}")
if not force_drop:
print("Ошибка: требуется явное подтверждение (force_drop=True)")
return
if backup:
print("Создание резервной копии перед удалением...")
print("УДАЛЕНИЕ ЗАВЕРШЕНО.")
# Это вызовет ошибку TypeError (мы пытаемся передать True позиционно)
# delete_database("production_db", True)
# Единственный верный способ вызова - предельно читаемый код:
delete_database("production_db", force_drop=True, backup=False)
До сих пор мы рассматривали магию звездочек (упаковку) исключительно со стороны создания функции — то есть при описании ее сигнатуры через def. Но это лишь половина картины! Операторы * и ** обладают невероятной мощью и на другой стороне баррикад — при вызове функции. Здесь этот процесс называется распаковкой (Unpacking). Давайте представим жизненную ситуацию: у вас есть функция def draw_point(x, y, z):, которая ожидает ровно три координаты. А ваши данные пришли из базы данных, внешнего API или математического расчета в виде списка: coordinates = [10, 20, 30]. Как передать эти данные в функцию? Неопытный разработчик (Junior) начнет писать монструозный код: draw_point(coordinates[0], coordinates[1], coordinates[2]). Это нарушает принцип DRY (Don't Repeat Yourself), выглядит ужасно и не масштабируется, если координат станет десять. Продвинутый (Intermediate) разработчик использует оператор распаковки. Вы можете просто написать draw_point(*coordinates). В момент вызова интерпретатор берет ваш список (или кортеж, или множество, любой итерируемый объект), «разрывает» его на отдельные элементы и аккуратно раскладывает их по позиционным аргументам функции, как если бы вы ввели их вручную через запятую. Аналогичная, но еще более мощная магия работает со словарями и оператором **. Если у вас есть функция def configure_server(host, port, user): и конфигурационный словарь config = {"host": "localhost", "port": 8080, "user": "admin"}, вам достаточно вызвать configure_server(**config). Python возьмет ключи словаря, сопоставит их с именами аргументов функции, и передаст соответствующие значения. Главное правило: ключи словаря должны быть строками (strings) и должны в точности, символ в символ, совпадать с именами параметров в сигнатуре функции. Если в словаре окажется лишний ключ или не хватит обязательного ключа, вы получите TypeError. Этот механизм является абсолютным стандартом при написании современных фреймворков (таких как Django, FastAPI, Pandas), где данные постоянно перетекают из JSON-структур напрямую в аргументы функций-обработчиков.
def print_vector_3d(x, y, z):
print(f"Вектор в пространстве: X={x}, Y={y}, Z={z}")
# 1. Распаковка списка (или кортежа)
my_list = [100, 200, 300]
# Вместо print_vector_3d(my_list[0], my_list[1], my_list[2])
print_vector_3d(*my_list) # Звездочка распаковывает список!
# 2. Распаковка словаря
def register_service(name, protocol, port):
print(f"Сервис {name} запущен на {protocol}://localhost:{port}")
config_dict = {
"protocol": "https",
"port": 443,
"name": "AuthService"
}
# Ключи словаря сопоставляются с именами параметров!
# Порядок ключей в словаре не имеет значения
register_service(**config_dict)
Задание
Проектное задание: Гибкий парсер настроек. Ваша цель - создать функцию, которая сможет объединять дефолтные настройки системы с пользовательскими, используя распаковку словарей. Это имитация реальной задачи из веб-разработки (PBL - Project-Based Learning).
- Создайте словарь `default_settings` с ключами: 'theme': 'light', 'language': 'ru', 'notifications': True.
- Напишите функцию `apply_settings(**user_kwargs)`, которая принимает произвольные пользовательские настройки.
- Внутри функции создайте новый словарь `final_settings`, используя оператор распаковки: объедините распакованные `default_settings` и `user_kwargs`. (Подсказка в Python 3.5+ можно писать `**dict1, **dict2`).
- Верните итоговый словарь и протестируйте функцию, передав `theme='dark'`.
Мы рассмотрели базовые механизмы, но где же *args и **kwargs раскрывают свою истинную, абсолютную мощь? Ответ кроется в паттернах проектирования, и самый яркий из них — Декораторы (Decorators). Декоратор — это функция, которая принимает другую функцию, расширяет ее поведение (например, добавляет логирование, проверку прав доступа или измерение времени выполнения), и возвращает новую функцию. Проблема в том, что когда вы пишете универсальный декоратор (например, декоратор @timer, который замеряет время), вы заранее не знаете, к какой именно функции он будет применен. Сегодня вы примените его к функции без аргументов def ping(), а завтра — к функции с десятью параметрами def calculate_matrix(a, b, c, mode, precision, ...). Как написать функцию-обертку (wrapper) внутри декоратора, которая гарантированно сможет принять и передать дальше ЛЮБОЙ набор аргументов? Именно здесь связка *args, **kwargs спасает положение. Вы определяете внутреннюю функцию как def wrapper(*args, **kwargs):. Она жадно собирает абсолютно всё, что передал пользователь при вызове. Затем она выполняет свою полезную нагрузку (например, засекает время start = time.time()), а после этого делает самое важное: вызывает оригинальную функцию, распаковывая обратно всё, что собрала: result = original_func(*args, **kwargs). Таким образом, аргументы прозрачно проходят сквозь декоратор, как свет сквозь стекло, не искажаясь и не теряясь. Это называется паттерном «Проксирование аргументов» (Argument Proxying). Без глубокого понимания этой концепции вы не сможете написать ни одного серьезного веб-приложения на Flask или FastAPI, так как там вся маршрутизация и middleware построены на декораторах, проксирующих запросы с помощью универсальных сигнатур.
import time
# Классический пример: Декоратор для замера времени выполнения
def execution_timer(func):
# Универсальная обертка, принимающая ЛЮБЫЕ аргументы
def wrapper(*args, **kwargs):
start_time = time.time()
# ПРОКСИРОВАНИЕ: передаем распакованные аргументы в оригинальную функцию
result = func(*args, **kwargs)
end_time = time.time()
print(f"[LOG] Функция {func.__name__} выполнена за {end_time - start_time:.4f} сек.")
return result
return wrapper
# Применяем декоратор к функции с разными сигнатурами
@execution_timer
def do_nothing():
time.sleep(0.5)
@execution_timer
def multiply(a, b, exponent=1):
time.sleep(0.2)
return (a * b) ** exponent
# Декоратор успешно обрабатывает вызовы с любым количеством аргументов!
do_nothing()
print(multiply(5, 4, exponent=2))
Почему в функции-обертке (wrapper) внутри декораторов повсеместно используется сигнатура `def wrapper(*args, **kwargs):`?
Второе место по важности, где *args и **kwargs играют критическую роль, — это Объектно-Ориентированное Программирование (ООП), а именно процесс Наследования (Inheritance) и вызов конструкторов базовых классов. Представьте, что вы разрабатываете иерархию классов для видеоигры. У вас есть базовый класс Character (Персонаж), который принимает имя и здоровье в свой магический метод инициализации __init__. Затем вы создаете дочерний класс Warrior (Воин), который расширяет базовый класс и добавляет новый атрибут — тип брони (armor_type). Внутри __init__ воина вы должны вызвать __init__ базового персонажа с помощью функции super(), чтобы тот установил имя и здоровье. Если вы будете жестко прописывать все аргументы super().__init__(name, health), то при добавлении в будущем нового свойства базовому классу (например, уровня маны), вам придется переписывать конструкторы ВСЕХ дочерних классов (Воина, Мага, Лучника). Это нарушает принцип открытости/закрытости (Open/Closed Principle) из SOLID. Как решают эту проблему Senior-разработчики? Они проектируют классы так, чтобы они забирали только те аргументы, которые нужны лично им, а все остальные «неизвестные» аргументы упаковывали в *args и **kwargs и пробрасывали наверх по цепочке наследования через super().__init__(*args, **kwargs). Такой подход называется «Кооперативное множественное наследование» (Cooperative Multiple Inheritance). Это обеспечивает потрясающую расширяемость системы. Вы можете менять логику базового класса, добавлять новые параметры, и дочерние классы автоматически пропустят их через себя, даже не зная об их существовании. В крупных фреймворках, вроде Django REST Framework, создание собственных View или Serializer практически полностью опирается на этот паттерн переопределения методов с обязательным пробросом *args, **kwargs.
class Character:
def __init__(self, name, health, **kwargs):
self.name = name
self.health = health
# Обрабатываем любые дополнительные свойства базового уровня
self.is_npc = kwargs.get('is_npc', False)
print(f"[Character] Инициализирован {self.name} (Здоровье: {self.health})")
class Warrior(Character):
# Мы явно запрашиваем armor_type, а всё остальное пакуем в args/kwargs
def __init__(self, armor_type, *args, **kwargs):
self.armor_type = armor_type
# Пробрасываем все остальные аргументы родительскому классу
super().__init__(*args, **kwargs)
print(f"[Warrior] Одет в броню: {self.armor_type}")
# Создаем воина, передавая параметры и для Warrior, и для Character
# 'armor_type' заберет Warrior, остальное уйдет в Character
conan = Warrior("Тяжелая броня", name="Конан", health=150, is_npc=False)
Давайте разберем еще одну интересную особенность, о которой часто не упоминают в базовых курсах. С версии Python 3.5 (согласно документу PEP 448) операторы распаковки * и ** получили невероятное расширение своих возможностей. Их назвали Дополнительными обобщениями распаковки (Additional Unpacking Generalizations). До Python 3.5 вы могли использовать распаковку списка только один раз в вызове функции. Если у вас было два списка, вам нужно было сначала склеить их через оператор сложения (list1 + list2), а затем распаковывать результат. Начиная с 3.5, вы можете использовать звездочки где угодно и сколько угодно раз! Например, вы можете создать новый список, смешивая отдельные элементы и распакованные старые списки прямо на лету: new_list = [1, 2, *old_list1, 5, *old_list2, 9]. Это работает потрясающе быстро на уровне интерпретатора CPython. То же самое касается словарей. Раньше для слияния двух словарей (merge dictionaries) приходилось писать функцию обновления или использовать метод .update(), что изменяло оригинальный словарь или требовало нескольких строк кода. С PEP 448 вы можете создать новый словарь буквально в одну строку: merged_dict = {**dict1, **dict2}. А если в обоих словарях есть одинаковые ключи? Правило простое и элегантное: побеждает тот, кто стоит правее. Значения из dict2 перезапишут значения из dict1. Более того, вы можете добавлять новые ключи прямо в процессе распаковки: new_config = {"version": 2.0, **old_config, "debug_mode": True}. Это настолько мощный, лаконичный и читаемый инструмент, что он практически полностью вытеснил старые методы работы с коллекциями в современных проектах. Понимание и использование этих синтаксических конструкций — верный признак того, что вы перешли из категории новичков в категорию уверенных Python-разработчиков (Intermediate/Middle level).
# PEP 448 в действии: Множественная распаковка (Python 3.5+)
# 1. Слияние списков на лету
frontend_devs = ["Alice", "Bob"]
backend_devs = ["Charlie", "Dave"]
# Создаем команду, добавляя лида и распаковывая два списка
full_team = ["Eve (TeamLead)", *frontend_devs, *backend_devs]
print("Команда:", full_team)
# 2. Элегантное слияние словарей
default_config = {"host": "0.0.0.0", "port": 8000, "timeout": 30}
user_config = {"port": 9090, "debug": True}
# Сливаем словари. 'port' из user_config перезапишет 'port' из default_config
final_settings = {**default_config, **user_config, "environment": "production"}
print("Финальные настройки:", final_settings)
# Вывод: {'host': '0.0.0.0', 'port': 9090, 'timeout': 30, 'debug': True, 'environment': 'production'}
Что выведет следующий код? d1 = {'a': 1, 'b': 2} d2 = {'b': 3, 'c': 4} res = {**d1, **d2} print(res['b'])
Мы подходим к завершению нашего масштабного погружения. Давайте подведем итоги и систематизируем типичные ошибки, которые допускают разработчики при работе с *args и **kwargs (Антипаттерны). Ошибка №1: Изменение упакованных данных. Запомните, *args — это кортеж. Если внутри функции вы попытаетесь сделать args[0] = 100, программа рухнет с TypeError: 'tuple' object does not support item assignment. Если вам нужно изменить данные, сначала преобразуйте их в список: mutable_args = list(args). Ошибка №2: Запутывание сигнатуры. Использование *args, **kwargs делает вашу функцию «черным ящиком». Если у функции нет явных параметров, ваша среда разработки (PyCharm, VS Code) не сможет подсказать вам (autocompletion), какие аргументы туда нужно передавать. Ваш код потеряет самодокументируемость. Поэтому используйте этот паттерн только там, где он действительно нужен (декораторы, наследование, универсальные диспетчеры, логирование). В обычных функциях бизнес-логики лучше использовать явные параметры с аннотациями типов (Type Hints), например: def calculate(price: float, tax: float) -> float:. Ошибка №3: Использование изменяемых объектов по умолчанию в связке с kwargs. Это отсылка к нашему прошлому уроку (Проблема изменяемых аргументов по умолчанию). Если вы извлекаете список из kwargs: items = kwargs.get('items', []), будьте осторожны, если вы начнете мутировать этот список внутри функции — хотя в данном конкретном случае get() создает новый пустой список при каждом вызове, что безопасно, но прямое присвоение в сигнатуре def func(data=[]) по-прежнему смертельно опасно. Владение связкой упаковки и распаковки — это ваш пропуск в мир настоящего, профессионального Python-кода. Возвращайтесь к этому уроку, экспериментируйте с кодом в интерпретаторе, ломайте его и чините. Практика — ваш лучший учитель.
Задание
Финальное практическое задание. Симуляция Code Review (из архивных материалов).
- Вспомните задание из введения: нужно написать функцию `custom_log`.
- Функция должна принимать обязательный аргумент `message`.
- Функция должна принимать любые дополнительные сообщения через `*args`.
- Функция должна принимать любые метаданные (пользователь, время, IP) через `**kwargs`.
- Внутри функции выведите `message`.
- Если есть `args`, выведите их отдельной строкой, соединив через запятую.
- Если есть `kwargs`, пройдитесь по словарю циклом и выведите ключи и значения.
- Напишите тестовый вызов функции, передав ей все 3 типа аргументов.
Флеш-карточки
В чем отличие упаковки (packing) от распаковки (unpacking)?
Нажмите, чтобы увидеть ответ
Упаковка происходит при определении функции (def func(*args) собирает данные). Распаковка происходит при вызове (func(*my_list) разбирает данные на элементы).
Нажмите, чтобы вернуться
Можно ли переименовать *args и **kwargs?
Нажмите, чтобы увидеть ответ
Технически да, важны только символы * и **. Но негласный стандарт PEP 8 требует использовать именно имена args и kwargs.
Нажмите, чтобы вернуться
Что такое Keyword-only аргумент?
Нажмите, чтобы увидеть ответ
Параметр, который можно передать только по имени. В сигнатуре он располагается строго после символа * или после *args.
Нажмите, чтобы вернуться