ההצהרה שמנקה אחרי עצמה
כל משאב שפותחים בתוכנית (קובץ, חיבור רשת, חיבור למסד נתונים, lock) צריך להיסגר כשמסיימים איתו. שוכחים, ואז יש דליפת זיכרון, locks שנשארים תפוסים וחוסמים תהליכים אחרים, או קבצים שנהרסים כשהתוכנית קורסת. הצהרת with של Python מטפלת בזה בשבילכם.
הדפוס שכולם מתחילים ממנו הוא קריאת קובץ:
with open("notes.txt") as f:
contents = f.read()
print(contents)
שני דברים קורים אוטומטית. בכניסה, open() נותנת אובייקט קובץ שקשור ל-f. ביציאה, בין אם הבלוק מסתיים כרגיל, חוזר מוקדם או זורק חריגה, Python קוראת בשבילכם ל-f.close().
זהו. זו כל המטרה של with.
מה "With" מחליפה
לפני context managers, הקוד הבטוח המקביל היה try/finally:
f = open("notes.txt")
try:
contents = f.read()
print(contents)
finally:
f.close()
חמש שורות של טקס בשביל "לקרוא קובץ ולסגור אותו בסוף". הכפילו את זה בכל open בתוכנית גדולה יותר, ותבינו את הקסם. with קצרה יותר, קשה יותר לטעות בה, ואי אפשר לשכוח איתה את הניקוי.
פתיחת כמה משאבים
אפשר לקשור כמה context managers ב-with אחת:
with open("input.txt") as src, open("output.txt", "w") as dst:
dst.write(src.read().upper())
שני הקבצים נפתחים בכניסה, ושניהם נסגרים ביציאה. אם ה-open הראשון מצליח והשני זורק חריגה, Python עדיין סוגרת את הראשון: המנגנון מטפל נכון גם בהקמה חלקית.
לרשימות ארוכות יותר של משאבים, הצורה עם סוגריים (Python 3.10 ומעלה) ברורה יותר:
with (
open("a.txt") as a,
open("b.txt") as b,
open("c.txt") as c,
):
...
מה זה בעצם context manager
כל אובייקט שמגדיר __enter__ ו-__exit__ הוא context manager. הפרוטוקול פשוט מאוד:
__enter__(self)רצה כשבלוק ה-withמתחיל. ערך ההחזרה שלה הוא מה ש-as nameקושר.__exit__(self, exc_type, exc_value, traceback)רצה כשהבלוק מסתיים, לא משנה איך. אם חריגה גרמה ליציאה, פרטי החריגה מועברים אליה כדי שה-context manager יוכל לבדוק אותה או להשתיק אותה.
הנה אחד מינימלי שמודד את זמן הבלוק שהוא עוטף:
with Timer(): יוצרת את האובייקט, קוראת ל-__enter__ שלו, מריצה את הגוף וקוראת ל-__exit__. בלי קובץ, בלי lock: רק עטיפה קטנה סביב "לעשות משהו ולמדוד כמה זמן זה לקח".
קיצור הדרך contextlib.contextmanager
להגדיר מחלקה לכל context manager זה כבד יותר ממה שצריך. contextlib.contextmanager הופכת פונקציית generator ל-context manager: yield אחד מפריד בין ה"לפני" ל"אחרי":
כל מה שלפני yield הוא ההתנהגות של __enter__. כל מה שאחריו הוא __exit__. ה-try/finally דואג שהניקוי ירוץ גם אם הגוף זורק חריגה.
רוב ה-context managers שתכתבו מתאימים לצורה הזו. התחילו מצורת ה-decorator; עברו למחלקה רק כשצריך משהו שצורת ה-generator לא יכולה לבטא.
שינוי זמני של משהו
צורה נפוצה: להגדיר משהו, להשתמש בו, ולשחזר. context managers מבטאים את זה בצורה נקייה:
כל דפוס של "להגדיר ואז לשחזר" (משתני סביבה, רמת פירוט הלוגים, feature flags, fixtures לבדיקות) נכנס באופן טבעי ל-context manager. מי שקורא לו לא צריך לזכור לשחזר שום דבר.
השתקת חריגות
המתודה __exit__ יכולה להחזיר True כדי לומר ל-Python "טיפלתי בחריגה; תבלעו אותה". זה נדיר ובדרך כלל סימן לבעיה, אבל כך עובדת contextlib.suppress:
suppress(FileNotFoundError) הופכת את ה-FileNotFoundError לפעולה שלא עושה כלום. השתמשו בה לפעולות שהן באמת אופציונליות: "נסו את זה, לא אכפת לי אם זה לא עובד". אל תשתמשו בה כדי להשתיק חריגות שלא חשבתם עליהן.
context managers נוספים שתפגשו
ברגע שמתחילים לחפש, context managers מופיעים בכל הספרייה הסטנדרטית:
import threading
from pathlib import Path
# Locks: מבטיחים שחרור גם אם הקטע הקריטי זורק חריגה.
lock = threading.Lock()
with lock:
...
# tempfile: מוחק את הקובץ הזמני כשמסיימים.
from tempfile import TemporaryDirectory
with TemporaryDirectory() as tmp:
path = Path(tmp) / "scratch.txt"
path.write_text("hello")
# חיבורים למסד נתונים: סוגרים את החיבור (או מסיימים את הטרנזקציה).
import sqlite3
with sqlite3.connect(":memory:") as conn:
conn.execute("CREATE TABLE t (x INTEGER)")
ספריות חיצוניות פועלות לפי אותן מוסכמות. כשרואים with something as x:, זה כמעט תמיד אומר "השתמשו ב-x למשך הבלוק הזה, ונקו אחר כך".
מתי לא להשתמש ב-with
- כשאין באמת הקמה ופירוק. לעטוף קוד שרירותי ב-context manager בלי סיבה רק מוסיף רעש.
- כשצריך את המשאב לאורך הרבה בלוקים שלא קשורים זה לזה. להחזיק
withפתוחה לכל אורך החיים של סקריפט ארוך יכול להסתיר מה טווח הניקוי האמיתי. שקלו במקום זאת מחלקה שהמשאב שייך לה. - כש-decorator מתאים יותר. חלק מהדפוסים החוזרים (ניסיון חוזר, לוג, מדידת זמן) נקראים טבעי יותר כ-
@decoratorעל פונקציה מאשר כ-with ...:בתוכה. בחרו במה שנקרא טוב יותר בנקודת הקריאה.
ברוב המקרים, with היא הבחירה הנכונה. את החריגים הנדירים קל לזהות ברגע שמחפשים אותם.
הבא בתור: עבודה עם קבצים אמיתיים
עכשיו אתם מכירים את המנגנון שמאחורי with open(...) as f:, שזה ההקשר שבו תשתמשו בו בתשעים אחוז מהמקרים. הפרק הבא מפעיל אותו לקריאה, כתיבה וניווט בקבצים בדיסק.
שאלות נפוצות
מה with open עושה ב-Python?
with open עושה ב-Python?with open(path) as f: פותחת את הקובץ וקושרת אותו ל-f למשך הבלוק. כשהבלוק מסתיים, באופן רגיל או בגלל חריגה, Python סוגרת את הקובץ אוטומטית. לא צריך f.close(); הצהרת with מבטיחה את זה.
למה להשתמש ב-with במקום ב-open() רגיל?
with במקום ב-open() רגיל?כי with סוגרת את הקובץ גם כשחריגה נזרקת באמצע הבלוק. עם open() רגיל האחריות עליכם לזכור לקרוא ל-close() בכל מסלול בקוד, כולל מסלולי שגיאה. with בטוחה וקצרה יותר.
איך פותחים כמה קבצים בהצהרת with אחת?
with אחת?הפרידו את ה-context managers בפסיקים: with open('a.txt') as a, open('b.txt') as b:. שני הקבצים נפתחים בכניסה ונסגרים ביציאה, בסדר הפוך. זה מחליף הצהרות with מקוננות כשצריך כמה משאבים בבת אחת.