נושאי רשות
הפרק הזה הוא פרק רשות: נושאיו מומלצים בתכנית הלימודים, אבל אינם חובה, שעותיהם אינן נספרות בסך השעות שלה, ואין עליהם שאלות בבחינת הבגרות. עם זאת, הם הצעד הטבעי אחרי הפרקים הקודמים: כל תוכנית אמיתית צריכה לחפש מהר, לשמור בקובץ את מה שלמדה, ולהמשיך לעבוד כשמגיע אליה טקסט פגום. נלמד כאן ארבעה נושאים, ובסוף נחבר אותם לשלב חדש בתחנת המדידה.
- נשתמש בקבוצה
setכשחשוב לנו אם האיבר קיים, ולא היכן הוא. - נשתמש במילון
dictכדי להגיע לערך בעזרת מפתח. - נבנה מחלקה על מחלקה בירושה, ונכתוב
super,__add__ומחלקות מופשטות. - נקרא מקובצי טקסט ונכתוב אליהם, וננוע בתוכם בעזרת
seekו-tell. - נבחין בין שגיאת תחביר לחריג, ונטפל בחריג בעזרת
try,exceptו-finally. - נכתוב תוכנית שקוראת את יומן התחנה, מדלגת על שורות פגומות ושומרת סיכום בקובץ.
| הנושא | הסעיפים | השעות המוצעות בתכנית הלימודים |
|---|---|---|
| מבני נתונים נוספים | 11.1 ו-11.2 | 10 |
| ירושה של מחלקות | 11.3 | 15 |
| עבודה עם קבצים | 11.4 | 10 |
| טיפול בחריגים | 11.5 | 10 |
try ו-import. לכן מריצים את התוכניות של הפרק הזה בפייתון על המחשב.11.1 מבנה נתונים מטיפוס Set (set)
ברשימה ובטאפל (Tuple) מפרק 6 יש סדר ואינדקסים. אבל לפעמים אין לנו צורך בסדר ולא בכפילויות. בלוח ארדואינו, למשל, אנחנו רוצים לדעת: האם ההדק 7 תפוס? לא חשוב מתי תפסנו אותו, ואין טעם לתפוס אותו פעמיים. זה תפקידה של הקבוצה.
אתחול, מבנה ריק, 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 מוסיפה איבר אחד ומשנה את הקבוצה עצמה. אם האיבר כבר קיים לא קורה דבר, ואין הודעת שגיאה.
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_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 Falseupdate משנה את הקבוצה עצמה ואינה מחזירה דבר. אם נשים את התוצאה שלה במשתנה נקבל 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'len({1, 1, 2})3 in {1, 2}len(set("ABBA"))sorted({3, 1, 2})len(set())type({}).__name__2 2 False 3 2 4 [1, 2, 3] 5 0 6 dict 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"].
אתחול, מבנה ריק, הדפסה:
dict.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'>- בין המפתח לערך שלו יש נקודתיים, ובין הזוגות פסיק. המילון הריק הוא
{}אוdict(). - מפתח אינו חוזר, וחייב להיות בלתי ניתן לשינוי. הערך יכול להיות מכל טיפוס, והוא יכול לחזור.
- המילון שומר על סדר ההוספה, אבל
d[0]מחפש את המפתח0, ואין פירושו הזוג הראשון.
in, שמחפש במפתחות בלבד ולא בערכים, כפי שרואים בשורה השנייה:station = {"BAT": 12.42, "TMP": 31.5}
print("TMP" in station)
print(12.42 in station)
print(station["HUM"])True False KeyError: 'HUM'
הוספת איברים:
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)| פקודה | part | kind | counts | kind 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}d["b"]len(d)"a" in d1 in dlist(d)list(d.values())2 2 2 3 True 4 False 5 ['a', 'b'] 6 [1, 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, ונבנה עליה.
היררכיה ברורה וחד-משמעית:
מחלקות שיורשות זו מזו יוצרות היררכיה, שמציירים כעץ. החץ יוצא מן המחלקה היורשת אל המחלקה שממנה היא יורשת:
היררכיה טובה עונה בבירור על שאלה אחת: האם כל עצם של המחלקה הזו הוא עצם של ההיא? קוראים את המשפט בקול, ואם הוא נשמע מוזר, הירושה שגויה.
| המשפט | התשובה | מה כותבים |
|---|---|---|
| נגד הוא רכיב אלקטרוני | נכון | ירושה: 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 ואינה תלויה בתדר, ואילו בקבל ובסליל גודלה הוא ההיגב:
נכתוב לכל מחלקה גרסה משלה של 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 | תכונות העצם והמחלקה שלו | מה ששייך לעצם אחד |
פעולת מחלקה @classmethod | cls | משתני המחלקה | דרך נוספת לבנות עצם |
פעולה סטטית @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
powerזקוקה לנגד מסוים, ולכן היא פעולת מופע.from_codeהיא דרך שנייה לבנות עצם, כאן מקוד נגד ה-SMD מסעיף 5.3. אם מזמנים אותה ממחלקה נגזרת,clsהוא המחלקה הנגזרת, והיא בונה בדיוק את מה שביקשנו.parallelהיא חישוב בלבד, ומקומה במחלקה הוא ארגוני. מזמנים אותה בשם המחלקה או מעצם.
העמסת פעולות:
בשפות כמו 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
__add__מחזירה עצם חדש, ולכן אפשר להמשיך:a + b + c.- אחרי
__eq__שני נגדים באותו ערך שווים זה לזה, אף שהם שני עצמים שונים, כפי ש-a is cמראה. ופייתון בונה מזה את!=. - אחרי
__lt__הפעולהsortעובדת על רשימת נגדים בלי שנסביר לה איך.
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'
הרווח האמיתי נראה במחלקה נגזרת ששכחה לכתוב את הפעולה: בניית עצם ממנה נותנת את אותה שגיאה, עם שם הפעולה החסרה, והשגיאה אינה מחכה לזימון הראשון באמצע הריצה.
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:
הפעולה 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
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:.
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 closedZeroDivisionError.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 / 0 | ZeroDivisionError | חילוק באפס |
[1, 2][5] | IndexError | אינדקס מחוץ לרשימה |
{"a": 1}["b"] | KeyError | מפתח שאינו קיים |
"V=" + 5 | TypeError | שני טיפוסים שאינם עובדים יחד |
volts | NameError | שם שלא הוגדר |
"AB".size | AttributeError | תכונה או פעולה שאינה קיימת |
open("no.txt") | FileNotFoundError | קובץ שאינו קיים |
בלוק try:
בבלוק try שמים את השורות שעלולות להיכשל. אם מופיע חריג, שאר הבלוק מופסק, והביצוע עובר לבלוק except המתאים. אם לא מופיע חריג, מדלגים על כל בלוק except.
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.
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 , 4 | Current: 3.0 A |
12 , x | Need a number |
x | Need 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.
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")התחנה שכחה הכול כשכיבו אותה. נכתוב עכשיו תוכנית שקוראת יומן מן הדיסק, עם שורות בצורה שראינו בפרק 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 מקובץ, במקום שיהיה כתוב בתוכנית.
עיקרי הפרק:
- קבוצה היא בלי כפילויות ובלי סדר, והשאלה היחידה בה היא
in. הקבוצה הריקה היאset()ולא{}. - מילון מגיע לערך לפי מפתח,
inמחפש במפתחות, ו-d[k] = vמוסיף או משנה. - ירושה היא יחס "הוא": המחלקה הנגזרת מקבלת את מה שיש בבסיס, דורסת את מה ששונה, ו-
super()מגיעה למקור. - פולימורפיזם פירושו שהלולאה אינה שואלת על הטיפוס, וכל עצם מבצע את הגרסה שלו.
- אין בפייתון העמסה לפי מספר פרמטרים: ההגדרה האחרונה מחליפה את הקודמות, והתחליף הוא ערך ברירת מחדל.
- המצב
"w"מוחק את הקובץ, והמצב"a"מוסיף לו. הפקודהwithסוגרת את הקובץ בשבילנו, גם בשגיאה. - כל קריאה מקדמת את מצביע הקובץ, ו-
seekמחזירה אותו. את מה ש-tellנותנת מחזירים ל-seekכפי שהוא. - שגיאת תחביר מונעת את כל הריצה, ואילו חריג מופיע בזמן הריצה, ואפשר לטפל בו.
- מציינים את סוג החריג שמצפים לו, כותבים את הפרטי לפני הכללי, ו-
finallyמתבצע תמיד.
סיכום פקודות הפרק:
| פקודה | מה היא עושה |
|---|---|
| {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 | ערבית |
|---|---|---|
| Set | Set | المجموعة |
| מילון | 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 | خطأ الترجمة |
2 3 , False True , [2, 5] [1, 2, 3]3 2 , 9 3 , ('L', 2) , False11 21 25 , True FalseBAT= , 12.42 , 3 , END2.0 , F , Z , F , F , TypeError , endשאלות לתרגול:
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]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}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 שלו.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"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, והחריג עולה החוצה ועוזב את הלולאה.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]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.0Res, כך ש-a + b פירושו חיבור טורי, ו-a | b (הפעולה __or__) פירושו חיבור מקבילי:__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 ohmtemps.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']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 יוצאת מן הלולאה ומן הפעולה יחד.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 blackSource עם פעולה מופשטת 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 abstractcfg.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" מוחק את הקובץ, אבל שמרנו הכול במילון.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}