# Scope — Local, Enclosing, Global ו-Built-in (LEGB)<no value>

## מה נלמד?

**Scope** (טווח הכרה) = **האזור בקוד שבו שם (משתנה או פונקציה) מוכר**. נלמד:

- **Local** — משתנים בתוך פונקציה.
- **Enclosing** — משתנים של פונקציה חיצונית, כשיש פונקציה בתוך פונקציה.
- **Global** — משתנים ברמה הראשית של הקובץ.
- **Built-in** — שמות מובנים של Python (`print`, `len`...).
- **LEGB** — הסדר שבו Python מחפשת שם.

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

אם כל משתנה היה נגיש מכל מקום, משתנה בשם `total` בפונקציה אחת היה דורס משתנה `total` בפונקציה אחרת, ובתוכנה של 10,000 שורות זה כאוס. Scope **מבודד** כל פונקציה: היא יכולה להשתמש בשמות שלה בלי לחשוש מהתנגשויות.

ומצד שני, שגיאות כמו `NameError` או `UnboundLocalError` מבלבלות מאוד אם לא מבינים Scope.

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

### Local — מקומי

משתנה שנוצר **בתוך פונקציה** (כולל הפרמטרים שלה) הוא **מקומי**:

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

### Global — גלובלי

משתנה שנוצר **ברמה הראשית של הקובץ** (לא בתוך פונקציה) הוא **גלובלי**:

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

### Enclosing — עוטף

אפשר להגדיר **פונקציה בתוך פונקציה**. הפנימית רואה את המשתנים של החיצונית. ה-Scope של החיצונית נקרא **Enclosing** מנקודת המבט של הפנימית.

### Built-in — מובנה

השמות ש-Python מכירה תמיד, בלי `import`: `print`, `len`, `sum`, `max`, `int`, `str`, `list`, `range`...

### סדר החיפוש: LEGB

כשהקוד משתמש בשם, Python מחפשת אותו **בסדר הזה**, ועוצרת **במקום הראשון** שבו מצאה:

```text
L  Local       → inside the current function
E  Enclosing   → inside outer functions (if nested)
G  Global      → top level of the file
B  Built-in    → Python's built-in names
              → not found anywhere: NameError
```

{{< box "analogy" "אנלוגיה: איפה המפתחות?" >}}
מחפשים מפתחות: קודם **בכיס** (Local), אחר כך **בתיק** שאתם נושאים (Enclosing), אחר כך **בבית** (Global), ולבסוף **בלובי של הבניין**, איפה שמשאירים לכולם (Built-in). ברגע שמצאתם, מפסיקים לחפש. ומפתח בכיס של חבר אתם לא רואים בכלל: זה ה-Local של פונקציה אחרת.
{{< /box >}}

## תחביר

```python
counter = 0                  # global

def outer():
    message = "hi"           # local to outer = enclosing for inner

    def inner():
        result = message + "!"   # local to inner; message found in Enclosing
        return result

    return inner()

def increase():
    global counter           # declare: assignments go to the GLOBAL counter
    counter = counter + 1
```

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

`week-2/scope_demo.py`:

```python
course = "Kodkod 6"


def welcome(name):
    message = "Welcome " + name + " to " + course
    return message


print(welcome("Dana"))
print(course)
print(message)
```

**פלט:**

```text
Welcome Dana to Kodkod 6
Kodkod 6
Traceback (most recent call last):
  File "scope_demo.py", line 11, in <module>
    print(message)
NameError: name 'message' is not defined
```

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

| שם | Scope | הסבר |
|---|---|---|
| `course` | **Global** | נוצר ברמה הראשית. `welcome` **קוראת** אותו: L (לא נמצא) ‏←‏ E (אין) ‏←‏ G (נמצא!). |
| `name` | **Local** של `welcome` | פרמטר = משתנה מקומי. קיים רק בזמן הקריאה. |
| `message` | **Local** של `welcome` | נוצר בתוך הפונקציה ונעלם כשהיא חוזרת. |
| `print`, `len`... | **Built-in** | נמצאים בסוף החיפוש. |
| `print(message)` בחוץ | — | ברמה הראשית אין `message`. הוא היה Local, וכבר נעלם, ולכן `NameError`. |

