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):
...
Порядок:
- Обычные позиционные (
pos1,pos2) *args- звёздочка плюс имя, собирает остальные позиционные- Keyword-only (
key1,key2) - после*args, обязательно по имени **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 (базис) |
| Keyword | 1.1 - 1.3 |
*args | 1.2 - 1.5 |
**kwargs | 1.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 даёт более гибкий синтаксис, что особенно полезно для библиотек с многими опциями.
Мини-задание
- Функция с *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"
)
- Распаковка при вызове:
def divide(a, b):
return a / b
# Через список
values = [10, 2]
print(divide(*values))
# Через dict
params = {"a": 10, "b": 2}
print(divide(**params))
- 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.