Modules: למה מחלקים לקבצים, import, from ו-as

מטרות השיעור
  • להסביר מהו Module ולמה מחלקים פרויקט למספר קבצים
  • ליצור Module משלכם ולייבא ממנו פונקציות וקבועים
  • להבחין בין import x, from x import y ו-import x as z, ולדעת מתי להשתמש בכל אחד
  • להבין שקוד ברמה הראשית של Module רץ בזמן הייבוא

מה נלמד?

  • מהו 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 לא ייובא!), ולא מתחילים בספרה.
אנלוגיה: מטבח עם מגירות

קובץ אחד ענק = מגירה אחת שבה כל הכלים: סכינים, כפות, מלקחיים, מספריים. כדי למצוא משהו, מחפשים בהכול. מודולים = מגירות נפרדות עם תוויות: “סכו"ם”, “כלי אפייה”, “תבלינים”. import = “תביא לי מהמגירה הזו”.

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

צורהאיך משתמשים אחר כךמתי
import pricesprices.add_vat(100)ברירת המחדל. ברור מאיפה כל שם מגיע.
from prices import add_vatadd_vat(100)כשמשתמשים הרבה בכמה שמות ספציפיים.
import prices as p / from prices import add_vat as vatp.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).

תחביר

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

def add_vat(amount):
    return amount * (1 + VAT_RATE)
# 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:

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:

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

פלט:

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

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

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

דוגמה נוספת

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

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:

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")

פלט:

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 הוספתם בדיקה:

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

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

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

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

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

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

הטעות 3: ModuleNotFoundError.

ModuleNotFoundError: No module named 'prices'

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

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

תרגול

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

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

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

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

After discount: 180.0
With VAT: 210.6
הצג פתרון

prices.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

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(...).

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

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

הצג פתרון
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 — שם קצר למודול עם שם ארוך.

תרגיל 3: Standard Library

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

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

פלט צפוי:

9.0
3.14
2
הצג פתרון
import math
import random

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

random.seed(1)
print(random.randint(1, 6))
9.0
3.14
2

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

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

בקובץ helpers.py:

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

print("helpers loaded")

בקובץ main.py:

from helpers import shout
print(shout("hi"))
  1. מה יודפס כשמריצים python main.py, ולמה?
  2. איך מונעים את השורה הראשונה?
הצג פתרון
  1. יודפס:
helpers loaded
HI!

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

  1. מודול שמיועד לייבוא לא צריך קוד ברמה הראשית — רק הגדרות. מוחקים את ה-print. (בשיעור הבא נלמד את if __name__ == "__main__":, שמאפשר להשאיר קוד בדיקה שרץ רק בהרצה ישירה.)

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

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

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

פלט צפוי:

Eilat: 35 C / 95.0 F - hot
Haifa: 22 C / 71.6 F - pleasant
Jerusalem: 9 C / 48.2 F - cold
הצג פתרון

temperature.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

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 רק משתמש בה ומדפיס.

סיכום

  • 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 *.

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