# Modules: למה מחלקים לקבצים, import, from ו-as<no value>

## מה נלמד?

- מהו **Module** (מודול).
- למה מחלקים פרויקט ל**כמה קבצים**.
- שלוש צורות ייבוא: `import`, `from ... import`, `as`.
- מה **בדיוק** קורה כש-Python מייבאת קובץ.

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

כבר השתמשנו במודולים: `import os`, `import logging`, `import socket`. היום נכתוב **משלנו**.

פרויקט אמיתי הוא מאות ואלפי שורות. קובץ אחד ענק הוא:

- **קשה לנווט** — איפה הפונקציה ששולחת מייל? שורה 1,840?
- **קשה לעבוד בצוות** — שלושה מפתחים עורכים את אותו קובץ ונתקלים זה בזה (ב-Git, מחר).
- **קשה לשימוש חוזר** — פונקציית ולידציה טובה "תקועה" בתוך קובץ של משחק.

חלוקה לקבצים היא **Separation of Concerns ברמת הפרויקט**. בנושא Backend ומסדי נתונים השרת יחולק בדיוק כך: `main.py` (Routes), `db.py` (גישה למסד), `models.py`.

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

### מהו Module?

**Module = כל קובץ `.py`.** זהו. כל קובץ Python שכתבתם הוא כבר מודול, ואפשר לייבא ממנו פונקציות, קבועים ומשתנים לקובץ אחר.

- שם המודול = שם הקובץ **בלי** `.py`. הקובץ `prices.py` ‏←‏ המודול `prices`.
- לכן שמות קבצים חייבים להיות שמות חוקיים ב-Python: `snake_case`, בלי מקפים (`my-utils.py` לא ייובא!), ולא מתחילים בספרה.

{{< box "analogy" "אנלוגיה: מטבח עם מגירות" >}}
קובץ אחד ענק = מגירה אחת שבה כל הכלים: סכינים, כפות, מלקחיים, מספריים. כדי למצוא משהו, מחפשים בהכול. **מודולים = מגירות נפרדות** עם תוויות: "סכו"ם", "כלי אפייה", "תבלינים". `import` = "תביא לי מהמגירה הזו".
{{< /box >}}

### שלוש צורות ייבוא

| צורה | איך משתמשים אחר כך | מתי |
|---|---|---|
| `import prices` | `prices.add_vat(100)` | ברירת המחדל. ברור מאיפה כל שם מגיע. |
| `from prices import add_vat` | `add_vat(100)` | כשמשתמשים הרבה בכמה שמות ספציפיים. |
| `import prices as p` / `from prices import add_vat as vat` | `p.add_vat(100)` / `vat(100)` | שם ארוך או התנגשות שמות. מוסכמות מוכרות: `import numpy as np`. |

### מה קורה בזמן `import`?

1. Python **מחפשת** את הקובץ: קודם בתיקייה של הסקריפט שהרצתם, ואחר כך בספריות של Python.
2. Python **מריצה את כל הקובץ מלמעלה למטה**, פעם אחת: יוצרת את הפונקציות והקבועים, ו**מריצה כל קוד ברמה הראשית**.
3. התוצאה נשמרת. `import` נוסף לאותו מודול **לא מריץ שוב**.

סעיף 2 חשוב מאוד, ונראה את ההשלכות שלו בטעות הנפוצה.

### `__pycache__`

אחרי ייבוא ראשון תופיע תיקייה בשם `__pycache__` עם קבצי `.pyc`. זה **ה-Bytecode** (נושא מחשב, מערכת הפעלה ורשת!) של המודולים, שנשמר כדי שבפעם הבאה הייבוא יהיה מהיר יותר. לא נוגעים בה, ובפרויקטי Git מתעלמים ממנה (נראה ב-`.gitignore` בנושא Git ו-GitHub).

## תחביר

```python
# file: prices.py   (the module)
VAT_RATE = 0.17

def add_vat(amount):
    return amount * (1 + VAT_RATE)
```

```python
# file: main.py     (uses the module; same folder)
import prices
prices.add_vat(100)

from prices import add_vat, VAT_RATE
add_vat(100)

import prices as p
p.add_vat(100)

from prices import add_vat as with_vat
with_vat(100)
```

**סדר מקובל בראש קובץ:** קודם ייבוא מהספרייה הסטנדרטית (`os`, `json`), שורה ריקה, ואז ייבוא מהמודולים שלכם.

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

צרו תיקייה `week-2/shop/`, ובה שני קבצים.

`week-2/shop/prices.py`:

```python
VAT_RATE = 0.17


def add_vat(amount):
    return round(amount * (1 + VAT_RATE), 2)


def apply_discount(amount, percent):
    return round(amount - amount * percent / 100, 2)
```

`week-2/shop/main.py`:

```python
import prices

base_price = 200
discounted = prices.apply_discount(base_price, 10)
final_price = prices.add_vat(discounted)

print("Base:", base_price)
print("After 10% discount:", discounted)
print("With VAT (" + str(prices.VAT_RATE) + "):", final_price)
```

**הרצה** (מתוך `week-2/shop`): `python main.py`

