# Exceptions ב': else ו-finally — המבנה המלא<no value>

## מה נלמד?

בשיעור הקודם השתמשנו ב-`try` וב-`except`. למבנה יש עוד שני חלקים:

- **`else`** — רץ **רק אם לא הייתה שגיאה**.
- **`finally`** — רץ **תמיד**, עם שגיאה או בלעדיה.

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

דמיינו קוד שפותח חיבור ל-Database (נושא Backend ומסדי נתונים), מבצע פעולה וסוגר את החיבור. מה קורה אם הפעולה נכשלת באמצע? אם הסגירה כתובה אחרי הפעולה, היא **לא תתבצע**, והחיבור יישאר פתוח. אחרי מאה כישלונות כאלה, השרת יקרוס כי נגמרו לו החיבורים.

`finally` מבטיח שהניקוי יקרה **בכל מקרה**. `else` שומר על ה-`try` קטן, ומבהיר מה רץ רק אחרי הצלחה.

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

### ארבעת הבלוקים

| בלוק | מתי רץ | למה משמש |
|---|---|---|
| `try` | תמיד מתחילים בו | הקוד ש**עלול להיכשל** |
| `except` | **רק אם** נזרקה שגיאה מהסוג המתאים | טיפול בשגיאה |
| `else` | **רק אם לא** נזרקה שום שגיאה ב-`try` | המשך שתלוי בהצלחה |
| `finally` | **תמיד**, בסוף, בכל מקרה | ניקוי: סגירת קבצים, חיבורים, הודעת סיום |

{{< box "analogy" "אנלוגיה: בישול" >}}
**try** = מנסים לאפות עוגה. **except** = אם נשרפה, מזמינים עוגה מהמאפייה. **else** = אם יצאה טובה, מקשטים אותה (לא מקשטים עוגה שרופה!). **finally** = בכל מקרה, מכבים את התנור ושוטפים כלים.
{{< /box >}}

### למה `else` ולא פשוט להמשיך ב-`try`?

אפשר לשים את קוד ההמשך בתוך ה-`try`. אבל אז, אם הקוד **הזה** נכשל באותה שגיאה, ה-`except` יתפוס אותו בטעות, ויטפל בבעיה הלא נכונה. `else` מבהיר: **רק השורה המסוכנת בתוך `try`**, והשאר בחוץ.

### `finally` רץ גם כש...

- הייתה שגיאה ונתפסה.
- הייתה שגיאה ו**לא** נתפסה. `finally` רץ, ואז התוכנית קורסת.
- יש `return` באמצע ה-`try`. `finally` רץ **לפני** שהפונקציה באמת חוזרת.

## תחביר

```python
try:
    # risky code
except SomeError:
    # runs only if SomeError happened
else:
    # runs only if NO error happened in try
finally:
    # runs ALWAYS, last
```

**סדר חובה:** `try` ‏←‏ `except` (אחד או יותר) ‏←‏ `else` (אופציונלי) ‏←‏ `finally` (אופציונלי). אפשר גם `try` + `finally` בלי `except`.

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

`week-2/full_try.py`:

```python
def convert(text):
    print("--- converting:", text)
    try:
        number = int(text)
    except ValueError:
        print("except: not a number")
    else:
        print("else: success, doubled =", number * 2)
    finally:
        print("finally: done with", text)


convert("21")
convert("hi")
```

**פלט:**

```text
--- converting: 21
else: success, doubled = 42
finally: done with 21
--- converting: hi
except: not a number
finally: done with hi
```

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

| בלוק | `convert("21")` | `convert("hi")` |
|---|---|---|
| `try: number = int(text)` | מצליח: `number = 21` | `ValueError` |
| `except ValueError:` | מדולג | **רץ** |
| `else:` | **רץ**: מדפיס 42 | מדולג |
| `finally:` | **רץ** | **רץ** |

`else` ו-`except` **לעולם לא רצים שניהם**: או שהייתה שגיאה, או שלא. ואילו `finally` רץ **בשני המקרים**.

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

**"חיבור" שחייב להיסגר.** כאן מדמים חיבור למשאב חיצוני (כמו Database), ורואים ש-`finally` סוגר גם כשיש כישלון באמצע, וגם כשיש `return`. `week-2/connection_demo.py`:

