# Функции подробно: значения по умолчанию, функции как объекты, *args, **kwargs и lambda<no value>

## Что мы изучим?

В теме чистого кода и отладки мы изучили основы: `def`, параметры, `return`. Сегодня изучим несколько возможностей, которые делают функции намного гибче:

1. **Значения по умолчанию** (default values) и **именованные аргументы** (keyword arguments).
2. **Функции как объекты** (Functions as Objects) — функция — такое же значение, как любое другое.
3. **`*args`** — сколько угодно обычных аргументов.
4. **`**kwargs`** — сколько угодно именованных аргументов.
5. **Lambda** — короткая функция без имени.

## Зачем это нужно?

- `print("a", "b", "c", sep="-")` принимает **сколько угодно аргументов** плюс **именованный** аргумент. Как это устроено? Через `*args`.
- `sorted(students, key=...)` принимает **функцию** как параметр, чтобы знать, по чему сортировать.
- **FastAPI** (темы бэкенда и баз данных) целиком построен на идее, что функция — объект: вы пишете функцию, а FastAPI «регистрирует» её как Endpoint и запускает, когда приходит запрос.

## Объяснение с основ

### Значения по умолчанию

Параметру можно дать значение, которое будет использовано, **если аргумент не передан**:

```python
def greet(name, greeting="Hello"):
    return greeting + ", " + name
```

`greet("Dana")` → `"Hello, Dana"`. `greet("Dana", "Hi")` → `"Hi, Dana"`.

**Правило:** параметры со значением по умолчанию идут **после** параметров без него.

### Именованные аргументы

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

```python
greet(greeting="Hey", name="Avi")
```

Это очень читаемо, когда параметров много: `create_user("dana", age=24, is_admin=False)`.

### Функции как объекты

В Python **функция — это объект**, как число или список. Поэтому можно:

- Сохранить её в переменной: `say = greet`.
- Положить её в список или dict.
- **Передать её как аргумент** другой функции.

**Критическая разница:** `greet` (без скобок) = **сама функция**. `greet("Dana")` (со скобками) = **её запуск** и его результат.

{{< box "analogy" "Аналогия: рецепт и торт" >}}
`greet` — это **рецепт**: его можно сфотографировать и отправить подруге. `greet("Dana")` — это **торт**: результат выпечки по рецепту. Когда вы передаёте функцию другой функции, вы отправляете рецепт, чтобы та могла печь по нему, когда понадобится.
{{< /box >}}

### `*args` — переменное число аргументов

Звёздочка перед именем параметра говорит: «собери **все** обычные аргументы (по позиции) в **tuple**»:

```python
def total(*numbers):
    return sum(numbers)

total(1, 2)          # numbers = (1, 2)
total(5, 10, 20, 1)  # numbers = (5, 10, 20, 1)
```

Имя `args` — соглашение. Работу делает звёздочка.

### `**kwargs` — переменное число именованных аргументов

Две звёздочки: «собери все **именованные** аргументы в **dict**»:

```python
def describe(**details):
    for key, value in details.items():
        print(key, "=", value)

describe(name="Dana", age=24, city="Haifa")
# details = {"name": "Dana", "age": 24, "city": "Haifa"}
```

**kwargs** = **k**ey**w**ord **arg**ument**s**.

### Lambda

**lambda** — способ записать **маленькую функцию в одну строку, без имени**:

```python
lambda x: x * 2
```

Равносильно:

```python
def double(x):
    return x * 2
```

- После `lambda` — параметры. После `:` — **одно выражение**, которое и есть возвращаемое значение (без `return`).
- Главное применение: передать **короткую** функцию как аргумент, вместо того чтобы определять отдельный `def`. Чаще всего с `sorted`, `min`, `max`.

## Синтаксис

```python
# default value + keyword arguments
def func(required, optional="default"):
    ...
func("a")
func("a", optional="b")

# function as an object
f = func            # no parentheses: the function itself
f("a")              # call it through the new name

def apply(operation, value):
    return operation(value)    # call the function we received

# *args and **kwargs
def func(*args, **kwargs):
    # args   -> tuple of positional arguments
    # kwargs -> dict of keyword arguments
    ...

# lambda
lambda parameters: expression
sorted(items, key=lambda item: item["price"])
```

## Первый пример кода

`week-2/functions_deep.py`:

```python
def make_greeting(name, greeting="Hello", punctuation="!"):
    return greeting + ", " + name + punctuation

print(make_greeting("Dana"))
print(make_greeting("Avi", "Good morning"))
print(make_greeting("Noa", punctuation="?"))

def shout(text):
    return text.upper()

def whisper(text):
    return text.lower() + "..."

def say(message, style):
    return style(message)

print(say("Hello there", shout))
print(say("Hello there", whisper))
```

**Вывод:**

```text
Hello, Dana!
Good morning, Avi!
Hello, Noa?
HELLO THERE
hello there...
```

