deшка
Урок 3.5

Функции: передача аргументов, *args и **kwargs, lambda

Как Python передаёт аргументы на самом деле, чем func отличается от func(), что такое **kwargs, где уместна lambda и почему изменяемое значение по умолчанию ломает код.

Функция в Python это обычный объект: у неё есть имя, её можно положить в список, передать в другую функцию, вернуть из функции. На этом стоят декораторы, sorted(key=...), обработчики в Airflow и половина идиом языка. Этот урок про механику вызова: что именно получает функция, когда ей передают аргумент, и как устроены *args, **kwargs и lambda.

func и func()

func это объект функции, func() это её вызов, результат которого подставляется на место выражения.

def now_label():
    return "2026-07-01"
 
label = now_label      # label это та же функция
value = now_label()    # value это строка
print(label, value)    # <function now_label at 0x...> 2026-07-01
print(label())         # 2026-07-01

Ошибка, которую делают постоянно: передать handler() туда, где ждут handler. Тогда функция вызывается сразу, а в параметр попадает её результат, часто None. В Airflow это python_callable=task() вместо python_callable=task: задача выполняется при разборе DAG, а не в расписании.

Как передаются аргументы

В Python не «по значению» и не «по ссылке», а по присваиванию: параметр функции становится ещё одним именем для переданного объекта, как при b = a. Отсюда два следствия, которые надо держать вместе.

Если объект изменяемый и функция меняет его на месте, вызывающий код это увидит:

def add_total(row):
    row["total"] = row["qty"] * row["price"]
 
order = {"qty": 2, "price": 10}
add_total(order)
print(order)     # {'qty': 2, 'price': 10, 'total': 20}

Если функция перепривязывает параметр, снаружи ничего не меняется:

def reset(row):
    row = {}         # новое имя для нового словаря, order не тронут
 
reset(order)
print(order)         # {'qty': 2, 'price': 10, 'total': 20}

С неизменяемыми объектами «изменить на месте» невозможно, поэтому число или строка в функции всегда остаются прежними снаружи. Это не отдельное правило, а то же присваивание.

Договорённость для кода загрузок: функция либо меняет аргумент и ничего не возвращает (add_total(row)), либо возвращает новый объект и не трогает аргумент (with_total(row)). Смешивать нельзя: вызывающий код не поймёт, можно ли использовать исходный объект дальше.

Позиционные, именованные, *args и **kwargs

Аргументы можно передавать по позиции и по имени. *args собирает лишние позиционные в кортеж, **kwargs собирает лишние именованные в словарь:

def log(level, *parts, **context):
    print(level, parts, context)
 
log("info", "loaded", 120, table="orders", took=3.5)
# info ('loaded', 120) {'table': 'orders', 'took': 3.5}

Имена args и kwargs это соглашение, звёздочки это синтаксис. Функция с **qwe в сигнатуре принимает любые именованные аргументы и видит их как словарь qwe.

Та же звёздочка работает в обратную сторону при вызове: распаковка.

params = {"table": "orders", "schema": "stg"}
build_query(**params)              # build_query(table="orders", schema="stg")
 
columns = ["id", "status"]
select(*columns)                   # select("id", "status")

Это основной способ прокинуть параметры сквозь слой: обёртка принимает **kwargs и отдаёт их дальше, не зная их состав. Так устроены декораторы и большинство клиентов к API. Цена: сигнатура перестаёт документировать, что функция принимает, поэтому в коде, который вызывают люди, лучше явные параметры.

После * в сигнатуре идут только именованные параметры, их нельзя передать по позиции:

def load(table, *, chunk_size=1000, dry_run=False): ...
load("orders", 500)              # TypeError
load("orders", chunk_size=500)   # так

Для функций с несколькими флагами это защита от вызова load("orders", True, 500), где через месяц никто не вспомнит порядок.

Изменяемое значение по умолчанию

Значение по умолчанию вычисляется один раз, при определении функции, и живёт в объекте функции. Если оно изменяемое, все вызовы делят один объект:

def collect(value, bucket=[]):
    bucket.append(value)
    return bucket
 
print(collect(1))    # [1]
print(collect(2))    # [1, 2]: тот же список

Идиома: по умолчанию None, внутри создать новый объект.

