Функции: positional, keyword, default — и mutable default trap
После двух ULTRA-DEEP модулей (M01 — primitives, M02 — структуры данных) мы переходим к функциям. Базовый синтаксис def знаком всем; его описание уложится в 5 минут. Но один pitfall — mutable default argument trap — настолько частый и настолько неочевидный, что ему посвящена половина урока.
Если вы запомните только одну вещь из этого урока — пусть это будет правило: никогда не пишите def f(x, target=[]):.
def синтаксис: positional / keyword / default
Базовая форма:
def f(a, b=10, c=20):
return a + b + c
a— positional (обязательный): передаётся по позиции.b,c— default: имеют значение по умолчанию, можно не передавать.
Вызовы:
f(1) # 1 + 10 + 20 = 31 (b и c — defaults)
f(1, 2) # 1 + 2 + 20 = 23 (b передан, c — default)
f(1, c=5) # 1 + 10 + 5 = 16 (b — default, c — keyword)
f(a=1, b=2, c=3) # 1 + 2 + 3 = 6 (все keyword)
Правило порядка в signature: все positional аргументы (без default) идут до default. То есть def f(a=1, b) — SyntaxError.
Правило порядка в call: positional аргументы идут до keyword. f(1, b=2) — OK; f(b=2, 1) — SyntaxError.
PEP 570 — positional-only / PEP 3102 — keyword-only
Python 3.8+ позволяет жёстко разделить, какие параметры могут передаваться только позиционно, а какие — только по имени:
def f(pos1, pos2, /, both, *, kw1, kw2):
...
- Всё до
/— positional-only (нельзя передавать по имени). - Всё между
/и*— обычные параметры (можно по позиции или по имени). - Всё после
*— keyword-only.
Примеры:
def f(pos1, pos2, /, both, *, kw1, kw2):
print(pos1, pos2, both, kw1, kw2)
f(1, 2, 3, kw1=4, kw2=5) # OK: pos1=1, pos2=2, both=3
f(1, 2, both=3, kw1=4, kw2=5) # OK: both — обычный, можно по имени
f(pos1=1, pos2=2, both=3, kw1=4, kw2=5)
# TypeError: f() got some positional-only arguments passed as keyword arguments: 'pos1, pos2'
f(1, 2, 3, 4, 5)
# TypeError: f() takes 3 positional arguments but 5 were given
# (kw1, kw2 — keyword-only, нельзя передать по позиции)
Зачем positional-only? Для API stability: переименование параметра pos1 не сломает вызывающих, потому что они не могут передавать его по имени. Это используется во встроенных функциях: len(obj, /), pow(base, exp, mod=None, /). См. PEP 570.
Зачем keyword-only? Для self-documenting вызовов: subprocess.run(cmd, *, check=True, timeout=60) — check и timeout обязаны передаваться по имени, что делает call-site читаемым. См. PEP 3102.
В новом коде используйте * для разделения позиционных и keyword-only — это улучшает читаемость и устойчивость API. Особенно для booleans: f(data, *, verbose=False) лучше, чем f(data, False).
Mutable default argument trap
Один из самых вредных footguns в Python. Прочитайте до конца — этот pitfall настигает даже опытных разработчиков.
Симптом: функция с def f(x, target=[]): ведёт себя так, будто между вызовами помнит state.
def append_to(item, target=[]): # WRONG
target.append(item)
return target
print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] — почему не [2]?!
print(append_to(3)) # [1, 2, 3] — список «накапливается»
Корень проблемы: default-значения evaluated один раз — at function-definition time, не при каждом вызове. Они хранятся в самом function-объекте, в поле __defaults__:
def append_to(item, target=[]):
target.append(item)
return target
print(append_to.__defaults__) # ([],) — tuple с одним пустым list
append_to(1)
print(append_to.__defaults__) # ([1],) — тот же list, теперь не пустой
Все вызовы без явного target получают один и тот же list. Это не bug — это документированное поведение CPython, прямое следствие того, что function — это объект, и default-tuple хранится в нём.
См. Objects/funcobject.c — структура PyFunctionObject, поля func_defaults (positional defaults) и func_kwdefaults (keyword-only defaults).
Правильное решение — sentinel pattern
Идиома: использовать None (или другой sentinel) как default, и создавать mutable объект внутри функции:
def append_to(item, target=None):
if target is None:
target = []
target.append(item)
return target
print(append_to(1)) # [1]
print(append_to(2)) # [2]
print(append_to(3)) # [3]
Каждый вызов без явного target создаёт свежий list — как и ожидается.
Почему is None, а не == None? Это identity check (см. M01 урок 01: is — id(x) == id(y), == — вызов __eq__). None — singleton, и is None — каноничная проверка sentinel’а. Кроме того, == может быть переопределён в кастомных классах, что приведёт к surprise (например, NumPy array возвращает массив bool из ==).
Альтернативные sentinels: для случаев, где None — валидное значение аргумента, используют _sentinel = object():
_MISSING = object()
def lookup(key, default=_MISSING):
val = data.get(key)
if val is None and default is _MISSING:
raise KeyError(key)
return val if val is not None else default
object() — уникальный объект, никогда не равный ничему другому по is. Это позволяет различать «пользователь не передал default» и «пользователь передал default=None».
Когда mutable default OK?
Очень редко. Один валидный паттерн — micro-optimization memoization, использующая default как cache:
def memoized(x, _cache={}):
if x not in _cache:
_cache[x] = expensive_compute(x)
return _cache[x]
Underscore prefix (_cache) сигнализирует «private internal, не передавайте сами». Но для production используйте functools.lru_cache (см. урок 4 этого модуля) — он чище и thread-safe.
Anti-pattern: mutable default ради «default empty list». Всегда используйте None sentinel для этого.
Type hints для defaults (preview)
Modern Python coders пишут signature с type hints (PEP 526):
def append_to(item: int, target: list[int] | None = None) -> list[int]:
if target is None:
target = []
target.append(item)
return target
Type hints не enforced рантаймом (это hints для IDE / mypy / pyright), но делают signature self-documenting и помогают избежать ошибок ещё на этапе редактирования. Подробнее — в Phase 67 (typing depth dive); здесь — для контекста.
Cross-course context
Ключевые выводы
- Default values evaluated один раз at function-definition time; хранятся в
func.__defaults__tuple. - Mutable defaults shared across calls — это not bug, а documented поведение. Используйте
Nonesentinel pattern + создание объекта внутри функции. - PEP 570 (
/) — positional-only маркер; PEP 3102 (*) — keyword-only маркер. Используйте для API stability и self-documenting вызовов. is Noneвместо== None— identity check, каноничный способ проверки sentinel’а.