## Разбор кода построчно

| Строка | Объяснение |
|---|---|
| `def make_greeting(name, greeting="Hello", punctuation="!")` | `name` обязателен. Два других необязательны и имеют значения по умолчанию. |
| `make_greeting("Dana")` | Оба необязательных параметра берут значение по умолчанию. |
| `make_greeting("Avi", "Good morning")` | Второй аргумент **по позиции** идёт в `greeting`. |
| `make_greeting("Noa", punctuation="?")` | **Именованный аргумент**: пропускаем `greeting` (по умолчанию) и задаём только `punctuation`. |
| `def say(message, style):` | `style` — параметр, который ожидает **функцию**. |
| `return style(message)` | **Запуск** полученной функции. `say` не знает и не интересуется, что это за функция. |
| `say("Hello there", shout)` | Передаём `shout` **без скобок**, то есть саму функцию. |

## Ещё один пример

**`*args`, `**kwargs` и `lambda` в маленькой настоящей системе.** `week-2/store_tools.py`:

```python
def order_total(*prices, discount=0):
    subtotal = sum(prices)
    return subtotal - subtotal * discount / 100

print(order_total(10, 20, 30))
print(order_total(100, 50, discount=10))

def build_user(username, **extra):
    user = {"username": username}
    for key, value in extra.items():
        user[key] = value
    return user

print(build_user("dana"))
print(build_user("avi", age=30, city="Haifa", is_admin=True))

products = [
    {"name": "Laptop", "price": 3500},
    {"name": "Mouse", "price": 80},
    {"name": "Desk", "price": 900},
]

by_price = sorted(products, key=lambda product: product["price"])
for product in by_price:
    print(product["name"], product["price"])

cheapest = min(products, key=lambda product: product["price"])
print("Cheapest:", cheapest["name"])
```

**Вывод:**

```text
60.0
135.0
{'username': 'dana'}
{'username': 'avi', 'age': 30, 'city': 'Haifa', 'is_admin': True}
Mouse 80
Desk 900
Laptop 3500
Cheapest: Mouse
```

**Разбор:**

- `def order_total(*prices, discount=0):` — `*prices` собирает **любое** число цен в tuple. `discount` идёт **после** `*prices`, поэтому его можно передать **только по имени** (`discount=10`). Иначе Python не знал бы, где заканчиваются цены.
- `build_user("avi", age=30, ...)` — `username` получает `"avi"`, а всё остальное собирается в `extra` как dict: `{"age": 30, "city": "Haifa", "is_admin": True}`.
- `sorted(products, key=lambda product: product["price"])` — `sorted` не умеет сравнивать словари. `key` — это **функция**, которая говорит ему: «для каждого товара используй для сравнения его цену». lambda вызывается по разу для каждого товара.
- `min(..., key=...)` — та же идея: «минимум по цене». Возвращается **весь товар**, а не только цена.

## Типичная ошибка

**Ошибка 1: передать результат вместо функции.**

```python
print(say("Hello", shout()))
```

```text
TypeError: shout() missing 1 required positional argument: 'text'
```

`shout()` со скобками пытается **запустить** `shout` сразу, без аргумента. Когда передаёте функцию, пишите **только имя**: `shout`.

**Ошибка 2: список или dict в качестве значения по умолчанию.**

```python
def add_item(item, cart=[]):
    cart.append(item)
    return cart

print(add_item("milk"))
print(add_item("bread"))
```

```text
['milk']
['milk', 'bread']
```

Во втором вызове мы ожидали `['bread']`! Значение по умолчанию `[]` создаётся **один раз**, при определении функции, и все вызовы **используют один и тот же список** (изменяемый — предыдущий урок!). Принятое исправление:

```python
def add_item(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart
```

**Ошибка 3: слишком сложная lambda.**

```python
result = sorted(users, key=lambda u: (0 if u["active"] else 1, -u["score"], u["name"].lower()))
```

Это работает, но через неделю никто не поймёт (KISS!). Если lambda не читается с первого взгляда, напишите `def` с понятным именем.

**Ошибка 4: параметр со значением по умолчанию перед обязательным параметром.**

```python
def greet(greeting="Hello", name):
```

```text
SyntaxError: parameter without a default follows parameter with a default
```

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Практика

Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.

### Упражнение 1: Значения по умолчанию и именованные аргументы

Напишите функцию `make_greeting(name, greeting="Hello", punct="!")`.

```python
print(make_greeting("Dana"))
print(make_greeting("Avi", "Good morning"))
print(make_greeting("Noa", punct="?"))
```

**Ожидаемый вывод:**

```text
Hello, Dana!
Good morning, Avi!
Hello, Noa?
```

{{< solution >}}
```python
def make_greeting(name, greeting="Hello", punct="!"):
    return greeting + ", " + name + punct


print(make_greeting("Dana"))
print(make_greeting("Avi", "Good morning"))
print(make_greeting("Noa", punct="?"))
```