**הדרך הנכונה להוציא ערך מפונקציה היא `return`**, וזה בדיוק מה ש-`welcome` עושה. מי שקרא קיבל את הערך (ויכול לשמור אותו במשתנה משלו).

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

**אותו שם בכמה Scopes, ו-`global`.** `week-2/scope_levels.py`:

```python
total = 100


def local_total():
    total = 5
    print("inside local_total:", total)


def read_global():
    print("inside read_global:", total)


def change_global():
    global total
    total = total + 50
    print("inside change_global:", total)


def outer():
    label = "outer"

    def inner():
        print("inner sees enclosing label:", label)

    inner()


local_total()
print("after local_total:", total)
read_global()
change_global()
print("after change_global:", total)
outer()
```

**פלט:**

```text
inside local_total: 5
after local_total: 100
inside read_global: 100
inside change_global: 150
after change_global: 150
inner sees enclosing label: outer
```

**פירוק:**

- `local_total` — ההשמה `total = 5` יוצרת **משתנה מקומי חדש** בשם `total`, ש"מסתיר" את הגלובלי. הגלובלי **לא השתנה** (100).
- `read_global` — רק **קוראת** `total`. אין לה מקומי בשם הזה, ולכן לפי LEGB מגיעים לגלובלי.
- `change_global` — `global total` אומר ל-Python: "בפונקציה הזו, `total` הוא הגלובלי". עכשיו ההשמה משנה את הגלובלי (150).
- `inner` רואה את `label` של `outer`. זה ה-**E** ב-LEGB.

## טעות נפוצה

**הטעות 1: `UnboundLocalError` — לקרוא ואז לשנות גלובלי בלי `global`.**

```python
count = 0


def add_one():
    count = count + 1


add_one()
```

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

**למה?** כי בפונקציה יש **השמה** ל-`count`, Python מחליטה (מראש, לפני הריצה) ש-`count` הוא **מקומי** בכל הפונקציה. ואז `count + 1` מנסה לקרוא מקומי שעוד לא קיבל ערך.

**הטעות 2 (הגדולה): להשתמש ב-`global` כפתרון קבוע.** `global` "עובד", אבל:

- כל פונקציה יכולה לשנות את המשתנה, וכשיש באג, אין דרך לדעת מי שינה.
- הפונקציה תלויה במשהו "מבחוץ", ולכן קשה לבדוק אותה ולהשתמש בה שוב.

**הדרך הנקייה: פרמטר נכנס, `return` יוצא.**

```python
def add_one(count):
    return count + 1


count = 0
count = add_one(count)
print(count)   # 1
```

עכשיו ברור לגמרי מה נכנס ומה יוצא, והפונקציה לא נוגעת בשום דבר שלא קיבלה. (בנושא OOP נלמד דרך נוספת לנהל "מצב" שמשתנה: אובייקטים.)

**הטעות 3: לדרוס שם Built-in.**

```python
list = [1, 2, 3]
numbers = list(range(5))
```

```text
TypeError: 'list' object is not callable
```

`list` הגלובלי (שלנו) נמצא **לפני** ה-Built-in ב-LEGB, ולכן "הסתיר" את פונקציית ה-Built-in. אותו דבר עם `sum = 0`, `max = 10`, `input = ...`, `str = ...`. **לעולם אל תקראו למשתנים בשמות של Built-ins.**

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

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

### תרגיל 1: המשתנה שנעלם

```python
def welcome(name):
    message = "Welcome " + name
    return message

print(welcome("Dana"))
print(message)
```

1. מה יקרה בשורה האחרונה, ולמה?
2. תקנו כך שההודעה תודפס פעמיים.

{{< solution >}}
1. `NameError: name 'message' is not defined`. `message` הוא משתנה **Local** של `welcome` — הוא נוצר בכל קריאה ונעלם כשהפונקציה מסתיימת.
2. שומרים את הערך שהוחזר במשתנה משלנו:

