args, kwargs, positional-only и keyword-only

Базовое объявление функций уже разобрали. Python даёт богатый набор способов объявить параметры функции: произвольное число позиционных через *args, произвольное число именованных через **kwargs, специальные ограничения через / и *. Это позволяет писать гибкие API: от тривиальных до фреймворочных. В этом уроке - полный разбор.

*args - произвольное число позиционных аргументов

def sum_all(*numbers):
    total = 0
    for n in numbers:
        total += n
    return total

print(sum_all(1, 2, 3))           # 6
print(sum_all(1, 2, 3, 4, 5))     # 15
print(sum_all())                    # 0

*numbers собирает все позиционные аргументы в кортеж. Имя args - конвенция, можно любое.

Можно комбинировать с обычными:

def log(level, *messages):
    for msg in messages:
        print(f"[{level}] {msg}")

log("INFO", "started", "loaded config", "ready")

level - обязательный позиционный. *messages - всё остальное.

**kwargs - произвольное число именованных аргументов

def show_config(**options):
    for key, value in options.items():
        print(f"{key} = {value}")

show_config(host="localhost", port=5432, timeout=30)
# host = localhost
# port = 5432
# timeout = 30

**options собирает именованные аргументы в dict. Имя kwargs - конвенция.

Полный порядок параметров

def func(pos1, pos2, *args, key1=None, key2=None, **kwargs):
    ...

Порядок:

  1. Обычные позиционные (pos1, pos2)
  2. *args - звёздочка плюс имя, собирает остальные позиционные
  3. Keyword-only (key1, key2) - после *args, обязательно по имени
  4. **kwargs - двойная звёздочка плюс имя, собирает остальные именованные

После *args все обычные параметры становятся keyword-only - их можно передать только по имени.

Распаковка при вызове

То же * и ** работает при вызове - распаковывают список или словарь в аргументы:

def greet(name, age, city):
    print(f"{name}, {age}, {city}")

# Список → позиционные
data = ["Alice", 30, "Moscow"]
greet(*data)

# Dict → именованные
options = {"name": "Bob", "age": 25, "city": "London"}
greet(**options)

# Можно комбинировать
greet(*["Charlie"], **{"age": 35, "city": "Paris"})

positional-only через /

С Python 3.8 можно явно ограничить параметры быть только позиционными:

def divide(a, b, /):
    return a / b

divide(10, 2)        # OK
divide(a=10, b=2)    # TypeError - a и b positional-only

Всё что до / - только позиционное. Это полезно для:

  • Имитации поведения встроенных функций (len(x) - нельзя len(obj=x))
  • Свободы менять имена параметров без breaking changes
  • Производительности (positional чуть быстрее keyword)

keyword-only через *

Можно потребовать передачу только по имени:

def connect(host, *, port=80, timeout=30):
    ...

connect("example.com")                  # OK
connect("example.com", port=443)        # OK
connect("example.com", 443)             # TypeError - port keyword-only

Всё что после голой * - только keyword. Полезно для:

  • Безопасности (нельзя случайно перепутать порядок необязательных)
  • Самодокументируемости вызова (func(timeout=30) яснее чем func(30))

Полный пример с / и *

def f(a, b, /, c, d, *, e, f):
    ...

f(1, 2, 3, 4, e=5, f=6)        # OK
f(1, 2, c=3, d=4, e=5, f=6)    # OK - c/d можно по имени или позиционно
f(a=1, b=2, ...)               # TypeError - a/b только позиционные
f(1, 2, 3, 4, 5, 6)            # TypeError - e/f только keyword
ПараметрПозиционноПо имени
a, b (до /)данет
c, d (между / и *)дада
e, f (после *)нетда

Зачем все эти ограничения

positional-only (/):

# Стандартная библиотека: len(obj) принимает только позиционно
len([1, 2, 3])        # OK
len(obj=[1, 2, 3])    # TypeError

# Для своих функций часто не нужно. Полезно когда:
# - имя параметра не имеет смысла для вызывающего (len, abs, sum)
# - хочется свободы переименовывать параметр

def square(x, /):  # пользователь не зависит от имени x
    return x * x

keyword-only (*):

# Без keyword-only - легко перепутать
def get_users(active=True, premium=False, suspended=False):
    ...

# При вызове get_users(False, True) - какой параметр кому?

# С keyword-only - вынужденная читаемость
def get_users(*, active=True, premium=False, suspended=False):
    ...

# Вызов get_users(False, True) - TypeError
# Только get_users(active=False, premium=True) - явно и понятно

Передача аргументов дальше

*args, **kwargs - стандартный приём для wrapper-функций:

def with_logging(func):
    def wrapper(*args, **kwargs):
        print(f"Вызов {func.__name__} с args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"Результат: {result}")
        return result
    return wrapper

@with_logging
def add(a, b):
    return a + b

add(2, 3)
# Вызов add с args=(2, 3), kwargs={}
# Результат: 5

Это основа декораторов.

Распространённые ошибки

**1. Mutable default в *args/kwargs - не работает.

# *args и **kwargs всегда новые при каждом вызове
# проблема с mutable default только у обычных параметров
def f(*args):
    args.append(1)  # TypeError - tuple immutable!

Кортеж сам по себе immutable, так что mutable default через *args невозможен.

*2. Передача args как один аргумент.

items = [1, 2, 3]
sum_all(items)     # передаст один аргумент - список
sum_all(*items)    # распакует - три аргумента

Без * список передаётся целиком.

3. Конфликт ключей.

def f(**kwargs):
    pass

d = {"a": 1, "b": 2}
f(**d, a=3)   # TypeError - дублирующийся ключ a

Производительность

Тип вызоваОтносительная скорость
Позиционные1.0 (базис)
Keyword1.1 - 1.3
*args1.2 - 1.5
**kwargs1.5 - 2.0

Разница незначительна для большинства задач. Не оптимизируй преждевременно - читаемость важнее.

Сравнение с Go

В Go variadic args - только последний параметр, тип фиксирован:

func sumAll(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

sumAll(1, 2, 3)
nums := []int{1, 2, 3}
sumAll(nums...)  // распаковка через ...

Keyword arguments как класс в Go нет вообще - параметры всегда позиционные. Структуры используются для имитации именованных аргументов:

type Options struct {
    Host    string
    Port    int
    Timeout time.Duration
}

func Connect(opts Options) {}

Python даёт более гибкий синтаксис, что особенно полезно для библиотек с многими опциями.

Мини-задание

  1. Функция с *args и **kwargs:
def make_request(url, *args, method="GET", **headers):
    print(f"{method} {url}")
    print(f"Path params: {args}")
    print(f"Headers: {headers}")

make_request(
    "/users",
    "active=true",
    method="POST",
    Authorization="Bearer ...",
    Content_Type="application/json"
)
  1. Распаковка при вызове:
def divide(a, b):
    return a / b

# Через список
values = [10, 2]
print(divide(*values))

# Через dict
params = {"a": 10, "b": 2}
print(divide(**params))
  1. Keyword-only для безопасности API:
def create_user(*, name, email, age=18, admin=False):
    return {"name": name, "email": email, "age": age, "admin": admin}

# Только по имени
create_user(name="Alice", email="a@b.c", admin=True)

# Это упадёт - попытка позиционно
# create_user("Alice", "a@b.c", 30, True)

Что дальше

Освоили гибкие списки параметров. В следующем уроке - замыкания и лямбды: как функции захватывают переменные из окружения, синтаксис анонимных функций и инструменты из functools.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.