יסודות התכנות בשפת Python · פרק 11 — נושאי רשות⌂📚🏠
פרק 11

נושאי רשות

הפרק הזה הוא פרק רשות: נושאיו מומלצים בתכנית הלימודים, אבל אינם חובה, שעותיהם אינן נספרות בסך השעות שלה, ואין עליהם שאלות בבחינת הבגרות. עם זאת, הם הצעד הטבעי אחרי הפרקים הקודמים: כל תוכנית אמיתית צריכה לחפש מהר, לשמור בקובץ את מה שלמדה, ולהמשיך לעבוד כשמגיע אליה טקסט פגום. נלמד כאן ארבעה נושאים, ובסוף נחבר אותם לשלב חדש בתחנת המדידה.

בפרק הזה:
  • נשתמש בקבוצה set כשחשוב לנו אם האיבר קיים, ולא היכן הוא.
  • נשתמש במילון dict כדי להגיע לערך בעזרת מפתח.
  • נבנה מחלקה על מחלקה בירושה, ונכתוב super, __add__ ומחלקות מופשטות.
  • נקרא מקובצי טקסט ונכתוב אליהם, וננוע בתוכם בעזרת seek ו-tell.
  • נבחין בין שגיאת תחביר לחריג, ונטפל בחריג בעזרת try, except ו-finally.
  • נכתוב תוכנית שקוראת את יומן התחנה, מדלגת על שורות פגומות ושומרת סיכום בקובץ.
הנושאהסעיפיםהשעות המוצעות בתכנית הלימודים
מבני נתונים נוספים11.1 ו-11.210
ירושה של מחלקות11.315
עבודה עם קבצים11.410
טיפול בחריגים11.510
הערה: המעבדה באתר אינה עובדת עם קבצים, ולא עם try ו-import. לכן מריצים את התוכניות של הפרק הזה בפייתון על המחשב.

11.1 מבנה נתונים מטיפוס Set (set)

ברשימה ובטאפל (Tuple) מפרק 6 יש סדר ואינדקסים. אבל לפעמים אין לנו צורך בסדר ולא בכפילויות. בלוח ארדואינו, למשל, אנחנו רוצים לדעת: האם ההדק 7 תפוס? לא חשוב מתי תפסנו אותו, ואין טעם לתפוס אותו פעמיים. זה תפקידה של הקבוצה.

אתחול, מבנה ריק, set, הדפסה:

קבוצה (Set): מבנה נתונים ששומר איברים בלי כפילויות ובלי סדר. כותבים אותה בין סוגריים מסולסלים { }, והיא עצם של המחלקה set.
pins = {13, 2, 7, 2, 13}
print(pins)
print(len(pins))
empty = set()
print(empty, type(empty))
other = {}
print(type(other))
פלט
{2, 13, 7}
3
set() <class 'set'>
<class 'dict'>

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

טעות נפוצה: קבוצה ריקה נכתבת set(). הסוגריים הריקים {} נותנים מילון ריק, כפי שרואים בשורה האחרונה של הפלט.

הפעולה set ממירה לקבוצה כל דבר שאפשר לעבור עליו: רשימה, טאפל, range, ואפילו מחרוזת. זו הדרך הקצרה ביותר לדעת כמה ערכים שונים יש ברשימה:

samples = [5, 3, 5, 5, 8, 3]
print(set(samples))
print(len(samples), len(set(samples)))
print(set(range(1, 4)))
print(sorted(set("10110010")))
פלט
{8, 3, 5}
6 3
{1, 2, 3}
['0', '1']

בשורה האחרונה השתמשנו בפעולה המוכנה sorted, שנותנת רשימה חדשה ממוינת. עם מחרוזות יש לכך סיבה נוספת: הסדר שלהן בתוך הקבוצה משתנה מהרצה להרצה של אותה תוכנית, ולכן בספר הזה איננו מדפיסים קבוצת מחרוזות אלא אחרי sorted.

טעות נפוצה: אין סדר, ולכן אין אינדקס: pins[0] עוצר את התוכנית. השאלה היחידה על איבר היא: האם הוא קיים? ואיברי הקבוצה חייבים להיות בלתי ניתנים לשינוי: מספר, מחרוזת וטאפל מותרים, אבל רשימה לא.

הוספת איברים על ידי add:

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

דוגמה 11.1
דרך התקשורת הטורית הגיעו מסגרות בעלות שם, וחלקן נשלחו שוב. נדפיס כל חזרה, ואחר כך את מספר המסגרות השונות.
פתרון:
frames = ["A7", "B2", "A7", "C4", "B2", "A7"]
seen = set()
for frame in frames:
    if frame in seen:
        print("Repeated:", frame)
    else:
        seen.add(frame)
print(len(seen), "different frames")
פלט
Repeated: A7
Repeated: B2
Repeated: A7
3 different frames

קבוצה בשם seen היא תבנית שימושית מאוד: "מה כבר ראינו?". והשאלה in מהירה בה גם אם יש בה מיליון איברים, כי פייתון הולכת ישר למקומו של האיבר, ואילו ברשימה היא עוברת על האיברים אחד אחד.

לולאת for העוברת על האיברים:

לולאת for עוברת על כל איבר פעם אחת, בסדר הפנימי של הקבוצה. אם הסדר חשוב לנו, נעבור על sorted(pins):

pins = {13, 2, 7}
for pin in pins:
    print("Pin", pin, "is an output")
for pin in sorted(pins):
    print(pin, end=" ")
print()
פלט
Pin 2 is an output
Pin 13 is an output
Pin 7 is an output
2 7 13 

פעולות המחלקה:

פקודהמה היא עושה
x in sהאם האיבר x נמצא בקבוצה: True או False
s.add(x)מוסיפה איבר אחד
s.update(t)מוסיפה את כל איברי t ל-s עצמה, ואינה מחזירה דבר
s.union(t)מחזירה קבוצה חדשה עם האיברים של שתיהן, ו-s אינה משתנה
s.discard(x)מוחקת את x אם הוא קיים, ואין שגיאה אם לא
s.pop()מוחקת איבר כלשהו ומחזירה אותו, ואי אפשר לדעת מראש איזה
s.clear()מרוקנת את הקבוצה

הפעולה union נותנת קבוצה חדשה עם כל מה שיש בשתי הקבוצות, והאיבר המשותף נמצא בה פעם אחת. שתי הקבוצות המקוריות נשארות כפי שהיו:

board_aboard_b2345{2, 3, 4, 5}union
ההדק 4 משמש בשני הלוחות, אבל מופיע באיחוד פעם אחת
board_a = {2, 3, 4}
board_b = {4, 5}
both = board_a.union(board_b)
print(both)
print(board_a)
print(board_b)
print(4 in both, 9 in both)
פלט
{2, 3, 4, 5}
{2, 3, 4}
{4, 5}
True False
טעות נפוצה: לעומתה, update משנה את הקבוצה עצמה ואינה מחזירה דבר. אם נשים את התוצאה שלה במשתנה נקבל None, והקבוצה תאבד:
busy = {2, 3}
busy.update([4, 5, 5])
print(busy)
busy = busy.update({9})
print(busy)
busy.add(5)
פלט
{2, 3, 4, 5}
None
AttributeError: 'NoneType' object has no attribute 'add'

הפעולה discard מוחקת איבר ואינה מתלוננת אם לא מצאה אותו, בניגוד ל-remove, שפועלת כמו remove ברשימות ועוצרת בשגיאה. הפעולה pop שולפת איבר שאיננו בוחרים: נשארו כאן 2 ו-9, והיא לקחה את 9. היא מתאימה כשכל איבר טוב, כמו הדק פנוי כלשהו. על קבוצה ריקה היא עוצרת בשגיאה:

busy = {2, 3, 9}
busy.discard(3)
busy.discard(100)
print(busy)
print("Took", busy.pop())
print(busy)
busy.clear()
print(busy, len(busy))
busy.pop()
פלט
{9, 2}
Took 9
{2}
set() 0
KeyError: 'pop from an empty set'
תרגול מהיר — קבוצה
1len({1, 1, 2})
23 in {1, 2}
3len(set("ABBA"))
4sorted({3, 1, 2})
5len(set())
6type({}).__name__
תשובות: 1 2 2 False 3 2 4 [1, 2, 3] 5 0 6 dict
שאלה 11.1: מה מדפיסה התוכנית הבאה?
a = {1, 2}
b = a.union({2, 3})
a.add(5)
a.discard(1)
a.discard(7)
print(len(a), len(b))
print(5 in b, 3 in b)
print(sorted(a), sorted(b))

11.2 מבנה נתונים מטיפוס Dictionary (dict)

