# Nested Functions, Closures ו-nonlocal<no value>

## מה נלמד?

- **Nested Function** — פונקציה שמוגדרת בתוך פונקציה.
- **Enclosing** — ה-E של LEGB, מקרוב.
- **Closure** — פונקציה ש"זוכרת" משתנים.
- **`nonlocal`** — איך משנים משתנה של הפונקציה העוטפת.

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

- **פונקציית עזר פרטית:** לפעמים פונקציה צריכה פונקציית עזר קטנה ששייכת **רק** לה. אין סיבה שהיא תהיה גלובלית ותתערבב עם שאר הקוד.
- **פונקציות שזוכרות מצב:** בשיעור הקודם ראינו ש-`make_multiplier(2)` מחזירה פונקציה ש"זוכרת" את המספר 2. היום נבין איך זה עובד — ואיך בונים מונה, מצבר או מטמון **בלי** `global`.

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

### פונקציה בתוך פונקציה

אפשר להגדיר `def` בתוך `def`. הפונקציה הפנימית:

- קיימת **רק בתוך** הפונקציה החיצונית — מבחוץ אי אפשר לקרוא לה.
- **רואה** את המשתנים והפרמטרים של הפונקציה החיצונית.

### Enclosing — ה-E של LEGB

בשיעור על Scope למדנו את סדר החיפוש **L → E → G → B**. כשפונקציה פנימית משתמשת בשם, Python מחפשת:

```text
L  Local       → inside the inner function
E  Enclosing   → inside the outer function
G  Global      → top level of the file
B  Built-in    → print, len, ...
```

### Closure — פונקציה שזוכרת

כשפונקציה חיצונית **מחזירה** את הפונקציה הפנימית, משהו מעניין קורה: הפונקציה החיצונית כבר הסתיימה, אבל הפנימית **עדיין זוכרת** את המשתנים שלה. הצירוף הזה — פונקציה + המשתנים שהיא זוכרת — נקרא **Closure**.

{{< box "analogy" "אנלוגיה: מכתב בתוך מעטפה" >}}
הפונקציה החיצונית כותבת מכתב (הפונקציה הפנימית) ומכניסה איתו למעטפה פתק עם כמה נתונים (המשתנים). היא שולחת את המעטפה ונעלמת — אבל מי שפותח את המעטפה מקבל גם את המכתב וגם את הפתק. כל מעטפה עם פתק משלה.
{{< /box >}}

### nonlocal

**לקרוא** משתנה מה-Enclosing — מותר תמיד. **לשנות** אותו (להשים לו ערך חדש) — אי אפשר בלי הכרזה, בדיוק כמו עם גלובלי. ההכרזה היא `nonlocal name`: "המשתנה הזה שייך לפונקציה העוטפת".

| הכרזה | המשתנה שייך ל... |
|---|---|
| `global x` | רמת הקובץ |
| `nonlocal x` | הפונקציה העוטפת הקרובה |

## תחביר

```python
def outer():
    count = 0                 # enclosing variable

    def inner():
        nonlocal count        # assignments go to outer's count
        count += 1
        return count

    return inner              # return the function itself (no parentheses)
```

## דוגמת קוד ראשונה

**פונקציית עזר פרטית.** `week-2/receipt.py`:

```python
def format_receipt(items):
    def line(name, price):
        return name.ljust(10) + str(price)

    lines = []
    for name, price in items:
        lines.append(line(name, price))
    return "\n".join(lines)


print(format_receipt([("Milk", 6), ("Bread", 9)]))
```

**פלט:**

```text
Milk      6
Bread     9
```

## פירוק הקוד שורה אחר שורה

| שורה | הסבר |
|---|---|
| `def line(name, price):` | פונקציה פנימית — קיימת רק בתוך `format_receipt`. |
| `name.ljust(10)` | מרפד את השם ברווחים עד 10 תווים, כדי שהמחירים יעמדו בטור. |
| `lines.append(line(name, price))` | הפונקציה החיצונית משתמשת בפונקציית העזר שלה. |
| `"\n".join(lines)` | מחבר את כל השורות לטקסט אחד, עם ירידת שורה ביניהן. |

## דוגמה נוספת

**Closure — פונקציה שזוכרת.** `week-2/greeter.py`:

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


hello = make_greeter("Hello")
shalom = make_greeter("Shalom")
print(hello("Dana"))
print(shalom("Avi"))
```

**פלט:**

```text
Hello, Dana
Shalom, Avi
```

`make_greeter` הסתיימה כבר בשורה `hello = ...`, אבל `hello` עדיין זוכרת ש-`greeting` הוא `"Hello"`. לכל פונקציה שנוצרה יש **עותק משלה** של המשתנה — `shalom` זוכרת `"Shalom"`.

**מונה עם `nonlocal`.** `week-2/counter.py`:

```python
def make_counter():
    count = 0

    def increment():
        nonlocal count
        count += 1
        return count

    return increment


counter = make_counter()
print(counter(), counter(), counter())

other = make_counter()
print(other())
```

**פלט:**

```text
1 2 3
1
```

כל מונה זוכר את ה-`count` **שלו**: `other` מתחיל מאפס, בלי קשר ל-`counter`. זה מצב פרטי — בלי משתנה גלובלי שכל הקוד יכול לשנות.

## טעות נפוצה

**הטעות: לשנות משתנה של הפונקציה העוטפת בלי `nonlocal`.**

```python
def make_counter():
    count = 0

    def increment():
        count += 1
        return count

    return increment