```python
def open_connection():
    print("[connection opened]")


def close_connection():
    print("[connection closed]")


def fetch_price(product_name, prices):
    open_connection()
    try:
        price = prices[product_name]
    except KeyError:
        print("No such product:", product_name)
        return None
    else:
        print("Found", product_name)
        return price
    finally:
        close_connection()


catalog = {"bread": 8, "milk": 6}

print("Result:", fetch_price("milk", catalog))
print()
print("Result:", fetch_price("cake", catalog))
```

**פלט:**

```text
[connection opened]
Found milk
[connection closed]
Result: 6

[connection opened]
No such product: cake
[connection closed]
Result: None
```

**פירוק:**

- `catalog = {"bread": 8, "milk": 6}` — זה **dict** (מילון): כל מפתח (`"bread"`) ממופה לערך (`8`). `prices["milk"]` מחזיר 6. נלמד לעומק בנושא מבני נתונים ופונקציות. כרגע מספיק לדעת שגישה למפתח שלא קיים זורקת `KeyError`.
- שימו לב: יש `return` גם ב-`except` וגם ב-`else`. **ובכל זאת** `[connection closed]` מודפס לפני ה-`Result`. כלומר, `finally` רץ **אחרי** ה-`return` "הוחלט", אבל **לפני** שהפונקציה באמת חוזרת.
- בלי `finally`, היינו צריכים לכתוב `close_connection()` לפני **כל** `return`. קל לשכוח, והרבה כפילות.

## טעות נפוצה

**הטעות 1: לשים ניקוי אחרי ה-`try`/`except` במקום ב-`finally`.**

```python
open_connection()
try:
    price = prices[name]
except KeyError:
    print("missing")
    return None
close_connection()   # never reached when except returns!
```

במקרה של `KeyError`, ה-`return` יוצא מהפונקציה, ו-`close_connection()` **לא רץ**. החיבור נשאר פתוח. `finally` פותר בדיוק את זה.

**הטעות 2: קוד ההמשך בתוך ה-`try`, ו-`except` שתופס את השגיאה הלא נכונה.**

```python
try:
    age = int(text)
    category = ["child", "adult"][age // 18]   # can also raise IndexError... or ValueError?
except ValueError:
    print("Age must be a number")
```