ברשימה מגיעים לאיבר בעזרת מספר: values[2]. אבל מה פירוש המקום 2 ברשימת קריאות? צריך לזכור שהטמפרטורה היא השלישית. במילון, לעומת זאת, מגיעים לערך לפי שמו: station["TMP"].

אתחול, מבנה ריק, הדפסה:

מילון (Dictionary): מבנה נתונים ששומר זוגות, ובכל זוג מפתח וערך. מגיעים לערך בעזרת המפתח שלו. זהו עצם של המחלקה dict.
keyvalue"BAT"12.42"TMP"31.5"SOL"18.0
המילון station: כל מפתח מוביל לערך אחד

הכתיבה d[key] = value מוסיפה זוג חדש אם המפתח אינו קיים, ומשנה את ערכו אם הוא קיים. הצורה אחת, והפעולה תלויה במה שיש:

station = {"BAT": 12.42, "TMP": 31.5}
print(station["TMP"])
station["SOL"] = 18.0
station["TMP"] = 33.0
print(station)
print(len(station))
empty = {}
print(empty, type(empty))
פלט
31.5
{'BAT': 12.42, 'TMP': 33.0, 'SOL': 18.0}
3
{} <class 'dict'>
טעות נפוצה: מפתח שאינו קיים עוצר את התוכנית. לכן שואלים קודם בעזרת in, שמחפש במפתחות בלבד ולא בערכים, כפי שרואים בשורה השנייה:
station = {"BAT": 12.42, "TMP": 31.5}
print("TMP" in station)
print(12.42 in station)
print(station["HUM"])
פלט
True
False
KeyError: 'HUM'

הוספת איברים:

דוגמה 11.2
צבעי נגד: נקרא שלושה צבעים. הראשון והשני הם הספרות, והשלישי הוא החזקה של עשר. הטבלה שאנחנו זוכרים בעל פה הופכת למילון.
פתרון:
brown1black0red× 10²
הנגד 10 · 102 = 1000 Ω. הפס הרביעי הוא הסבילות, והוא אינו נכנס לחישוב
CODES = {"black": 0, "brown": 1, "red": 2,
         "orange": 3, "yellow": 4, "green": 5,
         "blue": 6, "violet": 7, "grey": 8,
         "white": 9}
b1 = input("Band 1: ")
b2 = input("Band 2: ")
b3 = input("Band 3: ")
digits = CODES[b1] * 10 + CODES[b2]
ohms = digits * 10 ** CODES[b3]
print(ohms, "ohm")
פלט
Band 1: brown
Band 2: black
Band 3: red
1000 ohm

ולקלט yellow violet orange היא מדפיסה: 47000 ohm. אילו כתבנו זאת בעזרת elif, היינו צריכים עשרה ענפים לכל פס. המילון הפך את כל זה לנתונים.

לולאת for העוברת על האיברים:

לולאת for עוברת על המפתחות. אם רוצים מפתח וערך יחד, עוברים על d.items() ומפרקים כל זוג לשני משתנים, כמו שפירקנו טאפל בפרק 6. הפעולות keys ו-values נותנות את המפתחות בלבד או את הערכים בלבד:

station = {"BAT": 12.42, "TMP": 31.5, "SOL": 18.0}
for name in station:
    print(name, station[name])
for name, value in station.items():
    print(name, "=", value)
print(list(station.keys()))
print(list(station.values()))
פלט
BAT 12.42
TMP 31.5
SOL 18.0
BAT = 12.42
TMP = 31.5
SOL = 18.0
['BAT', 'TMP', 'SOL']
[12.42, 31.5, 18.0]

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

counts = {}
for part in "R1,C1,R2".split(","):
    kind = part[0]
    if kind in counts:
        counts[kind] += 1
    else:
        counts[kind] = 1
print(counts)
מעקב אחרי מילון המונים
פקודהpartkindcountskind in countsפלט
counts = {}{}
for part in "R1,C1,R2".split(","):'R1'
kind = part[0]'R'
if kind in counts:False
counts[kind] = 1{'R': 1}
for part in "R1,C1,R2".split(","):'C1'
kind = part[0]'C'
if kind in counts:False
counts[kind] = 1{'R': 1, 'C': 1}
for part in "R1,C1,R2".split(","):'R2'
kind = part[0]'R'
if kind in counts:True
counts[kind] += 1{'R': 2, 'C': 1}
for part in "R1,C1,R2".split(","):
print(counts){'R': 2, 'C': 1}

לענף הכפול הזה יש קיצור: counts.get(kind, 0) נותנת את הערך אם המפתח קיים, ואחרת את הערך שקבענו, בלי שגיאה. כך כותבים: counts[kind] = counts.get(kind, 0) + 1.

פעולות המחלקה: pop, popitem, del, clear, copy:

פקודהמה היא עושה
d.pop(k)מוחקת את המפתח k ומחזירה את ערכו, ו-KeyError אם אינו קיים
d.pop(k, x)כמו הקודמת, אבל מחזירה x במקום שגיאה אם אינו קיים
d.popitem()מוחקת את הזוג האחרון שנוסף ומחזירה אותו כטאפל, ו-KeyError אם המילון ריק
del d[k]פקודה ולא פעולה: מוחקת את הזוג, ואינה מחזירה דבר
d.clear()מרוקנת את המילון
d.copy()מחזירה מילון חדש עם אותם זוגות
cfg = {"rate": 9600, "port": "COM3",
       "bits": 8, "echo": True}
print(cfg.pop("echo"))
print(cfg)
print(cfg.pop("mode", "none"))
print(cfg.popitem())
print(cfg)
del cfg["port"]
print(cfg)
cfg.clear()
print(cfg, len(cfg))
פלט
True
{'rate': 9600, 'port': 'COM3', 'bits': 8}
none
('bits', 8)
{'rate': 9600, 'port': 'COM3'}
{'rate': 9600}
{} 0

ראינו בסעיף 6.4 שהשמה אינה מעתיקה רשימה, וכך גם מילון: same = base נותן לאותו עצם שם שני. לעומת זאת, copy בונה מילון חדש, ושינוי במקור אינו מגיע אליו. היא מעתיקה את הזוגות בלבד, ולכן אם הערך הוא רשימה, שני המילונים מצביעים על אותה רשימה.

base = {"rate": 9600, "bits": 8}
same = base
twin = base.copy()
base["rate"] = 115200
print(same["rate"])
print(twin["rate"])
print(same is base, twin is base)
פלט
115200
9600
True False
טעות נפוצה: לא מוחקים ממילון תוך כדי מעבר עליו. הפתרון הוא לעבור על רשימת מפתחות מועתקת: for name in list(loads):.
loads = {"LED": 0, "FAN": 5, "PUMP": 0}
for name in loads:
    if loads[name] == 0:
        del loads[name]
פלט
RuntimeError: dictionary changed size during iteration
רשימהטאפלקבוצהמילון
כתיבה[1, 2](1, 2){1, 2}{"a": 1}
ריק[]()set(){}
גישהבאינדקסבאינדקסin בלבדבמפתח
כפילויותמותרמותרלאבמפתח לא
שינויכןלאכןכן
תרגול מהיר — מילון, עם d = {"a": 1, "b": 2}
1d["b"]
2len(d)
3"a" in d
41 in d
5list(d)
6list(d.values())
תשובות: 1 2 2 2 3 True 4 False 5 ['a', 'b'] 6 [1, 2]
שאלה 11.2: מה מדפיסה התוכנית הבאה?
d = {"R": 3, "C": 1}
e = d
f = d.copy()
d["L"] = 2
e["R"] = 9
print(len(d), len(f))
print(d["R"], f["R"])
print(d.popitem())
print("L" in e)

11.3 ירושה של מחלקות (inheritance)

המהות: ירושה כיחס בין שתי מחלקות:

אנחנו רוצים מחלקה לנגדים ומחלקה לחוטים. לכל רכיב יש שם על הלוח, ולנגד יש גם ערך. אילו כתבנו אותן כמו בפרק 8, היינו חוזרים על אותן שורות. והחזרה הזאת אומרת משהו: שני אלה הם רכיבים אלקטרוניים. נכתוב פעם אחת את המשותף, במחלקה בשם Component, ונבנה עליה.

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

היררכיה ברורה וחד-משמעית:

מחלקות שיורשות זו מזו יוצרות היררכיה, שמציירים כעץ. החץ יוצא מן המחלקה היורשת אל המחלקה שממנה היא יורשת:

objectComponentnameshow()WireResistorohmsshow()
כל מחלקה כותבת רק את מה שהיא מוסיפה, ואת השאר היא מקבלת ממי שמעליה

היררכיה טובה עונה בבירור על שאלה אחת: האם כל עצם של המחלקה הזו הוא עצם של ההיא? קוראים את המשפט בקול, ואם הוא נשמע מוזר, הירושה שגויה.