```text
Hello, Dana!
Good morning, Avi!
Hello, Noa?
```

В третьем вызове `punct="?"` передаётся **по имени**, поэтому можно пропустить `greeting` и оставить ему значение по умолчанию.
{{< /solution >}}

### Упражнение 2: *args

Напишите функцию `average(*numbers)`, которая получает любое количество чисел и возвращает среднее. Если не передано ни одного числа — выбрасывает `ValueError` с сообщением, показанным в ожидаемом выводе.

```python
print(average(80, 90))
print(average(70, 80, 90, 100))
try:
    average()
except ValueError as error:
    print("error:", error)
```

**Ожидаемый вывод:**

```text
85.0
85.0
error: need at least one number
```

{{< solution >}}
```python
def average(*numbers):
    if len(numbers) == 0:
        raise ValueError("need at least one number")
    return sum(numbers) / len(numbers)


print(average(80, 90))
print(average(70, 80, 90, 100))
try:
    average()
except ValueError as error:
    print("error:", error)
```

```text
85.0
85.0
error: need at least one number
```

`*numbers` собирает все аргументы по позиции в **tuple**.
{{< /solution >}}

### Упражнение 3: `**kwargs`

Напишите функцию `format_profile(name, **details)`, которая возвращает строку с именем, за которым следует каждая дополнительная деталь в формате `key=value`.

```python
print(format_profile("Dana", age=24, city="Haifa"))
print(format_profile("Avi"))
```

**Ожидаемый вывод:**

```text
Dana: age=24 city=Haifa
Avi:
```

{{< solution >}}
```python
def format_profile(name, **details):
    line = name + ":"
    for key, value in details.items():
        line = line + " " + key + "=" + str(value)
    return line


print(format_profile("Dana", age=24, city="Haifa"))
print(format_profile("Avi"))
```

```text
Dana: age=24 city=Haifa
Avi:
```

`**details` собирает все **именованные** аргументы в **dict**.
{{< /solution >}}

### Упражнение 4: lambda в сортировке

```python
products = [("Laptop", 3500), ("Mouse", 80), ("Desk", 900)]
```

1. Отсортируйте по цене, от самого дешёвого к самому дорогому, и напечатайте каждый товар.
2. Напечатайте название самого дорогого товара.

**Ожидаемый вывод:**

```text
Mouse 80
Desk 900
Laptop 3500
Most expensive: Laptop
```

{{< solution >}}
```python
products = [("Laptop", 3500), ("Mouse", 80), ("Desk", 900)]

for name, price in sorted(products, key=lambda p: p[1]):
    print(name, price)

most_expensive = max(products, key=lambda p: p[1])
print("Most expensive:", most_expensive[0])
```

```text
Mouse 80
Desk 900
Laptop 3500
Most expensive: Laptop
```

`lambda p: p[1]` — короткая функция, которая возвращает цену, — по ней сравнивают `sorted` и `max`.
{{< /solution >}}

### Упражнение 5: Изменяемое значение по умолчанию

```python
def add_item(item, cart=[]):
    cart.append(item)
    return cart

print(add_item("milk"))
print(add_item("bread"))
```

1. Что будет напечатано во втором вызове и почему?
2. Исправьте функцию.

{{< solution >}}
1. Напечатается `['milk', 'bread']`. Значение по умолчанию `[]` создаётся **один раз**, при определении функции, — и все вызовы используют **один и тот же** список.
2. Используйте `None` и создавайте новый список внутри функции:

```python
def add_item(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart


print(add_item("milk"))
print(add_item("bread"))
```

```text
['milk']
['bread']
```
{{< /solution >}}

<!-- exercises:end -->

## Итоги

- **Значение по умолчанию**: `def f(a, b=10)`. Параметры со значением по умолчанию — в конце. Никогда `[]` или `{}` по умолчанию (используйте `None`).
- **Именованный аргумент**: `f(a=1, b=2)`. Читаемо, и порядок не важен.
- **Функция — объект**: `f` = функция, `f()` = её запуск. Её можно сохранить в переменной, в dict и передать как аргумент.
- `*args` собирает обычные аргументы в **tuple**. `**kwargs` собирает именованные аргументы в **dict**.
- `lambda x: expression` — короткая функция без имени. Идеальна для `key=` в `sorted`/`min`/`max`. Если сложно — лучше `def`.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Официальная документация

- [Default Argument Values](https://docs.python.org/3/tutorial/controlflow.html#default-argument-values)
- [Arbitrary Argument Lists](https://docs.python.org/3/tutorial/controlflow.html#arbitrary-argument-lists)
- [Keyword Arguments](https://docs.python.org/3/tutorial/controlflow.html#keyword-arguments)
- [Lambda Expressions](https://docs.python.org/3/tutorial/controlflow.html#lambda-expressions)

<!-- docs:end -->