**פלט:**

```text
Base: 200
After 10% discount: 180.0
With VAT (0.17): 210.6
```

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

| שורה | הסבר |
|---|---|
| `import prices` | Python מחפשת `prices.py` **באותה תיקייה** של `main.py`, מריצה אותו, ויוצרת את השם `prices` שמייצג את המודול. |
| `prices.apply_discount(...)` | ניגשים לפונקציה **דרך** שם המודול, עם נקודה. בדיוק כמו `os.getcwd()`. |
| `prices.VAT_RATE` | גם קבועים נגישים כך. |
| `main.py` לא מגדיר שום פונקציית מחיר | כל הלוגיקה של המחירים נמצאת **במקום אחד**. אם המע"מ משתנה, משנים רק את `prices.py`. |

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

**שלוש צורות הייבוא, ומודול שני.** הוסיפו את `week-2/shop/validation.py`:

```python
MIN_NAME_LENGTH = 2


def is_valid_price(price):
    return isinstance(price, (int, float)) and price >= 0


def is_valid_name(name):
    return len(name.strip()) >= MIN_NAME_LENGTH
```

ועדכנו את `main.py`:

```python
import prices as p
from validation import is_valid_price, is_valid_name

products = [("Laptop", 3500), ("X", 20), ("Mouse", -5), ("Desk", 900)]

for name, price in products:
    if not is_valid_name(name):
        print("Skipping invalid name:", repr(name))
    elif not is_valid_price(price):
        print("Skipping invalid price for", name, ":", price)
    else:
        print(name, "->", p.add_vat(price), "NIS incl. VAT")
```

**פלט:**

```text
Laptop -> 4095.0 NIS incl. VAT
Skipping invalid name: 'X'
Skipping invalid price for Mouse : -5
Desk -> 1053.0 NIS incl. VAT
```

**פירוק:**

- `import prices as p` — כינוי קצר. ניגשים עם `p.add_vat`.
- `from validation import is_valid_price, is_valid_name` — מייבאים **רק** את שתי הפונקציות, ומשתמשים בהן ישירות, בלי קידומת.
- `repr(name)` — מציגה את הערך עם מירכאות (`'X'`), כמו `%r` בלוגים.
- **מבנה הפרויקט:** `prices.py` = לוגיקה של מחירים. `validation.py` = כללי תקינות. `main.py` = מחבר ומריץ. כל קובץ באחריות אחת.

## טעות נפוצה

**הטעות 1: קוד "בדיקה" ברמה הראשית של מודול.** נניח שבתחתית `prices.py` הוספתם בדיקה:

```python
print("Testing prices module:", add_vat(100))
```

עכשיו **כל** `import prices`, מכל קובץ, ידפיס את השורה הזו, כי ייבוא **מריץ את הקובץ**. בשיעור הבא נלמד את הפתרון: `if __name__ == "__main__":`.

**הטעות 2: שם קובץ שמתנגש עם מודול סטנדרטי.**

יצרתם קובץ בשם `random.py` (או `logging.py`, `json.py`), ובקובץ אחר כתבתם `import random`:

```text
AttributeError: module 'random' has no attribute 'randint'
```

Python מוצאת **קודם** את `random.py` **שלכם**, באותה תיקייה, ומייבאת אותו במקום המודול האמיתי. לעולם אל תקראו לקבצים בשמות של מודולים סטנדרטיים.

**הטעות 3: `ModuleNotFoundError`.**

```text
ModuleNotFoundError: No module named 'prices'
```

הסיבות הנפוצות: הקובץ לא באותה תיקייה כמו הסקריפט שהרצתם, שגיאת כתיב בשם, או שם קובץ עם מקף (`my-prices.py`).

**הטעות 4: `from module import *`.** מייבא **את כל** השמות מהמודול. לא ברור מאיפה כל שם הגיע, ושמות עלולים לדרוס זה את זה בשקט. מייבאים במפורש את מה שצריך.

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

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

### תרגיל 1: המודול הראשון

צרו קובץ `prices.py` עם הקבוע `VAT_RATE = 0.17` ועם שתי פונקציות: `add_vat(amount)` ו-`apply_discount(amount, percent)` (שתיהן מעגלות לשתי ספרות). בקובץ `main.py` ייבאו את המודול, חשבו מחיר של 200 אחרי 10% הנחה, ואז הוסיפו לו מע"מ.

**פלט צפוי (הרצת main.py):**

```text
After discount: 180.0
With VAT: 210.6
```

{{< solution >}}
**prices.py**

```py
VAT_RATE = 0.17


def add_vat(amount):
    return round(amount * (1 + VAT_RATE), 2)


def apply_discount(amount, percent):
    return round(amount - amount * percent / 100, 2)
```

**main.py**

```py
import prices

discounted = prices.apply_discount(200, 10)
final_price = prices.add_vat(discounted)
print("After discount:", discounted)
print("With VAT:", final_price)
```

`import prices` טוען את הקובץ `prices.py` שנמצא באותה תיקייה. ניגשים לתוכן שלו עם נקודה: `prices.add_vat(...)`.
{{< /solution >}}