המשפטהתשובהמה כותבים
נגד הוא רכיב אלקטרונינכוןירושה: class Resistor(Component)
תחנה היא חיישןשגוי: יש בתחנה חיישניםתכונה: רשימת חיישנים בתוך Station, כמו בפרק 9
נגד הוא חוטשגוישתי מחלקות אחיות, בלי ירושה ביניהן

המונחים super ו-subclass:

מחלקת בסיס: המחלקה שממנה יורשים, ונקראת גם superclass.
מחלקה נגזרת: המחלקה שיורשת, ונקראת גם subclass. כותבים את שם מחלקת הבסיס בסוגריים אחרי שמה.

פעולה באותו שם במחלקה הנגזרת תופסת את מקום הפעולה של מחלקת הבסיס, וזו דריסה. כדי להגיע לפעולה של מחלקת הבסיס עצמה קיימת הפעולה super(). והפקודה pass פירושה גוף ריק בכוונה:

class Component:
    def __init__(self, name):
        self.name = name

    def show(self):
        print("Part:", self.name)

class Wire(Component):
    pass

class Resistor(Component):
    def __init__(self, name, ohms):
        super().__init__(name)
        self.ohms = ohms

    def show(self):
        super().show()
        print("Value:", self.ohms, "ohm")

w = Wire("W1")
r = Resistor("R1", 470)
w.show()
r.show()
print(r.name, r.ohms)
פלט
Part: W1
Part: R1
Value: 470 ohm
R1 470

ב-w.show() פייתון מחפשת את show ב-Wire, לא מוצאת, ועולה אל Component. ב-r.show() היא מוצאת אותה ב-Resistor, והשורה הראשונה שלה מזמנת את הפעולה של מחלקת הבסיס וממשיכה. החיפוש מתחיל תמיד במחלקה של העצם, ועולה בהיררכיה עד שהוא מוצא.

טעות נפוצה: אם כתבנו __init__ במחלקה הנגזרת ושכחנו את super().__init__, לעצם לא יהיו התכונות של מחלקת הבסיס. השגיאה AttributeError תופיע רחוק מן המקום, בשימוש הראשון בתכונה.

מחלקה קדומה בשם object:

כל מחלקה יורשת בסופו של דבר ממחלקה מוכנה בשם object, גם אם לא כתבנו זאת, ולכן הכתיבה class Component: פירושה class Component(object):. מ-object באה למשל הגרסה המוכנה של __str__ שראינו בפרק 8. גם מספרים ומחרוזות הם עצמים של מחלקות שיורשות ממנה.

הפעולה issubclass:

הפעולה issubclass(a, b) שואלת על שתי מחלקות: האם הראשונה נגזרת מן השנייה? הפעולה isinstance(x, c) שואלת על עצם ומחלקה. והתכונה __bases__ נותנת את המחלקות שמהן המחלקה יורשת ישירות:

class Component:
    pass

class Resistor(Component):
    pass

print(Component.__bases__)
r = Resistor()
print(r)
print(issubclass(Resistor, Component))
print(issubclass(Component, Resistor))
print(issubclass(Resistor, object))
print(isinstance(r, Component))
print(type(r) == Component)
print(issubclass(r, Component))
פלט
(<class 'object'>,)
<__main__.Resistor object at 0x000001F2A4C8>
True
False
True
True
False
TypeError: issubclass() arg 1 must be a class

שתי השורות הראשונות מראות את object ואת ההדפסה שירשנו ממנה, והכתובת קבועה רק בספר הזה. היחס הוא בכיוון אחד: כל נגד הוא רכיב, ולא כל רכיב הוא נגד. השורה שלפני השגיאה חשובה: type(r) == Component שואל על המחלקה המדויקת, ואילו isinstance מקבלת גם מחלקות נגזרות, ולכן עדיף להשתמש בה. השגיאה נובעת מכך שנתנו ל-issubclass עצם במקום מחלקה.

ירושה מרובה:

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

class PowerSource:
    def info(self):
        return "Source"

class Logger:
    def info(self):
        return "Logger"

class SmartBattery(PowerSource, Logger):
    pass

class Other(Logger, PowerSource):
    pass

print(SmartBattery().info())
print(Other().info())
for c in SmartBattery.__mro__:
    print(c.__name__)
פלט
Source
Logger
SmartBattery
PowerSource
Logger
object
טעות נפוצה: אם לשתי מחלקות הבסיס יש פעולה באותו שם ובמשמעות שונה, נקבל אחת מהן בלי שום אזהרה, כפי ש-Other מראה. לכן משתמשים בירושה מרובה כשמחלקות הבסיס קטנות ובלתי תלויות, ולכל אחת תפקיד משלה.

רב-צורתיות, פולימורפיזם:

פולימורפיזם: אותה פקודה מבצעת דבר שונה לפי טיפוס העצם שמקבל אותה.

העכבה שונה מרכיב לרכיב. בנגד היא R ואינה תלויה בתדר, ואילו בקבל ובסליל גודלה הוא ההיגב:

XC = 12·π·f·C
XL = 2·π·f·L

נכתוב לכל מחלקה גרסה משלה של impedance, ונשים את הרכיבים ברשימה אחת. הלולאה לעולם אינה שואלת על הטיפוס, וכל עצם מבצע את הגרסה שלו:

from math import pi

class Component:
    def __init__(self, name, value):
        self.name = name
        self.value = value

class Resistor(Component):
    def impedance(self, f):
        return self.value

class Capacitor(Component):
    def impedance(self, f):
        return 1 / (2 * pi * f * self.value)

class Inductor(Component):
    def impedance(self, f):
        return 2 * pi * f * self.value

parts = [Resistor("R1", 1000),
         Capacitor("C1", 0.000001),
         Inductor("L1", 0.1)]
for f in [50, 1000]:
    print("f =", f, "Hz")
    for part in parts:
        z = part.impedance(f)
        print(" ", part.name, round(z, 1), "ohm")
פלט
f = 50 Hz
  R1 1000 ohm
  C1 3183.1 ohm
  L1 31.4 ohm
f = 1000 Hz
  R1 1000 ohm
  C1 159.2 ohm
  L1 628.3 ohm

כשהתדר גדל, ההיגב של הקבל קטן ושל הסליל גדל, והנגד נשאר כפי שהיה. אם מחר נוסיף מחלקה לרכיב אחר, הלולאה לא תשתנה באות אחת. ובפייתון אפילו אין צורך בירושה לשם כך: די בכך שבזמן הזימון יש לעצם פעולה בשם הזה, ואחרת התוכנית נעצרת באותו רגע.

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

פעולה הורסת (destructor):

פעולה הורסת: הפעולה __del__: פייתון מזמנת אותה בעצמה כשהיא עומדת להסיר את העצם מן הזיכרון, והיא ההפך של __init__.

הפקודה del אינה הורסת את העצם, אלא מוחקת את השם בלבד. העצם נעלם כשלא נשאר שם שמצביע עליו. לכן לא קורה דבר במחיקת השם הראשון, אלא במחיקת השני:

class Port:
    def __init__(self, name):
        self.name = name
        print("Open", self.name)

    def __del__(self):
        print("Close", self.name)

a = Port("COM3")
b = a
del a
print("Still open")
del b
print("After second del")
פלט
Open COM3
Still open
Close COM3
After second del
טעות נפוצה: הפלט הזה קבוע בפייתון שאנחנו משתמשים בה על המחשב, אבל שפת פייתון עצמה אינה מבטיחה מתי תזומן __del__, ואפילו לא שתזומן בסוף התוכנית. לכן מחקנו כאן את שני השמות בעצמנו. אין לסמוך עליה לסגירת קובץ או יציאה: לשם כך משתמשים ב-with מסעיף 11.4 או ב-finally מסעיף 11.5.

פעולות מופע, מחלקה וסטטיות (instance, class, static methods):

הסוגהפרמטר הראשוןמגיעה אלהשימוש
פעולת מופעselfתכונות העצם והמחלקה שלומה ששייך לעצם אחד
פעולת מחלקה @classmethodclsמשתני המחלקהדרך נוספת לבנות עצם
פעולה סטטית @staticmethodאיןהפרמטרים שלה בלבדחישוב שאינו זקוק לעצם
class Resistor:
    def __init__(self, ohms):
        self.ohms = ohms

    def power(self, volts):
        return volts ** 2 / self.ohms

    @classmethod
    def from_code(cls, code):
        return cls(int(code[:-1]) * 10 ** int(code[-1]))

    @staticmethod
    def parallel(a, b):
        return a * b / (a + b)

r = Resistor(50)
s = Resistor.from_code("472")
print(r.power(5))
print(s.ohms)
print(Resistor.parallel(100, 300))
print(r.parallel(100, 100))
פלט
0.5
4700
75.0
50.0

העמסת פעולות:

