# פונקציות לעומק: ערכי ברירת מחדל, Functions as Objects, ‎*args, ‎**kwargs ו-Lambda<no value>

## מה נלמד?

בנושא Python מקצועי, קוד נקי ו-Debugging למדנו את הבסיס: `def`, פרמטרים, `return`. היום נלמד כמה יכולות שהופכות פונקציות לגמישות הרבה יותר:

1. **ערכי ברירת מחדל** ו**ארגומנטים בשם**.
2. **Functions as Objects** — פונקציה היא ערך כמו כל ערך אחר.
3. **`*args`** — כמה ארגומנטים רגילים שרוצים.
4. **`**kwargs`** — כמה ארגומנטים בשם שרוצים.
5. **Lambda** — פונקציה קצרה בלי שם.

## למה צריך את זה?

- `print("a", "b", "c", sep="-")` מקבלת **כמה ארגומנטים שרוצים**, ועוד ארגומנט **בשם**. איך היא בנויה? עם `*args`.
- `sorted(students, key=...)` מקבלת **פונקציה** כפרמטר, כדי לדעת לפי מה למיין.
- **FastAPI** (נושא Backend ומסדי נתונים) בנוי כולו על הרעיון שפונקציה היא אובייקט: כותבים פונקציה, ו-FastAPI "רושם" אותה כ-Endpoint ומפעיל אותה כשמגיעה בקשה.

## הסבר מהבסיס

### ערכי ברירת מחדל (Default Values)

אפשר לתת לפרמטר ערך שישמש **אם לא העבירו ארגומנט**:

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

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

**כלל:** פרמטרים עם ברירת מחדל באים **אחרי** פרמטרים בלי.

### ארגומנטים בשם (Keyword Arguments)

בקריאה אפשר לציין **שם** של פרמטר, והסדר כבר לא משנה:

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

זה מאוד קריא כשיש הרבה פרמטרים: `create_user("dana", age=24, is_admin=False)`.

### Functions as Objects

ב-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` לא יודעת איך להשוות בין dicts. ה-`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']` בקריאה השנייה! ערך ברירת המחדל `[]` נוצר **פעם אחת**, כשהפונקציה מוגדרת, וכל הקריאות **חולקות את אותה רשימה** (Mutable, השיעור הקודם!). התיקון המקובל:

```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) — *args
- [Keyword Arguments](https://docs.python.org/3/tutorial/controlflow.html#keyword-arguments) — **kwargs
- [Lambda Expressions](https://docs.python.org/3/tutorial/controlflow.html#lambda-expressions) — lambda

<!-- docs:end -->