counter = make_counter()
counter()
```

```text
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
```

**למה?** בגלל ההשמה `count += 1`, Python מחליטה ש-`count` הוא משתנה **מקומי** של `increment` — ואז מנסה לקרוא מקומי שעוד אין לו ערך. זו בדיוק אותה שגיאה שראינו עם גלובליים בשיעור על Scope.

**התיקון:** `nonlocal count` בתחילת `increment`.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## תרגול

התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.

### תרגיל 1: פונקציית עזר פנימית

כתבו פונקציה `format_receipt(items)` שמקבלת רשימה של זוגות (שם, מחיר) ומחזירה טקסט של קבלה. בתוכה הגדירו פונקציה פנימית `line(name, price)` שמרפדת את השם ל-10 תווים (`ljust`).

```python
print(format_receipt([("Milk", 6), ("Bread", 9)]))
```

**פלט צפוי:**

```text
Milk      6
Bread     9
```

{{< solution >}}
```python
def format_receipt(items):
    def line(name, price):
        return name.ljust(10) + str(price)

    lines = []
    for name, price in items:
        lines.append(line(name, price))
    return "\n".join(lines)


print(format_receipt([("Milk", 6), ("Bread", 9)]))
```

```text
Milk      6
Bread     9
```

`line` קיימת רק בתוך `format_receipt` — היא פונקציית עזר פרטית, ולא מתערבבת עם שאר הקוד.
{{< /solution >}}

### תרגיל 2: Closure

כתבו `make_greeter(greeting)` שמחזירה פונקציה שמקבלת שם ומחזירה ברכה.

```python
hello = make_greeter("Hello")
shalom = make_greeter("Shalom")
print(hello("Dana"))
print(shalom("Avi"))
```

**פלט צפוי:**

```text
Hello, Dana
Shalom, Avi
```

{{< solution >}}
```python
def make_greeter(greeting):
    def greet(name):
        return greeting + ", " + name
    return greet


hello = make_greeter("Hello")
shalom = make_greeter("Shalom")
print(hello("Dana"))
print(shalom("Avi"))
```

```text
Hello, Dana
Shalom, Avi
```

`make_greeter` כבר הסתיימה, אבל כל פונקציה שהיא החזירה **זוכרת** את ה-`greeting` שלה. זה Closure.
{{< /solution >}}

### תרגיל 3: חזו את הפלט

```python
def outer():
    message = "outer"

    def show():
        print("show:", message)

    def change():
        nonlocal message
        message = "changed"

    show()
    change()
    show()

outer()
```

{{< solution >}}
```python
def outer():
    message = "outer"

    def show():
        print("show:", message)

    def change():
        nonlocal message
        message = "changed"

    show()
    change()
    show()


outer()
```

```text
show: outer
show: changed
```

`change` משתמשת ב-`nonlocal`, ולכן ההשמה משנה את ה-`message` של `outer` — היא לא יוצרת משתנה מקומי חדש.
{{< /solution >}}

### תרגיל 4: מונה עם צעד

כתבו `make_counter(step)` — כל קריאה למונה מגדילה אותו ב-`step` ומחזירה את הערך החדש. צרו שני מונים ובדקו שהם לא משפיעים זה על זה.

```python
by_two = make_counter(2)
by_ten = make_counter(10)
print(by_two(), by_two())
print(by_ten())
print(by_two())
```

**פלט צפוי:**

```text
2 4
10
6
```

{{< solution >}}
```python
def make_counter(step):
    count = 0

    def increment():
        nonlocal count
        count += step
        return count

    return increment


by_two = make_counter(2)
by_ten = make_counter(10)
print(by_two(), by_two())
print(by_ten())
print(by_two())
```

```text
2 4
10
6
```

- `step` רק **נקרא** בתוך `increment`, ולכן לא צריך עבורו הכרזה.
- `count` **משתנה**, ולכן צריך `nonlocal count`.
{{< /solution >}}

### תרגיל 5: בלי nonlocal

```python
def make_counter():
    count = 0
    def increment():
        count += 1
        return count
    return increment

counter = make_counter()
counter()
```

1. איזו שגיאה תתקבל, ולמה?
2. מה ההבדל בין `global` ל-`nonlocal`?

{{< solution >}}
1. **`UnboundLocalError`**. בגלל ההשמה `count += 1`, Python מחליטה ש-`count` הוא משתנה **מקומי** של `increment` — ואין לו ערך כשמנסים להגדיל אותו. התיקון: `nonlocal count` בתחילת `increment`.
2. **`global`** — המשתנה שייך לרמת הקובץ. **`nonlocal`** — המשתנה שייך לפונקציה **העוטפת** הקרובה. Closure עם `nonlocal` נותן מצב פרטי לכל מונה, בלי משתנה גלובלי שכל הקוד יכול לשנות.
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- **Nested Function** — פונקציה בתוך פונקציה. קיימת רק בפנים, ורואה את המשתנים של העוטפת (**Enclosing**).
- **Closure** — פונקציה פנימית שהוחזרה, וזוכרת את המשתנים של הפונקציה שיצרה אותה. לכל אחת עותק משלה.
- **לקרוא** משתנה מה-Enclosing — מותר. **לשנות** אותו — רק עם `nonlocal`, אחרת `UnboundLocalError`.
- `global` — משתנה ברמת הקובץ. `nonlocal` — משתנה בפונקציה העוטפת.
- Closure נותן **מצב פרטי** בלי משתנה גלובלי. ועדיין — כשאפשר, עדיף פרמטר נכנס ו-`return` יוצא.

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

## דוקומנטציה רשמית

- [Scopes and Namespaces Example](https://docs.python.org/3/tutorial/classes.html#scopes-and-namespaces-example) — local, nonlocal ו-global בדוגמה אחת
- [The nonlocal statement](https://docs.python.org/3/reference/simple_stmts.html#the-nonlocal-statement) — nonlocal

<!-- docs:end -->