בשפות כמו C#‎ אפשר לכתוב באותה מחלקה שתי פעולות באותו שם עם פרמטרים שונים. בפייתון זה לא קיים. השם מצביע על פעולה אחת, וההגדרה השנייה תופסת את מקום הראשונה בלי אזהרה:

class Load:
    def power(self, volts):
        return volts ** 2 / 100

    def power(self, volts, amps):
        return volts * amps

d = Load()
print(d.power(12, 2))
print(d.power(12))
פלט
24
TypeError: Load.power() missing 1 required positional argument: 'amps'

הדרך הפייתונית היא פעולה אחת עם פרמטר בעל ערך ברירת מחדל, כפי שלמדנו בפרק 7. הערך המיוחד None פירושו "לא ניתן דבר", ושואלים עליו בעזרת is:

class Load:
    def __init__(self, ohms):
        self.ohms = ohms

    def power(self, volts, amps=None):
        if amps is None:
            return volts ** 2 / self.ohms
        return volts * amps

d = Load(100)
print(d.power(12))
print(d.power(12, 2))
print(d.power(amps=2, volts=5))
פלט
1.44
24
10

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

העמסת אופרטורים:

ראינו בפרק 8 שכתיבת __str__ גורמת ל-print לעבוד על העצם שלנו. באותה דרך גורמים לאופרטורים לעבוד עליו: כשכותבים a + b, פייתון מתרגמת זאת ל-a.__add__(b). יש גם __sub__ עבור -, __mul__ עבור *, __eq__ עבור ==, ו-__lt__ עבור <. ומה פירוש הסימן + בין שני נגדים? חיבור טורי:

class Res:
    def __init__(self, ohms):
        self.ohms = ohms

    def __add__(self, other):
        return Res(self.ohms + other.ohms)

    def __eq__(self, other):
        return self.ohms == other.ohms

    def __lt__(self, other):
        return self.ohms < other.ohms

    def __str__(self):
        return str(self.ohms) + " ohm"

a = Res(470)
b = Res(1000)
c = Res(470)
print(a + b + c)
print(a == c, a != b, a is c)
print(a < b)
פלט
1940 ohm
True True False
True
טעות נפוצה: הפרמטר other חייב להיות מהטיפוס שלנו: Res(100) + 220 עוצר את התוכנית, כי למספר אין ohms. ומי שכותב __eq__ מאבד את האפשרות לשים את העצם בקבוצה או להשתמש בו כמפתח במילון, אלא אם יכתוב גם __hash__.

מחלקות מופשטות, אבסטרקטיות:

מהי העכבה של "רכיב" שאיננו יודעים מה הוא? אין לשאלה משמעות. המחלקה Component קיימת כדי שירשו ממנה, ולא כדי לבנות ממנה עצמים.

מחלקה מופשטת: מחלקה שאי אפשר לבנות ממנה עצם ישירות, והיא קובעת פעולות שכל מחלקה נגזרת חייבת לכתוב.

בונים זאת בעזרת המודול המוכן abc: יורשים מ-ABC, ושמים @abstractmethod לפני כל פעולה שחייבים לכתוב במחלקות הנגזרות:

from abc import ABC, abstractmethod

class Component(ABC):
    def __init__(self, name):
        self.name = name

    @abstractmethod
    def impedance(self, f):
        pass

class Resistor(Component):
    def __init__(self, name, ohms):
        super().__init__(name)
        self.ohms = ohms

    def impedance(self, f):
        return self.ohms

r = Resistor("R1", 470)
print(r.name, r.impedance(50))
c = Component("X1")
פלט
R1 470
TypeError: Can't instantiate abstract class Component without an implementation for abstract method 'impedance'

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

שאלה 11.3: מה מדפיסה התוכנית הבאה? שימו לב איזו g פועלת בכל מקרה.
class A:
    def __init__(self):
        self.x = 1

    def f(self):
        return self.x + self.g()

    def g(self):
        return 10

class B(A):
    def g(self):
        return 20

class C(B):
    def __init__(self):
        super().__init__()
        self.x = 5

print(A().f(), B().f(), C().f())
print(issubclass(C, A), isinstance(A(), B))

11.4 עבודה עם קבצים (files)

כל מה שהכרנו עד כה נעלם כשהתוכנית מסתיימת. הקובץ, לעומת זאת, הוא טקסט ששמור בדיסק: התוכנית קוראת אותו היום, וממשיכה לכתוב בו מחר. נעבוד כאן עם קובצי טקסט, שאפשר לפתוח גם בפנקס הרשימות. כל תוכנית בספר רצה בתיקייה ריקה משלה, ורוב התוכניות בסעיף הזה עובדות על קובץ יומן בשם log.txt.

פתיחת קובץ טקסט, קריאת טקסט מקובץ:

פותחים, קוראים או כותבים, ואז סוגרים. הפעולה open נותנת עצם קובץ, והפעולה read בלי פרמטר נותנת את כל מה שנשאר בקובץ, כמחרוזת אחת:

f = open("log.txt")
text = f.read()
f.close()
print(text)
print(type(text))
print(len(text))
פלט
BAT=12.42
TMP=31.5
SOL=18.0

<class 'str'>
28

הקובץ הוא מחרוזת רגילה, ו-\n הוא תו אחד ממנה בסוף כל שורה. מאחר שבסוף הקובץ יש \n, ו-print מוסיפה שורה משלה, בפלט הופיעה שורה ריקה. נתיב מלא ב-Windows כותבים כמחרוזת גולמית: r"C:\data\log.txt".

הערה: אם יש בקובץ אותיות בעברית או בערבית, או סימנים כמו Ω, קובעים את הקידוד: open("a.txt", encoding="utf-8"). בלי זה Windows עלול לקרוא את הקובץ בקידוד אחר, ויופיעו אותיות מוזרות או שגיאה.

קריאת מספר מסוים של תווים: read(n):

תכנית הלימודים קוראת לזה "העמסה של read". כפי שראינו בסעיף 11.3 זו אינה העמסה, אלא פעולה אחת עם פרמטר אופציונלי: f.read(n) קוראת עד n תווים, וכל קריאה ממשיכה מהמקום שבו עצרה הקודמת. כשלא נשאר דבר היא נותנת מחרוזת ריקה. הפעולה repr שבשורה השלישית מראה את המחרוזת עם הסימנים שבה, ונחזור אליה כשנלמד את readline:

f = open("log.txt")
print(f.read(3))
print(f.read(6))
print(repr(f.read(1)))
print(f.read(3))
f.close()
פלט
BAT
=12.42
'\n'
TMP
דיוק: תכנית הלימודים אומרת "מספר בתים", וזה נכון רק במצב הבינארי "rb". במצב טקסט הפרמטר סופר תווים. ההבדל נראה בתו Ω, שתופס שני בתים. בקובץ כתוב 470Ω, ואנחנו קוראים ארבעה בכל אחד מן המצבים:
f = open("ohm.txt", encoding="utf-8")
print(f.read(4))
f.close()
f = open("ohm.txt", "rb")
print(f.read(4))
f.close()
פלט
470Ω
b'470\xce'

תצורות פתיחת קובץ:

הפרמטר השני של open הוא מצב הפתיחה, ואם משמיטים אותו הוא "r":

המצבהמשמעותאם הקובץ אינו קייםתוכן קובץ קייםמתחיל מ
"r"קריאה בלבדשגיאהנשארההתחלה
"w"כתיבה בלבדנוצרנמחק מידההתחלה
"a"הוספה בסוףנוצרנשארהסוף
"r+"קריאה וכתיבהשגיאהנשאר, והכתיבה דורסת אותוההתחלה

הפעולה write כותבת מחרוזת, ומחזירה את מספר התווים שכתבה. פתחנו כאן את הקובץ פעמיים במצב "w", ובפעם השנייה נמחק כל מה שהיה בו:

f = open("out.txt", "w")
print(f.write("BAT=12.42\n"))
f.close()
f = open("out.txt", "w")
f.write("NEW\n")
f.close()
f = open("out.txt")
print(f.read(), end="")
f.close()
פלט
10
NEW
טעות נפוצה: המצב "w" מוחק את הקובץ לפני שכתבנו תו אחד, ואינו שואל. הפעולה write אינה מוסיפה \n מעצמה, ואינה מקבלת אלא מחרוזת, ולכן ממירים מספר תחילה בעזרת str.

המצב "a" הוא מצב היומנים: כל הרצה של התוכנית מוסיפה את שורותיה לסוף הקובץ:

f = open("log.txt", "a")
f.write("HUM=40.0\n")
f.close()
f = open("log.txt")
print(f.read(), end="")
f.close()
פלט
BAT=12.42
TMP=31.5
SOL=18.0
HUM=40.0