אם שורת ההמשך תיכשל, למשל ב-`IndexError` עבור גיל 40 (40 // 18 = 2, ואין אינדקס 2), ההודעה לא קשורה בכלל. שימו בתוך ה-`try` רק את `int(text)`, ואת ההמשך ב-`else`.

**הטעות 3: לחשוב ש-`finally` "תופס" את השגיאה.** הוא לא. אם אין `except` מתאים, `finally` רץ, ו**אחר כך** התוכנית קורסת עם Traceback.

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

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

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

כתבו מה יודפס עבור כל קריאה — לפני שמריצים:

```python
def half(text):
    try:
        number = int(text)
    except ValueError:
        print("not a number:", text)
    else:
        print("half is", number / 2)
    finally:
        print("checked", text)

half("8")
half("x")
```

{{< solution >}}
```python
def half(text):
    try:
        number = int(text)
    except ValueError:
        print("not a number:", text)
    else:
        print("half is", number / 2)
    finally:
        print("checked", text)


half("8")
half("x")
```

```text
half is 4.0
checked 8
not a number: x
checked x
```

- עבור `"8"` אין שגיאה, ולכן רץ `else`, ואחריו `finally`.
- עבור `"x"` נזרקת `ValueError`, ולכן רץ `except` — ו-`else` **לא** רץ. `finally` רץ בשני המקרים.
{{< /solution >}}

### תרגיל 2: finally רץ גם אחרי return

```python
def check(number):
    try:
        if number > 0:
            return "positive"
        return "not positive"
    finally:
        print("done checking", number)

print(check(5))
print(check(-2))
```

מה יודפס, ובאיזה סדר? הסבירו למה.

{{< solution >}}
```python
def check(number):
    try:
        if number > 0:
            return "positive"
        return "not positive"
    finally:
        print("done checking", number)


print(check(5))
print(check(-2))
```

```text
done checking 5
positive
done checking -2
not positive
```

`finally` רץ **לפני** שהפונקציה באמת חוזרת — גם כשיש `return` בתוך ה-`try`. לכן "done checking" מודפס לפני הערך שהפונקציה החזירה.
{{< /solution >}}

### תרגיל 3: ארבעת הבלוקים

כתבו פונקציה `parse_quantity(text)` שמשתמשת בכל ארבעת הבלוקים:

- `try` — ממיר את הטקסט למספר;
- `except` — מדפיס `invalid: <text>`;
- `else` — מדפיס `quantity: <number>`;
- `finally` — מדפיס `---`.

```python
parse_quantity("3")
parse_quantity("three")
```

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

```text
quantity: 3
---
invalid: three
---
```

{{< solution >}}
```python
def parse_quantity(text):
    try:
        quantity = int(text)
    except ValueError:
        print("invalid:", text)
    else:
        print("quantity:", quantity)
    finally:
        print("---")


parse_quantity("3")
parse_quantity("three")
```

```text
quantity: 3
---
invalid: three
---
```

הקוד שרץ רק כשהכול הצליח נמצא ב-`else` ולא בתוך ה-`try` — כך ה-`try` נשאר קטן, ותופס רק את השורה שבאמת עלולה להיכשל.
{{< /solution >}}

### תרגיל 4: לסגור את מה שפתחנו

הפונקציות `open_register()` ו-`close_register()` פותחות וסוגרות קופה. כתבו פונקציה `sell(prices, position)` שפותחת את הקופה, מחזירה את המחיר במיקום שנבחר (המיקום הוא אינדקס של הרשימה — מתחיל מ-0), ו**תמיד** סוגרת את הקופה — גם כשהמיקום לא קיים. מיקום לא קיים מחזיר `None`.

```python
def open_register():
    print("register opened")

def close_register():
    print("register closed")

print(sell([10, 20, 30], 1))
print(sell([10, 20, 30], 7))
```

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

```text
register opened
register closed
20
register opened
register closed
None
```

{{< solution >}}
```python
def open_register():
    print("register opened")


def close_register():
    print("register closed")


def sell(prices, position):
    open_register()
    try:
        return prices[position]
    except IndexError:
        return None
    finally:
        close_register()


print(sell([10, 20, 30], 1))
print(sell([10, 20, 30], 7))
```

```text
register opened
register closed
20
register opened
register closed
None
```

זה בדיוק התפקיד של `finally`: **ניקוי** — סגירת משאב שנפתח, לא משנה איך הפונקציה הסתיימה.
{{< /solution >}}

### תרגיל 5: else או בתוך ה-try?

שני מפתחים כתבו אותו קוד בשתי דרכים:

```text
# A                              # B
try:                             try:
    number = int(text)               number = int(text)
    print(100 / number)          except ValueError:
except ValueError:                   print("not a number")
    print("not a number")        else:
                                     print(100 / number)
```

מה ההבדל, ואיזו גרסה עדיפה? מה יקרה בכל אחת כש-`text` הוא `"0"`?

{{< solution >}}
- ב-**A**, גם החלוקה נמצאת בתוך ה-`try`. ב-**B**, בתוך ה-`try` יש רק את ההמרה, והחלוקה עברה ל-`else`.
- כש-`text` הוא `"0"`, בשתי הגרסאות תיזרק `ZeroDivisionError`, שאף `except` לא תופס, והתוכנית תקרוס. אבל ב-**B** ברור לגמרי שה-`except ValueError` שייך **רק** להמרה.
- **B עדיפה**: `try` קטן ככל האפשר. כך `except` לא תופס בטעות שגיאות שמגיעות משורות אחרות, וקל לראות מה כל חלק עושה.
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- **`try`** — קוד מסוכן. **`except`** — רק אם הייתה שגיאה מתאימה. **`else`** — רק אם **לא** הייתה שגיאה. **`finally`** — **תמיד**.
- `except` ו-`else` לעולם לא רצים יחד.
- `finally` רץ גם אחרי `return`, וגם כשהשגיאה לא נתפסה (ואז התוכנית קורסת אחריו).
- **`else`** שומר את ה-`try` קטן וממוקד. **`finally`** הוא המקום לניקוי: סגירת קבצים וחיבורים.

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

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

- [Python Tutorial — Defining Clean-up Actions](https://docs.python.org/3/tutorial/errors.html#defining-clean-up-actions) — finally
- [The try statement](https://docs.python.org/3/reference/compound_stmts.html#the-try-statement) — המבנה המלא: try / except / else / finally

<!-- docs:end -->