### תרגיל 2: שלוש צורות ייבוא

כתבו את אותה שורה — חישוב מע"מ על 100 — בשלוש צורות הייבוא: `import`, `from ... import` ו-`import ... as`. מתי כדאי להשתמש בכל אחת?

{{< solution >}}
```py
import prices
print(prices.add_vat(100))

from prices import add_vat
print(add_vat(100))

import prices as p
print(p.add_vat(100))
```

- **`import prices`** — ברור תמיד מאיפה כל שם מגיע (`prices.add_vat`). מתאים כשמשתמשים בהרבה דברים מהמודול.
- **`from prices import add_vat`** — כותבים פחות. מתאים לכמה שמות ספציפיים שמשתמשים בהם הרבה.
- **`import prices as p`** — שם קצר למודול עם שם ארוך.
{{< /solution >}}

### תרגיל 3: Standard Library

בלי לכתוב מודול משלכם, השתמשו במודולים המובנים של Python:

1. `math` — שורש של 81, ו-`math.pi` מעוגל לשתי ספרות.
2. `random` — עם `random.seed(1)`, הגרילו מספר בין 1 ל-6.

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

```text
9.0
3.14
2
```

{{< solution >}}
```python
import math
import random

print(math.sqrt(81))
print(round(math.pi, 2))

random.seed(1)
print(random.randint(1, 6))
```

```text
9.0
3.14
2
```

`math` ו-`random` שייכים ל-**Standard Library** — מגיעים עם Python, ומייבאים אותם בדיוק כמו מודול שכתבנו בעצמנו. `random.seed(1)` מקבע את ההגרלה, כדי שהתוצאה תהיה זהה בכל הרצה.
{{< /solution >}}

### תרגיל 4: קוד שרץ בזמן import

בקובץ `helpers.py`:

```text
def shout(text):
    return text.upper() + "!"

print("helpers loaded")
```

בקובץ `main.py`:

```text
from helpers import shout
print(shout("hi"))
```

1. מה יודפס כשמריצים `python main.py`, ולמה?
2. איך מונעים את השורה הראשונה?

{{< solution >}}
1. יודפס:

```text
helpers loaded
HI!
```

**`import` מריץ את כל הקובץ** — כולל `print` שנמצא ברמה הראשית. גם `from helpers import shout` מריץ את כל `helpers.py`, לא רק את הפונקציה.

2. מודול שמיועד לייבוא לא צריך קוד ברמה הראשית — רק הגדרות. מוחקים את ה-`print`. (בשיעור הבא נלמד את `if __name__ == "__main__":`, שמאפשר להשאיר קוד בדיקה שרץ רק בהרצה ישירה.)
{{< /solution >}}

### תרגיל 5: מודול טמפרטורות

צרו מודול `temperature.py` עם `c_to_f(celsius)` ועם `describe(celsius)` שמחזירה `"hot"` מ-30 מעלות, `"cold"` מתחת ל-15, ו-`"pleasant"` בין לבין. ב-`main.py` הדפיסו לכל עיר את הטמפרטורה בשתי היחידות ואת התיאור.

```py
cities = ["Eilat", "Haifa", "Jerusalem"]
temperatures = [35, 22, 9]
```

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

```text
Eilat: 35 C / 95.0 F - hot
Haifa: 22 C / 71.6 F - pleasant
Jerusalem: 9 C / 48.2 F - cold
```

{{< solution >}}
**temperature.py**

```py
HOT_FROM = 30
COLD_BELOW = 15


def c_to_f(celsius):
    return round(celsius * 9 / 5 + 32, 1)


def describe(celsius):
    if celsius >= HOT_FROM:
        return "hot"
    if celsius < COLD_BELOW:
        return "cold"
    return "pleasant"
```

**main.py**

```py
from temperature import c_to_f, describe

cities = ["Eilat", "Haifa", "Jerusalem"]
temperatures = [35, 22, 9]

for i in range(len(cities)):
    celsius = temperatures[i]
    print(cities[i] + ":", celsius, "C /", c_to_f(celsius), "F -", describe(celsius))
```

הלוגיקה של הטמפרטורות נמצאת במודול, ו-`main.py` רק משתמש בה ומדפיס.
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- **Module = קובץ `.py`.** שם המודול = שם הקובץ בלי `.py` (`snake_case`, בלי מקפים).
- `import m` → `m.func()`. `from m import func` → `func()`. `import m as x` → `x.func()`.
- Python מחפשת מודולים קודם **בתיקיית הסקריפט**. לא קוראים לקבצים `random.py`, `json.py` וכו'.
- ייבוא **מריץ את המודול** פעם אחת. קוד ברמה הראשית רץ גם כשמייבאים.
- חלוקה לקבצים = Separation of Concerns ברמת הפרויקט. בלי `import *`.

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

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

- [Python Tutorial — Modules](https://docs.python.org/3/tutorial/modules.html) — import, from ו-as
- [More on Modules](https://docs.python.org/3/tutorial/modules.html#more-on-modules) — from module import name

<!-- docs:end -->