המצב "r+" קורא וכותב באותו קובץ. הכתיבה תופסת את מקום התווים שנמצאים במקום המצביע, ואינה דוחפת אותם קדימה: אין "הכנסה" בקובץ. כאן נחליף את AUTO במילה באותו אורך:

f = open("cfg.txt", "r+")
print(f.readline(), end="")
f.seek(5)
f.write("HAND")
f.seek(0)
print(f.read(), end="")
f.close()
פלט
MODE=AUTO
MODE=HAND
RATE=9600

סגירת קובץ טקסט:

למה סוגרים? כי write אינה כותבת לדיסק מיד: היא אוספת את מה שכתבנו בזיכרון זמני, ו-close כותבת את מה שנאסף ומשחררת את הקובץ. לכן קריאה לפני הסגירה מוצאת קובץ ריק, ואחריה מוצאת את הטקסט. התכונה closed אומרת אם הקובץ נסגר, וכתיבה אחרי הסגירה עוצרת בשגיאה:

out = open("a.txt", "w")
out.write("saved?")
check = open("a.txt")
print(repr(check.read()))
out.close()
print(repr(check.read()))
check.close()
print(out.closed)
out.write("more")
פלט
''
'saved?'
True
ValueError: I/O operation on closed file.

מצביע הקובץ, seek:

מצביע הקובץ: מספר שעצם הקובץ שומר: המקום של הקריאה או הכתיבה הבאה. בדרך כלל הוא מתחיל מאפס, ומתקדם בכל קריאה וכתיבה.
051015202528BAT=12.42\nTMP=31.5\nSOL=18.0\nopenf.read(3)f.read(6)f.readline()f.readline()f.seek(4)f.seek(0)
מקום המצביע אחרי כל פקודה: החלק הצבוע הוא מה שנקרא, והחץ הוא המקום שממנו תתחיל הקריאה הבאה

הפעולה f.seek(n) מציבה את המצביע במקום שנקבע. זו התשובה לשאלה שחוזרת: למה הקריאה השנייה אחרי f.read() נותנת מחרוזת ריקה? כי המצביע בסוף הקובץ.

f = open("log.txt")
print(f.tell())
print(f.read(3))
print(f.tell())
f.seek(0)
print(f.read(3))
f.seek(4)
print(f.read(5))
print(f.tell())
f.close()
פלט
0
BAT
3
BAT
12.42
9

גם כאן תכנית הלימודים אומרת "העמסה", וזה פרמטר שני אופציונלי: ב-seek(offset, whence) קובע whence את נקודת המוצא: 0 ההתחלה, וזו ברירת המחדל, 1 המקום הנוכחי, ו-2 הסוף. זה עובד בחופשיות במצב הבינארי, ושם התוצאה היא בתים שמודפסים עם הקידומת b, והפעולה decode ממירה אותם למחרוזת:

f = open("log.txt", "rb")
f.seek(-9, 2)
print(f.read())
f.seek(4)
f.seek(3, 1)
print(f.tell())
print(f.read(2).decode())
f.close()
פלט
b'SOL=18.0\n'
7
42

במצב טקסט מותר מן המקום הנוכחי ומן הסוף רק היסט אפס: f.seek(0, 2) מעביר אותנו לסוף הקובץ, ו-f.seek(-9, 2) עוצר את התוכנית.

הפעולה tell:

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

f = open("log.txt")
f.read()
mark = f.tell()
f.close()
print(mark)
w = open("log.txt", "a")
w.write("BAT=11.90\n")
w.close()
f = open("log.txt")
f.seek(mark)
print(f.read(), end="")
f.close()
פלט
28
BAT=11.90
דיוק: במצב טקסט לא מחשבים בעצמנו את מה ש-tell נותנת, אלא מחזירים אותו ל-seek כפי שהוא. התוכנית כותבת \n כתו אחד, אבל Windows שומר אותו בדיסק כשני תווים, \r\n, כפי שרואים בתוכנית הבאה, שהרצנו ב-Windows. ב-Linux וב-macOS התוצאה היא 2, והבתים הם b'A\nB\n'. קובצי היומן בדוגמאות שלנו נכתבו עם \n בלבד, ולכן המספרים שכתבנו ישירות ב-seek נכונים: כל תו לפניהם תופס בית אחד.
f = open("t.txt", "w")
f.write("A\nB\n")
f.close()
f = open("t.txt")
print(f.readline(), end="")
print(f.tell())
f.close()
f = open("t.txt", "rb")
print(f.read())
f.close()
פלט
A
3
b'A\r\nB\r\n'

הפעולה readline:

הפעולה readline קוראת שורה אחת, כולל \n בסופה, ובסוף הקובץ נותנת מחרוזת ריקה. נדפיס בעזרת repr כדי לראות את המחרוזת עם הסימנים שבה, והפעולה strip מוחקת רווחים ו-\n משני קצותיה:

f = open("log.txt")
first = f.readline()
print(repr(first))
print(first.strip())
print(repr(f.readline()))
print(repr(f.readline()))
print(repr(f.readline()))
f.close()
פלט
'BAT=12.42\n'
BAT=12.42
'TMP=31.5\n'
'SOL=18.0\n'
''

שורה ריקה בקובץ היא "\n" ולא מחרוזת ריקה, ולכן למחרוזת ריקה יש משמעות אחת בלבד: הקובץ נגמר. על זה בונים את הלולאה:

f = open("log.txt")
line = f.readline()
while line != "":
    print(line.strip())
    line = f.readline()
f.close()
פלט
BAT=12.42
TMP=31.5
SOL=18.0

הפעולה readlines:

הפעולה readlines קוראת את כל הקובץ בבת אחת, ונותנת רשימה של שורות, כל שורה עם \n בסופה:

f = open("log.txt")
lines = f.readlines()
f.close()
print(lines)
print(len(lines))
פלט
['BAT=12.42\n', 'TMP=31.5\n', 'SOL=18.0\n']
3
דוגמה 11.3
בקובץ volts.txt קריאת מתח בכל שורה. נחשב ממוצע וערך גבוה ביותר. הדרך הפשוטה ביותר היא לולאת for על עצם הקובץ עצמו: היא נותנת שורה בכל סבב, ו-float אינה מתנגדת ל-\n.
פתרון:
f = open("volts.txt")
values = []
for line in f:
    values.append(float(line))
f.close()
print(values)
print(round(sum(values) / len(values), 2))
print(max(values))
פלט
[12.42, 12.1, 11.95]
12.16
12.42

הפעולה splitlines:

אם איננו רוצים את \n, קוראים את כל הקובץ ומפצלים בעזרת splitlines. היא עדיפה על split("\n"), שנותנת איבר ריק מיותר כי בסוף הקובץ יש \n:

f = open("log.txt")
lines = f.read().splitlines()
f.close()
print(lines)
print("A\nB\n".split("\n"))
print("A\nB\n".splitlines())
print("A\r\nB".splitlines())
פלט
['BAT=12.42', 'TMP=31.5', 'SOL=18.0']
['A', 'B', '']
['A', 'B']
['A', 'B']
טעות נפוצה: הדוגמה בתכנית הלימודים כתובה file.read().splitlines, בלי סוגריים. בלי סוגריים לא מזמנים את הפעולה, אלא מקבלים אותה עצמה, ואין הודעת שגיאה.

עבודה עם print לתוך קובץ:

הפעולה print כותבת לקובץ אם נותנים לה את הפרמטר file=f. כל מה שאנחנו מכירים בה עובד כרגיל: היא מקבלת כל טיפוס, מפרידה ברווח, מוסיפה \n ומקבלת sep. לכן בוחרים בה לדוחות ולטבלאות, וב-write למחרוזת מוכנה. נכתוב כאן טבלת הספק בתבנית CSV, קובץ טקסט שתוכנת גיליונות אלקטרוניים פותחת:

loads = [(12.0, 0.5), (5.0, 0.2), (24.0, 1.5)]
f = open("power.csv", "w")
print("V,I,P", file=f)
for v, i in loads:
    print(v, i, v * i, sep=",", file=f)
f.close()
f = open("power.csv")
print(f.read(), end="")
f.close()
פלט
V,I,P
12.0,0.5,6.0
5.0,0.2,1.0
24.0,1.5,36.0

שימוש ב-with:

הפקודה with open("log.txt") as f: פותחת את הקובץ, וכשהגוף המוזח מסתיים הקובץ נסגר מעצמו, גם אם הגוף נעצר בשגיאה. תכנית הלימודים אומרת שהפקודה מבטלת את הצורך בפתיחה ובסגירה, ומדויק יותר שהיא מבטלת רק את הצורך ב-close. מעכשיו נשתמש בדרך הזו:

with open("log.txt") as f:
    print(f.closed)
    first = f.readline()
print(f.closed)
print(first.strip())
פלט
False
True
BAT=12.42