```python
def welcome(name):
    message = "Welcome " + name
    return message


result = welcome("Dana")
print(result)
print(result)
```

```text
Welcome Dana
Welcome Dana
```
{{< /solution >}}

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

```python
x = "global"

def first():
    x = "local in first"
    print("1:", x)

def second():
    print("2:", x)

first()
second()
print("3:", x)
```

{{< solution >}}
```python
x = "global"


def first():
    x = "local in first"
    print("1:", x)


def second():
    print("2:", x)


first()
second()
print("3:", x)
```

```text
1: local in first
2: global
3: global
```

- `first` יוצרת `x` **מקומי** משלה — הגלובלי לא נגע.
- `second` לא מגדירה `x`, ולכן לפי **LEGB** מגיעים ל-Global.
{{< /solution >}}

### תרגיל 3: UnboundLocalError

```python
count = 0

def add_one():
    count = count + 1

add_one()
```

1. איזו שגיאה תתקבל, ולמה?
2. תקנו בלי `global`.

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

```python
def add_one(count):
    return count + 1


count = 0
count = add_one(count)
count = add_one(count)
print(count)
```

```text
2
```
{{< /solution >}}

### תרגיל 4: לדרוס שם מובנה

```python
list = [1, 2, 3]
numbers = list(range(5))
```

1. איזו שגיאה תתקבל?
2. הסבירו בעזרת LEGB למה זה קורה, ותקנו.

{{< solution >}}
1. `TypeError: 'list' object is not callable`.
2. לפי **LEGB**, Python מחפשת את `list` ב-**Global** לפני **Built-in**. המשתנה שלנו בשם `list` "הסתיר" את הפונקציה המובנית. התיקון — לא לתת למשתנים שמות של Built-ins:

```python
items = [1, 2, 3]
numbers = list(range(5))
print(items, numbers)
```

```text
[1, 2, 3] [0, 1, 2, 3, 4]
```
{{< /solution >}}

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

```python
cart = []
total = 0

def add(price):
    global total
    cart.append(price)
    total = total + price

add(100)
add(50)
print(cart, total)
```

בצעו Refactoring כך שאף פונקציה לא תשתמש ב-`global` ולא תשנה משתנים חיצוניים: כל פונקציה מקבלת בפרמטרים את מה שהיא צריכה ומחזירה תוצאה.

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

```text
[100, 50] 150
```

{{< solution >}}
```python
def add(cart, price):
    return cart + [price]


def cart_total(cart):
    return sum(cart)


cart = []
cart = add(cart, 100)
cart = add(cart, 50)
print(cart, cart_total(cart))
```

```text
[100, 50] 150
```

- `total` כבר לא "מצב" שצריך לתחזק — הוא **מחושב** מהעגלה.
- `add` מחזירה רשימה **חדשה** (`cart + [price]`) ולא משנה את הקיימת.
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- **Scope** = איפה שם מוכר. משתנה שנוצר בפונקציה (כולל פרמטרים) הוא **Local**, ונעלם בסוף הקריאה.
- **LEGB**: Local → Enclosing → Global → Built-in. נעצרים במקום הראשון שבו נמצא. אם לא נמצא, `NameError`.
- השמה בתוך פונקציה יוצרת משתנה **מקומי**, גם אם יש גלובלי באותו שם.
- `global x` מאפשר לשנות גלובלי מתוך פונקציה, אבל **עדיף להימנע**. פרמטר נכנס ו-`return` יוצא.
- לא נותנים למשתנים שמות של Built-ins (`list`, `sum`, `max`, `str`...).

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

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

- [Python Scopes and Namespaces](https://docs.python.org/3/tutorial/classes.html#python-scopes-and-namespaces) — Local, Enclosing, Global ו-Built-in
- [Resolution of names](https://docs.python.org/3/reference/executionmodel.html#resolution-of-names) — הסדר שבו Python מחפשת שם
- [The global statement](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement) — global

<!-- docs:end -->