def collect(value, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(value)
    return bucket

Та же ловушка с dict, set и с любым объектом, который вы собираетесь менять. Для неизменяемых значений (0, "", (), None) проблемы нет.

Функция как аргумент

Функцию можно передать в другую функцию. Та вызовет её, когда и сколько раз сочтёт нужным, со своими аргументами.

def apply_rules(row, rules):
    for rule in rules:
        row = rule(row)
    return row
 
def strip_strings(row):
    return {k: v.strip() if isinstance(v, str) else v for k, v in row.items()}
 
def lower_status(row):
    return {**row, "status": row["status"].lower()}
 
clean = apply_rules({"status": " PAID "}, [strip_strings, lower_status])

Переданная функция использует свои собственные параметры, те, которые ей передаст вызывающий код. Если ей нужно что-то ещё, например порог из настроек, это захватывается замыканием: внутренняя функция видит переменные функции, внутри которой она определена, и помнит их после её завершения.

def make_threshold_filter(limit):
    def keep(row):
        return row["amount"] >= limit
    return keep
 
big_only = make_threshold_filter(1000)
rows = list(filter(big_only, rows))

keep помнит limit, хотя make_threshold_filter давно вернула результат. Это и есть замыкание, и на нём построены декораторы из следующего урока.

lambda

lambda это функция без имени из одного выражения. Она нужна там, где функция маленькая и используется один раз: ключ сортировки, условие фильтра.

rows.sort(key=lambda r: (r["city"], -r["amount"]))
paid = [r for r in rows if r["status"] == "paid"]       # здесь lambda не нужна

Внутри lambda нельзя написать несколько инструкций, присваивание или try. Как только логика перестаёт помещаться в одно выражение или нужна второй раз, это def с именем. Присваивать lambda имени (f = lambda x: ...) считается ошибкой стиля: это def без преимуществ def, в traceback такая функция называется <lambda>.

Для простых ключей вместо lambda есть operator.itemgetter("city") и operator.attrgetter("name"): читаются лучше и работают быстрее.

Как отвечать на собеседовании

Как передаются аргументы в функцию? По присваиванию: параметр становится ещё одной ссылкой на переданный объект. Изменение объекта на месте видно снаружи, перепривязка параметра нет. Для неизменяемых типов это выглядит как передача по значению, для изменяемых как по ссылке, но механизм один.

В чём разница между func и func()? func это объект функции, его можно передать и вызвать позже. func() это вызов, на его место подставляется результат.

Что означает **qwe в параметрах? Функция принимает любые именованные аргументы, они собираются в словарь qwe. Одна звёздочка собирает позиционные в кортеж. При вызове те же звёздочки распаковывают словарь и последовательность в аргументы.

Что такое lambda и где её использовать? Анонимная функция из одного выражения. Уместна как короткий ключ сортировки или условие, которые используются один раз. Для всего остального def.

Может ли функция-аргумент использовать свои аргументы? Да: вызывающая сторона передаёт ей аргументы при вызове, а дополнительные значения она берёт из замыкания, то есть из области видимости, где была определена.

Практика

python 3.12 · песочницарешено 0 / 3

Функция apply_rules(row, *rules) принимает запись и любое число функций-правил. Каждое правило получает запись и возвращает новую запись. Примените правила по порядку и верните результат. Если правил нет, верните запись как есть.

Правила передаются позиционно, без списка: apply_rules(row, strip_strings, lower_status).

оболочка, не редактируется
def strip_strings(row):
    return {k: v.strip() if isinstance(v, str) else v for k, v in row.items()}


def lower_status(row):
    return {**row, "status": row["status"].lower()}


def add_total(row):
    return {**row, "total": row["qty"] * row["price"]}
Ctrl+Enter
ТЕСТЫ
ВводВаш выводОжидаемый вывод
print(apply_rules({"status": " PAID "}, strip_strings, lower_status))—{'status': 'paid'}
print(apply_rules({"qty": 2, "price": 10}, add_total))—{'qty': 2, 'price': 10, 'total': 20}
row = {"status": "NEW"} print(apply_rules(row), apply_rules(row) is row)—{'status': 'NEW'} True
скрытый тест 4
решённые задания сохранятся в прогресс после входа
Войти, чтобы отмечатьдальше →
3.6 →Декораторы