באותה שורה אפשר לפתוח שני קבצים: with open("a.txt") as src, open("b.txt", "w") as dst:.

שאלה 11.4: מה מדפיסה התוכנית הבאה, אם log.txt הוא הקובץ של הסעיף הזה?
with open("log.txt", "a") as f:
    print("END", file=f)
with open("log.txt") as f:
    print(f.read(4))
    print(f.readline().strip())
    print(len(f.readlines()))
    f.seek(0)
    print(f.read().splitlines()[-1])

11.5 טיפול בחריגים (exceptions)

ההבדל בין חריג לשגיאת קומפילציה:

הבחנו בסעיף 2.14 בין שגיאת תחביר לשגיאה בזמן ריצה. תכנית הלימודים קוראת לראשונה שבהן "שגיאת קומפילציה", כי לפני הביצוע פייתון מהדרת את כל התוכנית לקוד ביניים, bytecode. בהידור הזה מתגלה רק שגיאה בצורת הכתיבה, כלומר שגיאת תחביר, ושום דבר בתוכנית אינו רץ, אפילו לא השורה התקינה שלפניה:

print("Start")
print("Reading"
פלט
SyntaxError: '(' was never closed
חריג: מצב שגיאה שמופיע בזמן הריצה, ועוצר את התוכנית אם לא טיפלנו בו. זהו עצם של מחלקה, כמו ZeroDivisionError.
print("Start")
print(10 / 0)
print("End")
פלט
Start
ZeroDivisionError: division by zero

גם שגיאה בטיפוסים היא בפייתון חריג ולא שגיאת קומפילציה, ולכן היא מתגלה רק כשמגיעים לשורה. התוכנית הבאה רצה בשלום, ויש בה שורה שגויה שמחכה ליום שבו המתח יהיה גדול מ-100:

volts = 12
if volts > 100:
    print("High" + 1)
print("OK")
פלט
OK
הביטויהחריגהמשמעות
int("hot")ValueErrorמחרוזת שאינה מספר
1 / 0ZeroDivisionErrorחילוק באפס
[1, 2][5]IndexErrorאינדקס מחוץ לרשימה
{"a": 1}["b"]KeyErrorמפתח שאינו קיים
"V=" + 5TypeErrorשני טיפוסים שאינם עובדים יחד
voltsNameErrorשם שלא הוגדר
"AB".sizeAttributeErrorתכונה או פעולה שאינה קיימת
open("no.txt")FileNotFoundErrorקובץ שאינו קיים

בלוק try:

בבלוק try שמים את השורות שעלולות להיכשל. אם מופיע חריג, שאר הבלוק מופסק, והביצוע עובר לבלוק except המתאים. אם לא מופיע חריג, מדלגים על כל בלוק except.

try:line 1line 2except ValueError:finally:next lineValueErrorno error
שתי דרכים: הירוקה בלי שגיאה, והאדומה כשהשורה הראשונה נכשלת. את finally נכיר בסוף הסעיף

בלוק except:

text = input("Volts: ")
try:
    volts = float(text)
    print("Double:", volts * 2)
except ValueError:
    print("Not a number")
print("Done")
פלט
Volts: abc
Not a number
Done

הקלט כאן אינו מספר. התוכנית לא נעצרה, והשורה שאחרי הכישלון לא בוצעה. לקלט 12.5 היא מדפיסה: Double: 25.0 / Done.

דוגמה 11.4
בסעיף 5.7 כתבנו בדיקה ארוכה כדי לדעת אם מחרוזת היא מספר, והיא לא קיבלה אפילו מספרים שליליים. ניתן ל-float להחליט, ונחזור על השאלה עד שיגיע מספר תקין. זו מסננת הקלט העקשנית מפרק 4, עם try:
פתרון:
while True:
    try:
        volts = float(input("Volts: "))
        break
    except ValueError:
        print("Try again")
print("Got", volts)
פלט
Volts: abc
Try again
Volts: 12
Got 12.0

הפקודה break מתבצעת רק אם השורה שלפניה הצליחה. הסגנון "מנסים ומטפלים בכישלון" הוא הסגנון המקובל בפייתון.

שורות יכולות להיכשל ביותר מדרך אחת, וכותבים בלוק לכל סוג. מתבצע הבלוק הראשון שמתאים. בלוק אחד יכול לקבל גם כמה סוגים בטאפל: except (ValueError, KeyError):. חריג שאינו מתאים לשום בלוק עוצר את התוכנית כרגיל.

try:
    volts = float(input("Volts: "))
    ohms = float(input("Ohms: "))
    print("Current:", volts / ohms, "A")
except ValueError:
    print("Need a number")
except ZeroDivisionError:
    print("Ohms cannot be zero")
פלט
Volts: 12
Ohms: 0
Ohms cannot be zero
הקלטהפלט
12 , 4Current: 3.0 A
12 , xNeed a number
xNeed a number

חריגים הם מחלקות, ויש ביניהם ירושה כמו בסעיף 11.3. הבלוק מתאים למחלקה ולכל מה שנגזר ממנה, ולכן הבלוק except Exception תופס את רוב החריגים, כי ZeroDivisionError למשל נגזרת מ-Exception.

טעות נפוצה: לכן סדר הבלוקים חשוב: כותבים את הפרטי לפני הכללי. כאן הביצוע לעולם לא יגיע לבלוק ZeroDivisionError:
try:
    print(10 / int(input("Divisor: ")))
except Exception:
    print("Something failed")
except ZeroDivisionError:
    print("Zero")
פלט
Divisor: 0
Something failed
טעות נפוצה: הבלוק except: בלי שם תופס הכול, גם את שגיאות הכתיב שלנו. כאן שם המשתנה נכתב בטעות, והופיע NameError, אבל התוכנית מדווחת על בעיה באינדקס. לכן תמיד מציינים את הסוג שאנחנו מצפים לו:
values = [1, 2]
try:
    print(valeus[0])
except:
    print("Bad index")
פלט
Bad index

קינון חריגים (exceptions):

בלוק try יכול להיות בתוך בלוק try אחר. זה טבעי כשיש שתי בעיות בשתי רמות: הקובץ כולו אינו קיים, או שורה אחת בו פגומה:

total = 0
bad = 0
try:
    with open("volts.txt") as f:
        for line in f:
            try:
                total += float(line)
            except ValueError:
                bad += 1
    print("Total:", round(total, 2))
    print("Bad lines:", bad)
except FileNotFoundError:
    print("No file")
פלט
Total: 24.3
Bad lines: 1

כשהקובץ אינו קיים, אותה תוכנית מדפיסה: No file. אם החריג אינו מתאים לבלוק הפנימי, הוא עולה לבלוק החיצוני. כך גם בין פעולות: חריג שלא טופל בפעולה עולה למי שזימן אותה. ב-parse אין try, וההחלטה נשארת אצל המזמן:

def parse(item):
    name, text = item.split("=")
    return name, float(text)

for item in ["BAT=12.4", "TMP31", "SOL=x"]:
    try:
        print(parse(item))
    except ValueError:
        print("Bad item:", item)
פלט
('BAT', 12.4)
Bad item: TMP31
Bad item: SOL=x

שם חלופי (alias):

הכתיבה except ValueError as e: שמה את עצם החריג במשתנה e. הדפסתו נותנת את נוסח ההודעה, ו-type(e).__name__ נותן את שם המחלקה שלו. כך מבחינים בין חריגים, גם כשבלוק אחד תופס אותם:

for text in ["25", "abc", "0"]:
    try:
        print(100 / int(text))
    except (ValueError, ZeroDivisionError) as e:
        print(type(e).__name__, "-", e)
פלט
4.0
ValueError - invalid literal for int() with base 10: 'abc'
ZeroDivisionError - division by zero

עבודה עם finally:

הבלוק finally מתבצע תמיד: אם try הצליח, אם טופל חריג, אם לא טופל, ואפילו אם יצאנו בפקודה return. זה המקום הנכון להחזיר דבר למצבו: לסגור קובץ, או לכבות מנוע. בתוכנית הבאה אין except, ולכן החריג עוצר את התוכנית, אבל המנוע נכבה קודם:

try:
    print("Motor on")
    print(1 / 0)
    print("Work done")
finally:
    print("Motor off")
print("End")
פלט
Motor on
Motor off
ZeroDivisionError: division by zero

הפקודה with פועלת למעשה כמו finally שסוגר את הקובץ: לו כתבנו זאת בעצמנו, היינו פותחים את הקובץ לפני try, וכותבים f.close() ב-finally.

שאלה 11.5: מה מדפיסה התוכנית הבאה?
def f(x):
    try:
        print(10 / x)
    except ZeroDivisionError:
        print("Z")
    finally:
        print("F")

try:
    f(5)
    f(0)
    f("a")
    print("never")
except TypeError as e:
    print(type(e).__name__)
print("end")
פרויקט הספר: תחנת המדידה · שלב 11 — התחנה זוכרת בקובץ

התחנה שכחה הכול כשכיבו אותה. נכתוב עכשיו תוכנית שקוראת יומן מן הדיסק, עם שורות בצורה שראינו בפרק 5, וחלקן פגומות כי הקו ספג הפרעות. התוכנית אינה נעצרת בשורה פגומה: היא סופרת אותה ומדלגת עליה, ובסוף כותבת סיכום לקובץ. זה היומן:

with open("log.txt") as f:
    print(f.read(), end="")
פלט
BAT=12.42;TMP=31.5
BAT=11.65;TMP=33.0
BAT=12.10;TMP=hot
BAT12.05;TMP=38.5
BAT=11.95;TMP=41.5
BAT=12.20
BAT=12.31;TMP=42.0

נשתמש כאן בכל נושאי הפרק. settings הוא מילון במקום מספרים מפוזרים בתוכנית. faulty היא קבוצה, כי חיישן שנכשל עשר פעמים נשאר חיישן אחד. try מטפל בשורות הפגומות, והסיכום נכתב ל-summary.txt בעזרת print:

settings = {"cutoff": 11.8, "fan": 40.0}
faulty = set()
good = bad = low = hot = 0

with open("log.txt") as f:
    lines = f.read().splitlines()

for line in lines:
    try:
        values = {}
        for item in line.split(";"):
            sensor = item[:3]
            name, text = item.split("=")
            values[name] = float(text)
        bat, tmp = values["BAT"], values["TMP"]
        if bat < settings["cutoff"]:
            low += 1
        if tmp > settings["fan"]:
            hot += 1
        good += 1
    except ValueError as e:
        print("Skipped:", line, "-", e)
        faulty.add(sensor)
        bad += 1
    except KeyError as e:
        print("Skipped:", line, "- no", e)
        faulty.add(e.args[0])
        bad += 1

with open("summary.txt", "w") as out:
    print("Good lines:", good, file=out)
    print("Bad lines:", bad, file=out)
    print("Low battery:", low, file=out)
    print("Fan on:", hot, file=out)
    print("Faulty:", sorted(faulty), file=out)

with open("summary.txt") as f:
    print(f.read(), end="")
פלט
Skipped: BAT=12.10;TMP=hot - could not convert string to float: 'hot'
Skipped: BAT12.05;TMP=38.5 - not enough values to unpack (expected 2, got 1)
Skipped: BAT=12.20 - no 'TMP'
Good lines: 4
Bad lines: 3
Low battery: 1
Fan on: 2
Faulty: ['BAT', 'TMP']

שתי שגיאות שונות הגיעו לאותו בלוק: float כשהמחרוזת אינה מספר, וכשאין בקריאה סימן =, כי פירוק הרשימה לשני משתנים נכשל. ההודעה ב-e מראה את ההבדל. בשורה החסרה אין קריאת טמפרטורה, ו-values["TMP"] נותן KeyError, ואת שם החיישן מוצאים ב-e.args[0]. המונה good גדל רק בסוף הבלוק, אחרי שכל מה שלפניו הצליח.

ועכשיו ירושה. המחלקה Sensor מפרקים 8 ו-9 עובדת היטב, ואיננו רוצים לגעת בה. חיישן הטמפרטורה צריך תוספת אחת, התרעה, ולכן נגזור לו מחלקה. הרשימה מחזיקה עצמים משני סוגים, ו-print בוחרת את __str__ המתאימה:

class Sensor:
    step = 5 / 1023

    def __init__(self, name, unit, scale):
        self.name = name
        self.unit = unit
        self.scale = scale
        self.raw = 0

    def read(self, raw):
        self.raw = raw

    def value(self):
        return round(self.raw * Sensor.step * self.scale, 2)

    def __str__(self):
        text = self.name + ": " + str(self.value())
        return text + " " + self.unit

class TempSensor(Sensor):
    def __init__(self, name, limit):
        super().__init__(name, "C", 100)
        self.limit = limit

    def alarm(self):
        return self.value() > self.limit

    def __str__(self):
        text = super().__str__()
        if self.alarm():
            text += " ALARM"
        return text

sensors = [Sensor("Battery", "V", 4),
           TempSensor("Board", 40.0),
           TempSensor("Motor", 80.0)]
raws = [620, 90, 90]
for i in range(len(sensors)):
    sensors[i].read(raws[i])
    print(sensors[i])
פלט
Battery: 12.12 V
Board: 43.99 C ALARM
Motor: 43.99 C

נסו בעצמכם: הוסיפו לתוכנית הראשונה מילון מונים, שסופר כמה פעמים הופיע כל סוג חריג, וכתבו אותו בסיכום. אחר כך קראו את המילון settings מקובץ, במקום שיהיה כתוב בתוכנית.

עיקרי הפרק:

סיכום פקודות הפרק:

פקודהמה היא עושה
{1, 2} set() set(x)אתחול קבוצה, קבוצה ריקה, והמרה
s.add(x) s.update(t) s.union(t)הוספת איבר, הוספת איברים, ואיחוד חדש
s.discard(x) s.pop() s.clear()מחיקה בלי שגיאה, שליפת איבר, וריקון
{"a": 1} {} dict()אתחול מילון, ומילון ריק
d[k] d[k] = v k in d d.get(k, x)קריאה, הוספה או שינוי, בדיקה, וקריאה בלי שגיאה
d.items() d.keys() d.values()הזוגות, המפתחות והערכים
d.pop(k) d.popitem() del d[k]מחיקה לפי מפתח, מחיקת הזוג האחרון, ומחיקה בפקודה
class B(A): super().__init__()מחלקה נגזרת, וזימון הפעולה הבונה של הבסיס
issubclass(B, A) isinstance(x, A)היחס בין מחלקות, ובין עצם למחלקה
__del__ @classmethod @staticmethodפעולה הורסת, פעולת מחלקה, ופעולה סטטית
__add__ __eq__ __lt__העמסת האופרטורים +, == ו-<
ABC @abstractmethodמחלקה מופשטת, ופעולה מופשטת
f = open(name, mode) f.close()פתיחה באחד המצבים r, w, a, r+, וסגירה
f.read() f.read(n) f.readline()הכול, עד n תווים, או שורה
f.readlines() f.read().splitlines()רשימת השורות, עם \n או בלעדיו
f.seek(n) f.seek(0, 2) f.tell()הזזת המצביע, לסוף, ומיקומו
f.write(s) print(x, file=f)כתיבת מחרוזת, והדפסה לקובץ
with open(name) as f:פתיחה עם סגירה אוטומטית
try: except T as e: finally:ניסיון, טיפול עם העצם, ובלוק שמתבצע תמיד

מילון מונחים:

עבריתEnglishערבית
SetSetالمجموعة
מילוןDictionaryالقاموس
מפתחKeyالمفتاح
ירושהInheritanceالوراثة
היררכיהHierarchyهرمية
מחלקת בסיסBase class, Superclassالفئة الأساس
מחלקה נגזרתDerived class, Subclassالفئة المشتقّة
דריסהOverridingإعادة التعريف
פולימורפיזםPolymorphismتعدّد الأشكال
פעולה הורסתDestructorالعمليّة الهادمة
פעולת מופעInstance methodعمليّة النسخة
פעולת מחלקהClass methodعمليّة الفئة
פעולה סטטיתStatic methodالعمليّة الساكنة
העמסהOverloadingالتحميل الزائد
מחלקה מופשטתAbstract classالفئة المجرّدة
קובץFileالملف
מצב פתיחהFile modeوضع الفتح
מצביע הקובץFile pointerمؤشّر الملف
קידודEncodingالترميز
חריגExceptionالاستثناء
שגיאת קומפילציהCompilation errorخطأ الترجمة
התשובות הסופיות לשאלות הפרק
11.1 2 3 , False True , [2, 5] [1, 2, 3]
11.2 3 2 , 9 3 , ('L', 2) , False
11.3 11 21 25 , True False
11.4 BAT= , 12.42 , 3 , END
11.5 2.0 , F , Z , F , F , TypeError , end

שאלות לתרגול:

רמה ראשונה — בסיסי
1.
(קבוצה) מה מדפיסה התוכנית?
a = {1, 2, 3}
b = {3, 4}
c = a.union(b)
a.add(3)
a.discard(1)
b.update([9, 9])
print(len(a), len(b), len(c))
print(2 in a, 1 in a)
print(sorted(c))
תשובות: 2 3 4 , True False , [1, 2, 3, 4]
2.
(מילון) מה מדפיסה התוכנית?
d = {"A": 1, "B": 2}
e = d
f = d.copy()
d["C"] = 3
e["A"] = 10
print(len(d), len(e), len(f))
print(f["A"], d["A"])
print(d.pop("B"), d.popitem())
print(d)
תשובות: 3 3 2 , 1 10 , 2 ('C', 3) , {'A': 10}
3.
(ירושה) מה מדפיסה התוכנית? למה B().show() מדפיס את השורה B?
class A:
    def who(self):
        return "A"

    def show(self):
        print(self.who())

class B(A):
    def who(self):
        return "B"

class C(B):
    def show(self):
        print("C", end=" ")
        super().show()

A().show()
B().show()
C().show()
print(issubclass(C, A), isinstance(B(), C))
תשובות: A , B , C B , True False. הפעולה show נמצאת ב-A, אבל self שם הוא עצם של B, ולכן פועל who שלו.
4.
(קבצים) בקובץ a.txt שלוש שורות: one, two ו-three. מה מדפיסה התוכנית?
with open("a.txt") as f:
    print(f.read(2))
    print(f.readline().strip())
    print(f.readlines())
    f.seek(0)
    print(f.read().splitlines()[-1])
תשובות: on , e , ['two\n', 'three\n'] , three
5.
(חריגים) מה מדפיסה התוכנית? למה היא אינה מגיעה ל-"2"?
try:
    for t in ["4", "0", "x", "2"]:
        try:
            print(8 / int(t))
        except ZeroDivisionError:
            print("zero")
        finally:
            print("end", t)
except ValueError:
    print("outer")
print("done")
תשובות: 2.0 , end 4 , zero , end 0 , end x , outer , done. החריג ValueError אינו מתאים לבלוק הפנימי, ולכן פועל finally, והחריג עולה החוצה ועוזב את הלולאה.
רמה שנייה — בינוני
6.
(קבוצה) שני לוחות, ולכל אחד שורה עם מספרי ההדקים שבשימוש. כתבו תוכנית שמדפיסה את מספר ההדקים השונים בסך הכול ואת ההדקים המשותפים לשני הלוחות, בעזרת union ו-in.
תשובות:
a = set()
for t in input("Board A: ").split():
    a.add(int(t))
b = set()
for t in input("Board B: ").split():
    b.add(int(t))
shared = set()
for pin in a:
    if pin in b:
        shared.add(pin)
print("All:", len(a.union(b)))
print("Shared:", sorted(shared))
לקלטים 2 3 4 9 ו-4 5 9 היא מדפיסה: All: 5 , Shared: [4, 9]
7.
(מילון) כתבו תוכנית שקוראת ערך עם קידומת, כמו 4.7k או 47u, ומדפיסה אותו בלי קידומת. הקידומות במילון: k, M, m ו-u.
תשובות:
PREFIX = {"k": 1000, "M": 1000000,
          "m": 0.001, "u": 0.000001}
text = input("Value: ")
if text[-1] in PREFIX:
    print(float(text[:-1]) * PREFIX[text[-1]])
else:
    print(float(text))
לקלטים 4.7k, 2M ו-220 היא מדפיסה: 4700.0 , 2000000.0 , 220.0
8.
(העמסה) כתבו את המחלקה Res, כך ש-a + b פירושו חיבור טורי, ו-a | b (הפעולה __or__) פירושו חיבור מקבילי:
R = R1 · R2R1 + R2
והוסיפו __eq__ ו-__str__.
תשובות:
class Res:
    def __init__(self, ohms):
        self.ohms = ohms

    def __add__(self, other):
        return Res(self.ohms + other.ohms)

    def __or__(self, other):
        p = self.ohms * other.ohms
        return Res(p / (self.ohms + other.ohms))

    def __eq__(self, other):
        return self.ohms == other.ohms

    def __str__(self):
        return str(self.ohms) + " ohm"

a = Res(100)
b = Res(100)
c = Res(50)
print(a + b)
print(a | b)
print((a | b) == c)
print((a | b) + c)
היא מדפיסה: 200 ohm , 50.0 ohm , True , 100.0 ohm
9.
(קבצים) בקובץ temps.txt טמפרטורה בכל שורה. כתבו תוכנית שקוראת גבול, מעתיקה ל-hot.txt כל טמפרטורה גבוהה ממנו בעזרת with ו-print, ומדפיסה את מספרן.
תשובות:
limit = float(input("Limit: "))
count = 0
with open("temps.txt") as src, open("hot.txt", "w") as dst:
    for line in src:
        if float(line) > limit:
            print(line.strip(), file=dst)
            count += 1
print(count, "values")
with open("hot.txt") as f:
    print(f.read().splitlines())
לקובץ 31.5 44.0 39.9 41.2 ולגבול 40 היא מדפיסה: 2 values , ['44.0', '41.2']
10.
(חריגים) כתבו את הפעולה read_int(prompt, low, high), שמבקשת מספר שלם עד שמגיע מספר בתחום, ומדפיסה הודעה מתאימה לכל סוג של כישלון.
תשובות:
def read_int(prompt, low, high):
    while True:
        try:
            value = int(input(prompt))
            if low <= value <= high:
                return value
            print("Out of range")
        except ValueError:
            print("Not a number")

print("Duty:", read_int("Duty: ", 0, 100), "%")
לקלטים x, 170 ו-42 היא מדפיסה: Not a number , Out of range , Duty: 42 %. הפקודה return יוצאת מן הלולאה ומן הפעולה יחד.
רמה שלישית — אתגר
11.
(מילון) ההפך מדוגמת הצבעים: כתבו תוכנית שקוראת ערך נגד, כמו 4700, ומדפיסה את שלושת צבעיו. בנו מן המילון מילון הפוך, שמפתחו הספרה וערכו הצבע.
תשובות:
CODES = {"black": 0, "brown": 1, "red": 2,
         "orange": 3, "yellow": 4, "green": 5,
         "blue": 6, "violet": 7, "grey": 8,
         "white": 9}
names = {}
for color, digit in CODES.items():
    names[digit] = color
ohms = input("Ohms: ")
power = len(ohms) - 2
print(names[int(ohms[0])], names[int(ohms[1])],
      names[power])
לקלט 4700 היא מדפיסה: yellow violet red , ולקלט 47 היא מדפיסה: yellow violet black
12.
(ירושה) כתבו מחלקה מופשטת Source עם פעולה מופשטת power. גזרו ממנה Battery (מתח כפול זרם) ו-Solar (שטח כפול קרינה כפול נצילות 0.2). הדפיסו את ההספק הכולל של רשימת מקורות, והראו שאי אפשר לבנות עצם מ-Source.
תשובות:
from abc import ABC, abstractmethod

class Source(ABC):
    def __init__(self, x, y):
        self.x = x
        self.y = y

    @abstractmethod
    def power(self):
        pass

class Battery(Source):
    def power(self):
        return self.x * self.y

class Solar(Source):
    def power(self):
        return self.x * self.y * 0.2

total = 0
for s in [Battery(12, 2.5), Solar(0.5, 800)]:
    total += s.power()
print("Total:", total, "W")
try:
    Source(1, 1)
except TypeError:
    print("Source is abstract")
היא מדפיסה: Total: 110.0 W , Source is abstract
13.
(קבצים) בקובץ cfg.txt הגדרות בצורה key=value. קראו אותן למילון, שנו את fan ל-45, הוסיפו name=roof, ושמרו את הקובץ מחדש.
תשובות:
cfg = {}
with open("cfg.txt") as f:
    for line in f.read().splitlines():
        key, value = line.split("=")
        cfg[key] = value
print(cfg)
cfg["fan"] = "45"
cfg["name"] = "roof"
with open("cfg.txt", "w") as f:
    for key in cfg:
        print(key + "=" + cfg[key], file=f)
with open("cfg.txt") as f:
    print(f.read(), end="")
לקובץ cutoff=11.8 fan=40 היא מדפיסה: {'cutoff': '11.8', 'fan': '40'} , cutoff=11.8 , fan=45 , name=roof. המצב "w" מוחק את הקובץ, אבל שמרנו הכול במילון.
14.
(חריגים) בקובץ ohm.txt שורות בצורה V,R, כלומר מתח והתנגדות. כתבו תוכנית שמדפיסה את הזרם לכל שורה תקינה, וסופרת כל סוג חריג במילון, בעזרת as ו-get.
תשובות:
errors = {}
good = 0
with open("ohm.txt") as f:
    for line in f:
        try:
            v, r = line.split(",")
            print(float(v) / float(r), "A")
            good += 1
        except (ValueError, ZeroDivisionError) as e:
            kind = type(e).__name__
            errors[kind] = errors.get(kind, 0) + 1
print("Good:", good)
print(errors)
לקובץ 12,4 5,0 abc,3 9 6,2 היא מדפיסה: 3.0 A , 3.0 A , Good: 2 , {'ZeroDivisionError': 1, 'ValueError': 2}