Функции в Python: аргументы, дефолты и возврат нескольких значений
Изменяемый объект в значении по умолчанию накапливает данные между вызовами, а «возврат нескольких значений» на деле возвращает один кортеж. Разбираем, как аргументы работают на самом деле.

Функция, которая помнит предыдущие вызовы, хотя вы её об этом не просили. Список, растущий сам по себе. Порядок аргументов, из-за которого вызов молча делает не то. Всё это — не баги Python, а следствие того, как язык обходится с аргументами и значениями по умолчанию. Разберём поведение на конкретных примерах для Python 3.11+ и покажем, где очевидное решение стреляет в ногу.
Статья про механику передачи аргументов и про то, что возвращает return a, b. Аннотации типов и декораторы — отдельная тема, здесь их не касаемся.
Позиционные и именованные аргументы
Один и тот же аргумент можно передать двумя способами: по позиции и по имени. Это не два разных вида параметров, а два способа вызова.
def send(message, retries, timeout):
return f"{message} / retries={retries} / timeout={timeout}"
# по позиции — порядок решает всё
send("ping", 3, 10)
# по имени — порядок не важен, читается яснее
send("ping", timeout=10, retries=3)
Проблема позиционного вызова в том, что send("ping", 3, 10) и send("ping", 10, 3) — обе строки валидны, но делают разное. Ошибку не поймает ни интерпретатор, ни линтер. При трёх и более параметрах-числах именованный вызов почти всегда честнее.
Как заставить вызывать по имени
Начиная с Python 3.0 можно принудительно запретить позиционную передачу. Всё, что идёт после * в сигнатуре, передаётся только по имени.
def send(message, *, retries=3, timeout=10):
return f"{message} / retries={retries} / timeout={timeout}"
send("ping", timeout=5) # ок
send("ping", 5) # TypeError: takes 1 positional argument
Зеркальный механизм — символ / (появился в Python 3.8): параметры до него можно передавать только по позиции. Полезно, когда имя параметра — часть реализации, а не публичного контракта.
Значения по умолчанию: главная ловушка
Значение по умолчанию вычисляется один раз — в момент определения функции, а не при каждом вызове. Для чисел и строк это незаметно. Для изменяемого объекта — источник трудноуловимых багов.
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item("яблоко")) # ['яблоко']
print(add_item("груша")) # ['яблоко', 'груша'] — не то, что ждали
Список basket создался единожды и живёт между вызовами. Второй вызов дописал в тот же самый объект. То же произойдёт с dict, set и любым другим mutable-значением по умолчанию.
Правило простое: значением по умолчанию не должен быть изменяемый объект. Если нужен пустой список или словарь — ставьте None и создавайте объект внутри функции.
def add_item(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
print(add_item("яблоко")) # ['яблоко']
print(add_item("груша")) # ['груша'] — новый список каждый раз
Проверять именно через is None, а не if not basket: пустой переданный список тоже ложен в булевом контексте, и вы затрёте осмысленный аргумент.
*args и **kwargs: сбор лишних аргументов
*args собирает лишние позиционные аргументы в кортеж, **kwargs — лишние именованные в словарь. Имена args и kwargs — просто соглашение; работают звёздочки, а не имена.
def log(level, *messages, **fields):
print(level, messages, fields)
log("INFO", "start", "loaded", user="anna", ip="10.0.0.1")
# INFO ('start', 'loaded') {'user': 'anna', 'ip': '10.0.0.1'}
Обратная операция — распаковка. Одна звёздочка разворачивает последовательность в позиционные аргументы, две — словарь в именованные.
params = {"retries": 5, "timeout": 15}
send("ping", **params) # эквивалент send("ping", retries=5, timeout=15)
Порядок параметров в сигнатуре
Порядок жёсткий, нарушите — SyntaxError ещё на этапе разбора файла:
- обычные позиционные;
/— граница только-позиционных (необязательна);- параметры со значениями по умолчанию;
*argsили одиночная*;- только-именованные параметры;
**kwargs— всегда последним.
Возврат нескольких значений — это кортеж
Формально Python возвращает из функции ровно один объект. «Несколько значений» — синтаксический сахар: перечисление через запятую собирается в кортеж.
def divide(a, b):
return a // b, a % b # это один кортеж (частное, остаток)
result = divide(17, 5)
print(result) # (3, 2)
print(type(result)) #
quotient, remainder = divide(17, 5) # распаковка обратно
Когда возвращаемых значений больше двух-трёх или их смысл не очевиден из позиции, кортеж превращается в загадку: process()[2] ничего не говорит читателю. Здесь лучше именованный результат.
Named tuple и dataclass вместо голого кортежа
Два способа дать полям имена, не теряя распаковку:
from typing import NamedTuple
class DivResult(NamedTuple):
quotient: int
remainder: int
def divide(a, b):
return DivResult(a // b, a % b)
res = divide(17, 5)
print(res.quotient) # 3 — доступ по имени
q, r = res # и распаковка по-прежнему работает
Сравнение подходов к возврату нескольких значений:
| Подход | Плюс | Минус | Когда брать |
|---|---|---|---|
Кортеж return a, b | Ничего писать не надо | Позиции без имён, легко перепутать | 2 значения, смысл очевиден |
NamedTuple | Имена + распаковка, неизменяемый | Нужен отдельный класс | 3+ значений, читаемость важна |
dict | Гибко, легко расширять | Опечатка в ключе — KeyError в рантайме | Набор полей меняется динамически |
@dataclass | Методы, значения по умолчанию, мутабельность | Тяжелее кортежа | Результат — полноценный объект с логикой |
Аргумент передаётся по ссылке на объект
Python не копирует объект при передаче в функцию и не передаёт его «по значению» в привычном смысле. Функция получает ссылку на тот же объект. Отсюда — практическое следствие: изменяемый аргумент можно случайно испортить.
def normalize(values):
values.sort() # меняет переданный список на месте
return values
data = [3, 1, 2]
normalize(data)
print(data) # [1, 2, 3] — исходный список тоже отсортирован
Если функция не должна трогать вход, копируйте явно: values = list(values) внутри функции или sorted(values) вместо .sort(). Для чисел, строк и кортежей проблемы нет — они неизменяемы, изменить их «на месте» нельзя.
Что проверить в своём коде
- Пройдитесь по сигнатурам — нет ли
=[],={}или=set()в значениях по умолчанию. Замените наNone. - Для функций с тремя и более числовыми параметрами добавьте
*, чтобы вызывающий указывал имена. - Если возвращаете больше двух значений — оцените, не пора ли завести
NamedTuple. - Проверьте функции, принимающие списки и словари: не мутируют ли они вход незаметно для вызывающего.
Дальше стоит посмотреть в сторону аннотаций типов и mypy — они ловят часть ошибок с аргументами ещё до запуска. Официальный раздел про определение функций — в туториале на python.org.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: Python Tutorial — Defining Functions, Python Reference — Function definitions
Частые вопросы
А если у меня Python 3.7 — сработает ли синтаксис с одиночной звёздочкой?
Почему нельзя просто написать basket=[] и не мучиться с None?
Можно ли смешивать *args и **kwargs в одной функции?
return a, b возвращает список или кортеж?
Если функция изменила переданный список, а мне этого не надо — что делать?
Как передать словарь настроек в функцию как отдельные аргументы?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.