טיפוסי נתונים מורכבים
המחלקה Sensor שכתבנו בפרק 8 מתארת חיישן אחד, וכל התכונות שלה הן מספרים ומחרוזות. אבל דברים אמיתיים בנויים מדברים אחרים: במכשיר יש סוללה, למעגל יש מרכז, על לוח אלקטרוני יושבות נוריות, והתחנה מרכזת חיישנים רבים. בפרק הזה נבנה מחלקה ממחלקות אחרות: נהפוך עצם לתכונה של עצם אחר, נאסוף עצמים ברשימה, ונצייר את הכול בתרשים לפני שנכתוב קוד. בסוף הפרק התחנה עצמה תהיה עצם שמחזיק את רשימת החיישנים שלה.
- נגדיר טיפוס מורכב, ונבחין בין מה שיהיה תכונה פשוטה למה שראוי למחלקה משלו.
- ניצור עצמים שתכונותיהם עצמים, ונגיע למה שבתוכם בשרשרת של נקודות.
- נדע מתי שתי תכונות מצביעות על אותו עצם, ונמנע זאת או ננצל זאת.
- נקרא תרשים UML ונצייר אותו, ונעבור ממנו לקוד ומן הקוד אליו.
- נעבוד על רשימה של עצמים, גם כשהם מטיפוסים שונים.
- נכתוב מחלקה שמחזיקה רשימה של עצמים, ומשרתת אותה בפעולות הוספה, חיפוש וספירה.
9.1 הגדרה של טיפוס מורכב
נכתוב מחלקה שמתארת מכשיר שפועל על סוללה, ונרצה לדעת כמה שעות הוא פועל. הרעיון הראשון הוא לשים הכול במחלקה אחת:
class Device:
def __init__(self, name, watts, volts, amp_hours):
self.name = name
self.watts = watts
self.volts = volts
self.amp_hours = amp_hoursאבל volts ו-amp_hours אינן מתארות את המכשיר, אלא את הסוללה שלו. אם נתקין במכשיר סוללה אחרת, נצטרך לשנות שתי תכונות יחד. ואם נזדקק לאנרגיה של הסוללה במחלקה אחרת, נחזור על החישוב שם. הסוללה היא דבר העומד בפני עצמו, ולכן ניתן לה מחלקה משלה, והמכשיר יכיל סוללה. לקשר הזה קוראים הכלה:
class Battery:
def __init__(self, volts, amp_hours):
self.volts = volts
self.amp_hours = amp_hours
def energy(self):
return self.volts * self.amp_hours
class Device:
def __init__(self, name, watts, battery):
self.name = name
self.watts = watts
self.battery = battery
def hours(self):
return round(self.battery.energy() / self.watts, 1)
pack = Battery(12, 7)
fan = Device("Fan", 6, pack)
print(fan.name, fan.hours())
print(fan.battery.volts)Fan 14.0 12
התכונה battery מקבלת עצם שלם, ולא מספר. הביטוי self.battery.energy() פועל בשני שלבים: self.battery הוא העצם Battery, ועליו מזמנים את energy(). המכשיר אינו יודע איך מחשבים אנרגיה, אלא מבקש אותה מן הסוללה, וכל מחלקה אחראית למה ששייך לה.
battery אינה מכילה את הסוללה עצמה, אלא הפניה אליהb = a בפרק 6 נתנה לאותה רשימה שם נוסף.נחליף עכשיו את הסוללה של המאוורר. נשנה תכונה אחת ומשך הפעולה ישתנה, והסוללה הישנה נשארת כפי שהייתה:
fan.battery = Battery(24, 20) print(fan.hours(), pack.volts)
הפלט: 80.0 12
ומתי כותבים מחלקה חדשה? שואלים: האם זה ערך אחד שמתאר את העצם, או דבר שיש לו חלקים ופעולה משלו? ההספק של המכשיר הוא מספר מטיפוס float, ואילו לסוללה שלו יש מתח וקיבולת והיא מחשבת את האנרגיה שלה, ולכן היא מטיפוס Battery.
"Fan", כפי שראינו בסעיף 2.3. החידוש כאן הוא שאת המחלקה הפנימית כתבנו בעצמנו.Device("Fan", 6, 12) ונשים מספר במקום הסוללה, איש לא יתנגד ביצירת העצם. השגיאה AttributeError תופיע מאוחר יותר, בזימון hours(), רחוק מן הסיבה. לכן בשגיאה כזאת מחפשים את המקום שבו יצרנו את העצם, ולא את השורה שבה התוכנית נעצרה.radio = Device("Radio", 4, Battery(6, 4))
print(radio.hours())
print(radio.battery.energy())
radio.battery.volts = 12
print(radio.hours())(א) תחום המדידה
(ב) תצוגה שיש לה מספר ספרות והיא מציגה ערך
(ג) שם היצרן
(ד) שני גששים, ולכל אחד צבע, אורך והתנגדות
9.2 עצמים כערכי תכונות: יצירת ושימוש בעצמים מטיפוס מורכב
למעגל יש מרכז ורדיוס. נתחיל מן החלק, נקודה במישור: יש לה פעולה שמזיזה אותה, ופעולה שמחשבת את מרחקה מנקודה אחרת לפי פיתגורס, והפרמטר other שלה הוא בעצמו עצם מן המחלקה Point. במחלקה Circle התכונה center היא עצם מן המחלקה Point, והתכונה radius היא מספר מטיפוס float:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def move(self, dx, dy):
self.x += dx
self.y += dy
def distance(self, other):
dx = self.x - other.x
dy = self.y - other.y
return (dx ** 2 + dy ** 2) ** 0.5
def __str__(self):
return "(" + str(self.x) + ", " + str(self.y) + ")"class Circle:
def __init__(self, center, radius):
self.center = center
self.radius = radius
def move_to(self, x, y):
self.center.x = x
self.center.y = y
def contains(self, point):
return self.center.distance(point) <= self.radius
def __str__(self):
c = str(self.center)
return "center " + c + ", radius " + str(self.radius)c = Circle(Point(3, 4), 2.5) print(c) print(c.center) print(c.center.x) print(c.center.distance(Point(0, 0)))
הפלט: center (3, 4), radius 2.5 , (3, 4) , 3 , 5.0
את הביטוי c.center.x קוראים משמאל לימין, וכל נקודה פירושה: היכנסו אל מה שלפניה. המחלקה Circle אינה יודעת איך כותבים נקודה, ולכן היא מזמנת את str(self.center) ו-Point דואגת לזה. כך גם contains שואלת את הנקודה מה המרחק, ואינה מחשבת אותו בעצמה.
c = Circle(Point(3, 4), 2)c.radiusc.center.ystr(c.center)c.contains(Point(3, 5))2 2 4 3 (3, 4) 4 True שינוי המרכז:
נתחיל מ-c = Circle(Point(3, 4), 2.5), נבצע את הפקודות הבאות זו אחר זו, ונדפיס את המעגל אחרי כל פקודה:
| הפקודה | הפלט | מה קורה |
|---|---|---|
c.center.x = 10 | center (10, 4), radius 2.5 | משנה תכונה בתוך עצם הנקודה |
c.center.move(1, -2) | center (11, 2), radius 2.5 | מבקשת מן הנקודה לזוז |
c.move_to(0, 0) | center (0, 0), radius 2.5 | מבקשת מן המעגל, והוא משנה את הנקודה שלו. זו הדרך הברורה ביותר למי שמשתמש במחלקה |
c.center = Point(7, 7) | center (7, 7), radius 2.5 | שמה בתכונה הפניה לעצם חדש, והנקודה הישנה אינה משתנה |
(0, 0) משדרת עד למרחק 40 m, ולכן אזור הכיסוי שלה הוא מעגל. נבדוק אם היא מגיעה ליחידת מדידה, ואז נעביר את יחידת התקשורת.gateway = Circle(Point(0, 0), 40) node = Point(30, 30) print(gateway.contains(node)) gateway.move_to(10, 10) print(gateway.contains(node))
הפלט: False , True
לא כתבנו את חישוב המרחק במחלקה Circle, אלא השתמשנו במה שכתבנו ב-Point.
שתי תכונות שמצביעות על אותו עצם:
self.center = center שומרת רק את ההפניה, ולכן לשני המעגלים נקודה אחת, ואם מזיזים את המרכז של אחד מהם גם המרכז של השני זז:p = Point(0, 0) a = Circle(p, 5) b = Circle(p, 8) a.center.x = 3 print(a.center, b.center, p) print(a.center is b.center)
הפלט: (3, 0) (3, 0) (3, 0) , True
האופרטור is שואל: האם אלה עצם אחד? התיקון הוא ליצור לכל מעגל נקודה משלו, Circle(Point(0, 0), 5), או שהמעגל יכין עותק משלו בפעולה הבונה, ואז לא משנה מה מעבירים אליו:
class Circle:
def __init__(self, center, radius):
self.center = Point(center.x, center.y)
self.radius = radiusואותן פקודות נותנות עכשיו: (3, 0) (0, 0) (0, 0) , False
== אינו משווה את תכונות העצמים כל עוד המחלקה אינה מבקשת זאת, אלא שואל אם זה אותו עצם. לכן הביטוי Point(1, 2) == Point(1, 2) נותן False. משווים אפוא את התכונות אחת אחת.ושיתוף אינו תמיד טעות. התראה עוקבת אחר חיישן Sensor מפרק 8, והיא חייבת לראות את מה שהחיישן רואה. נבנה שתי התראות על אותה סוללה, אזהרה ב-12.2 V וניתוק ב-11.8 V מפרק 3. נעדכן את החיישן פעם אחת, ושתי ההתראות יראו יחד את הקריאה החדשה. החיישן הוא דבר אחד בעולם, ואילו המרכזים של שני מעגלים נפרדים הם שני דברים:
class Alarm:
def __init__(self, sensor, limit):
self.sensor = sensor
self.limit = limit
def active(self):
return self.sensor.value() < self.limit
batt = Sensor("Battery", "V", 4)
warn = Alarm(batt, 12.2)
cut = Alarm(batt, 11.8)
batt.read(620)
print(batt, warn.active(), cut.active())
batt.read(590)
print(batt, warn.active(), cut.active())הפלט: Battery: 12.12 V True False , Battery: 11.53 V True True
p = Point(1, 1) a = Circle(p, 2) b = Circle(Point(1, 1), 2) p.x = 9 print(a.center.x, b.center.x) print(a.center is p, b.center is p)
9.3 תרשים UML
כשיש מחלקות רבות, קשה לראות מן הקוד את מבנה התוכנית. תרשים מחלקות הוא תמונה של המבנה הזה: אילו מחלקות יש לנו, מה יש בכל אחת, ואיך הן קשורות. מציירים אותו לפני הכתיבה כדי לחשוב, ואחריה כדי להסביר. שפת הציור הזאת אחידה בכל העולם, שמה UML, וכל מתכנת מבין אותה בלי קשר לשפת התכנות שלו.
Pointכל שורה נכתבת באופן קבוע. הטיפוס בא אחרי נקודתיים, ולעולם לא כותבים self, כי הוא נמצא בכל פעולה:
| בתרשים | בקוד | המשמעות |
|---|---|---|
+ x : float | self.x = x | תכונה מטיפוס float |
+ move(dx: float, dy: float) | def move(self, dx, dy): | פעולה עם שני פרמטרים |
+ value() : float | def value(self): | פעולה שמחזירה מספר |
+ step : float | step = 5 / 1023 | קו מתחת לשורה: משתנה מחלקה |
הסימן בתחילת השורה הוא הרשאת הגישה. + פירושו ציבורי: כל מי שמחזיק בעצם יכול להגיע אליו. ו-- פירושו פרטי: לשימוש בתוך המחלקה בלבד. בפייתון אין חסימה של ממש, אלא הסכמה בין מתכנתים: שם שמתחיל בקו תחתון _ נחשב פרטי, ולא נוגעים בו מבחוץ.
class Fuse:
def __init__(self, amps):
self.amps = amps
self._blown = False
def carry(self, current):
if current > self.amps:
self._blown = True
def is_blown(self):
return self._blown
f = Fuse(2)
f.carry(3)
f.carry(0.5)
print(f.is_blown())True
אילו מישהו היה כותב f._blown = False הפקודה הייתה פועלת, אבל הקו התחתון והסימן - אומרים לו שהוא נוגד את תכנון המחלקה. בשאר הדוגמאות נשאיר את התכונות ציבוריות, כדי שיישארו קצרות.
הקווים בין המחלקות:
קו בין שני מלבנים פירושו שעצם מן המחלקה הראשונה מכיר עצם מן השנייה, כלומר יש בו תכונה שמצביעה עליו. סוג הקו אומר איך הוא מכיר אותו, והמספרים בקצותיו הם הריבוי: המספר שכתוב ליד מחלקה אומר כמה עצמים ממנה מתאימים לעצם אחד מן המחלקה האחרת.
| בתרשים | השם | המשמעות | בקוד |
|---|---|---|---|
| קו וחץ פתוח | שיוך | מכיר אותו ומשתמש בו, והעצם ההוא עומד בפני עצמו ועשוי לשרת גם אחרים | העצם מועבר מבחוץ, ומי שיצר אותו ממשיך להחזיק בו |
| קו ומעוין מלא בצד השלם | הרכבה | הוא חלק ממנו: אינו משרת עצם אחר | השלם יוצר את החלק בעצמו, מעתיק אותו, או מקבל עצם חדש שאיש מלבדו אינו מחזיק בו |
| הסימן | המשמעות | דוגמה |
|---|---|---|
1 | אחד בדיוק | לכל מעגל מרכז אחד |
0..1 | אחד או אף לא אחד | מכשיר שאולי אין בו סוללה |
* | כל מספר, גם אפס | לוח שמתחיל ריק ואחר כך נושא נוריות |
1..* | לפחות אחד | שרשרת שיש בה לפחות נגד אחד |
Device ו-Battery מסעיף 9.1.ניקח מכל מחלקה את התכונות והפעולות. הסוללה מועברת מבחוץ ואפשר להעבירה למכשיר אחר, ולכן נצייר שיוך. החץ אומר שהמכשיר מכיר את הסוללה, והסוללה אינה מכירה את המכשיר:
Thermostat מפעיל מאוורר Fan. המעוין אומר שהמאוורר הוא חלק ממנו, ולכן הוא יוצר אותו בעצמו בפעולה הבונה.class Fan:
def __init__(self):
self.speed = 0
def set_speed(self, speed):
self.speed = speed
class Thermostat:
def __init__(self, target):
self.target = target
self.fan = Fan()
def update(self, temp):
if temp > self.target:
self.fan.set_speed(1)
else:
self.fan.set_speed(0)
t = Thermostat(30)
for temp in [28, 32]:
t.update(temp)
print(temp, t.fan.speed)28 0 32 1
התרשים נתן לנו את השמות, הטיפוסים והמבנה, ולא אמר דבר על פעולתה של update: הכללים באים מן הבעיה.
* נכתב ליד LED, כי הוא אומר כמה מהן יש בלוח אחד. ולפעמים מציירים חץ מן החלק אל השלם כשאין בקוד תכונה כזאת: הנקודה אינה יודעת איזה מעגל משתמש בה.(א) כמה מתגים יכולים להיות בלוח?
(ב) האם מתג יכול לדעת באיזה לוח הוא?
(ג) מי יוצר את עצמי
Switch לפי המעוין?(ד) מה הטיפוס של האיברים ב-
switches?9.4 רשימה של עצמים
הרשימה מפרק 6 שומרת כל ערך, ועצם הוא ערך. כל מה שלמדנו על אינדקס ועל הלולאה for נשאר כמו שהוא, והחידוש הוא הנקודה אחרי האיבר: parts[1] נותן עצם, ו-parts[1].ohms נותן תכונה שלו. לרוב הנתונים מגיעים מבחוץ, ולכן בונים את העצמים בלולאה, טאפל (Tuple) אחר טאפל:
class Resistor:
def __init__(self, name, ohms, watts):
self.name = name
self.ohms = ohms
self.watts = watts
def power(self, amps):
return round(amps ** 2 * self.ohms, 3)
def __str__(self):
return self.name + ": " + str(self.ohms) + " ohm"
data = [("R1", 220, 0.25),
("R2", 1000, 0.25),
("R3", 47, 0.5)]
parts = []
for name, ohms, watts in data:
parts.append(Resistor(name, ohms, watts))
print(len(parts))
print(parts[1], parts[1].ohms)
for r in parts:
print(r)3 R2: 1000 ohm 1000 R1: 220 ohm R2: 1000 ohm R3: 47 ohm
amps = 0.02
count = 0
for r in parts:
if r.power(amps) > r.watts:
print(r.name, "overloaded:", r.power(amps), "W")
count += 1
print("Overloaded:", count)
big = parts[0]
for r in parts:
if r.ohms > big.ohms:
big = r
print("Largest:", big)
big.watts = 1
print(parts[1].watts)הפלט: R2 overloaded: 0.4 W , Overloaded: 1 , Largest: R2: 1000 ohm , 1
אלה אותם אלגוריתמים מפרק 6. ההבדל הוא ש-big שומר הפניה לעצם כולו, ולכן אחרי הלולאה אפשר להדפיס או לשנות אותו: שינינו דרך big, והשינוי נראה ב-parts[1].
הדפסת הרשימה: __str__ או __repr__?:
__str__, ו-print(leds[0]) פועלת כרצוננו. אחר כך הדפסנו את כל הרשימה, ויצאו כתובות במקום תיאורים:class Led:
def __init__(self, pin):
self.pin = pin
def __str__(self):
return "LED on pin " + str(self.pin)
leds = [Led(13), Led(12)]
print(leds[0])
print(leds)LED on pin 13 [<__main__.Led object at 0x000001F2A4C8>, <__main__.Led object at 0x000001F2A4C8>]
הכתובת שונה מעצם לעצם ומהרצה להרצה, ואצלכם היא תופיע במספרים אחרים. בספר קיבענו אותה כדי שהפלט לא ישתנה, ולכן כאן שני העצמים מראים אותו מספר. הסיבה היא ש-__str__ הוא התיאור לקורא, וזה מה ש-print ו-str מבקשות. הרשימה לעומת זאת מציגה את איבריה בתיאור הטכני __repr__, ואם לא הגדרנו אותו מקבלים את התיאור שבברירת המחדל, עם הכתובת. נגדיר אותו במחלקה:
def __repr__(self):
return "repr" + str(self.pin)אלה שלושת המקרים בהרצה אמיתית, עם led = Led(13). אם לא הגדרנו __str__, הפעולה print משתמשת במקומו ב-__repr__, ולא להפך:
| מה שהוגדר במחלקה | print(led) | print([led]) |
|---|---|---|
__str__ | str13 | [<__main__.Led object at 0x000001F2A4C8>] |
__repr__ | repr13 | [repr13] |
| שתיהן | str13 | [repr13] |
טיפוסים שונים ברשימה אחת:
באותה רשימה אפשר לשים עצמים ממחלקות שונות. לכל מחלקה __str__ משלה. הפקודה בלולאה אחת, והתוצאה לפי טיפוס העצם:
class Resistor:
def __init__(self, name, ohms):
self.name = name
self.ohms = ohms
def __str__(self):
return self.name + ": " + str(self.ohms) + " ohm"
class Capacitor:
def __init__(self, name, uf):
self.name = name
self.uf = uf
def __str__(self):
return self.name + ": " + str(self.uf) + " uF"
parts = [Resistor("R1", 220), Capacitor("C1", 100)]
for part in parts:
print(part)R1: 220 ohm C1: 100 uF
זו העמסה, כפי שהיא עוזרת לנו כאן: פעולה בשם אחד, ולכל מחלקה גרסה משלה, והמפרש בוחר לפי העצם שבידיו, ובפרק 11 ניתן לזה את שמו המדויק: פולימורפיזם. את אותו רעיון ראינו בסימן +, שמחבר מספרים ומשרשר מחרוזות. בלעדיה הלולאה חייבת להכיר את כל הטיפוסים, וכל טיפוס חדש פירושו שינוי שלה:
for part in parts:
if type(part) == Resistor:
print(part.name + ": " + str(part.ohms) + " ohm")
elif type(part) == Capacitor:
print(part.name + ": " + str(part.uf) + " uF")[Led(0)] * 3 אינו יוצר שלושה עצמים, אלא שלוש הפניות לעצם אחד. החיתוך good[:] יוצר רשימה חדשה, אבל האיברים שלה הם אותם עצמים: מחיקת איבר מן העותק אינה נוגעת במקור, ואילו שינוי עצם דרך העותק נראה במקור:class Led:
def __init__(self, pin):
self.pin = pin
self.lit = False
def __repr__(self):
return str(self.pin) + ":" + str(self.lit)
bad = [Led(0)] * 3
bad[0].lit = True
print(bad)
good = []
for pin in range(3):
good.append(Led(pin))
spare = good[:]
spare[1].lit = True
spare.pop()
print(good)
print(len(good), len(spare))[0:True, 0:True, 0:True] [0:False, 1:True, 2:False] 3 2
class Cell:
def __init__(self, volts):
self.volts = volts
cells = [Cell(3.7), Cell(3.6), Cell(3.9)]
low = cells[0]
for c in cells:
if c.volts < low.volts:
low = c
low.volts = 4.2
print(cells[1].volts)
print(cells[0].volts + cells[2].volts)9.5 רשימה של עצמים כתכונה במחלקה
עד עכשיו הרשימה הייתה משתנה בתוכנית, והלולאות היו מפוזרות סביבה. הצעד הבא הוא להכניס אותה לתוך עצם שמחזיק בה, ומציע פעולות שעובדות עליה. הדוגמה שלנו היא שרשרת נגדים המחוברים בטור:
Chain מייצגתclass Resistor:
def __init__(self, name, ohms):
self.name = name
self.ohms = ohms
def __repr__(self):
return self.name + "=" + str(self.ohms)class Chain:
def __init__(self, volts):
self.volts = volts
self.parts = []
def add(self, resistor):
self.parts.append(resistor)
def total(self):
t = 0
for r in self.parts:
t += r.ohms
return t
def current(self):
return self.volts / self.total()
def drops(self):
i = self.current()
for r in self.parts:
print(r.name, round(i * r.ohms, 2), "V")c = Chain(12)
c.add(Resistor("R1", 100))
c.add(Resistor("R2", 200))
c.add(Resistor("R3", 300))
print(c.parts)
print(c.total(), "ohm", c.current(), "A")
c.drops()
print(c.parts[1].ohms)הפלט:[R1=100, R2=200, R3=300]600 ohm 0.02 AR1 2.0 VR2 4.0 VR3 6.0 V200
הרשימה מאותחלת ריקה בפעולה הבונה, add ממלאת אותה, ו-total ו-drops עוברות עליה. המשתמש עובד מול השרשרת, ואינו צריך לדעת איך הנגדים שלה שמורים. השורה האחרונה מראה את כל מסלול הגישה: עצם, רשימה, איבר, תכונה.
c.parts[1].ohms__init__, בצורה self.parts = []. אם כותבים אותה בגוף המחלקה היא משתנה מחלקה, כפי שלמדנו בפרק 8, וכל העצמים חולקים אותה:class Chain:
parts = []
def add(self, ohms):
self.parts.append(ohms)
a = Chain()
b = Chain()
a.add(100)
print(b.parts)[100]
self.parts ולא parts לבד, אחרת התוכנית נעצרת כי השם אינו מוכר. ומבחוץ אי אפשר לעבור על העצם עצמו: for r in c: עוצרת את התוכנית, כי הרשימה נמצאת בתכונה ואינה העצם.Board נושא נוריות LED. אסור ששתי נוריות יהיו על אותו הדק, ולכן add בודקת זאת לפני ההוספה, ומחזירה True או False.class LED:
def __init__(self, pin, color):
self.pin = pin
self.color = color
self.on = False
def toggle(self):
self.on = not self.on
def __repr__(self):
return self.color + ":" + str(self.on)class Board:
def __init__(self, name):
self.name = name
self.leds = []
def led_at(self, pin):
for led in self.leds:
if led.pin == pin:
return led
return None
def add(self, led):
if self.led_at(led.pin) is None:
self.leds.append(led)
return True
return False
def __str__(self):
return self.name + " " + str(self.leds)uno = Board("Uno")
print(uno.add(LED(13, "red")))
print(uno.add(LED(13, "blue")))
uno.add(LED(12, "green"))
uno.led_at(13).toggle()
print(uno)
print(uno.led_at(9))הפלט:TrueFalseUno [red:True, green:False]None
הפעולה add נשענת על led_at ואינה כותבת את החיפוש מחדש. ו-led_at(13) מחזירה את אותו עצם שברשימה, ולכן נדלקה הנורית שנמצאת בלוח. כשהיא אינה מוצאת נורית היא מחזירה None, ואם קוראים ממנה תכונה בלי בדיקה התוכנית נעצרת בשגיאה AttributeError, ולכן בודקים קודם בצורה led is None. הביטוי str(self.leds) מציג את הרשימה בתיאור __repr__, ובגלל זה הגדרנו אותו ב-LED.
Resistor ו-Chain כמו בתחילת הסעיף. מה מדפיסה התוכנית הבאה?c = Chain(6)
c.add(Resistor("A", 10))
c.add(Resistor("B", 20))
d = Chain(6)
d.add(c.parts[0])
d.parts[0].ohms = 30
print(c.total(), d.total())
print(len(c.parts), len(d.parts))בפרק 8 קיבל כל חיישן מחלקה, אבל החיישנים נשארו משתנים נפרדים בתוכנית. עכשיו נאסוף אותם לעצם אחד: תחנה שיש לה שם ורשימת חיישנים, שמוסיפה אליה, מחפשת בה, סופרת התראות ומדפיסה דוח. נתחיל מן התרשים:
המחלקה Sensor היא אותה מחלקה שכתבנו בפרק 8, ונשים אותה בקובץ לפני המחלקה Station:
class Station:
cutoff = 11.8
def __init__(self, name):
self.name = name
self.sensors = []
def add(self, sensor):
self.sensors.append(sensor)
def find(self, name):
for s in self.sensors:
if s.name == name:
return s
return None
def alarm_count(self):
count = 0
for s in self.sensors:
if s.unit == "V" and s.value() < Station.cutoff:
count += 1
return count
def report(self):
print("Station", self.name)
for s in self.sensors:
print(" ", s)
print("Alarms:", self.alarm_count())st = Station("Roof")
st.add(Sensor("Battery", "V", 4))
st.add(Sensor("Backup", "V", 4))
st.add(Sensor("Temp", "C", 100))
st.find("Battery").read(620)
st.find("Backup").read(585)
st.find("Temp").read(65)
st.report()
print(st.find("Fan"))הפלט:Station Roof Battery: 12.12 V Backup: 11.44 V Temp: 31.77 CAlarms: 1None
החיישנים שונים, ו-__str__ שבמחלקה גורמת לדוח להדפיס כל אחד מהם בפקודה אחת. סף הניתוק 11.8 V מפרק 3 הוא ערך אחד לכל התחנות, ולכן הוא משתנה מחלקה. סופרים התראה רק לחיישני מתח, כי טמפרטורה נמוכה אינה סוללה ריקה. הפקודה st.find("Battery").read(620) פועלת כי find מחזירה את אותו עצם שברשימה, והשורה האחרונה מראה מה חוזר כשהחיישן אינו קיים.
נסו בעצמכם: הוסיפו ל-Station פעולה active_count שמחזירה את מספר החיישנים שקראו משהו, כלומר ש-raw שלהם שונה מאפס. בפרק 10 הקריאות יגיעו מן הארדואינו דרך היציאה הטורית, במקום שנכתוב אותן בתוכנית.
עיקרי הפרק:
- טיפוס מורכב הוא מחלקה שאחת מתכונותיה היא עצם ממחלקה אחרת. השלם מבקש מן החלק, ואינו כותב מחדש את החישוב שלו.
- תכונה שומרת הפניה ולא עותק. שתי הפניות לעצם אחד פירושן שכל שינוי נראה בשתיהן: פעם טעות, ופעם תכנון מכוון.
isשואל אם זה עצם אחד, ו-==במחלקות שלנו שואל אותו דבר אם לא הגדרנו אחרת.- תרשים UML הוא שלושה תאים, סימן גישה, וקווים עם ריבוי, ולא כותבים בו
self. print(lst)מדפיסה את הרשימה בעזרת__repr__, ולא בעזרת__str__.- כתיבת
__str__לכל מחלקה מאפשרת לאותה פקודה לפעול על טיפוסים שונים בלי בדיקה. - רשימה שהיא תכונה מאותחלת ב-
__init__, ולא בגוף המחלקה. - חיפוש מחזיר את העצם עצמו או
None, ובודקים לפני שמשתמשים בתוצאה.
סיכום פקודות הפרק:
| פקודה | מה היא עושה |
|---|---|
| self.part = part | תכונה ששומרת הפניה לעצם ממחלקה אחרת |
| self.part = Part(x, y) | העצם יוצר את החלק שלו בעצמו: הרכבה |
| a.b.c a.b.f() | גישה דרך עצם פנימי |
| a is b x is None | האם זה עצם אחד, והאם אין עצם |
| items = [Part(1), Part(2)] | רשימה של עצמים |
| for item in items: | מעבר על העצמים |
| items[i].x | תכונה של איבר ברשימה |
| self.items = [] | רשימה כתכונה, מאותחלת בפעולה הבונה |
| def __repr__(self): | התיאור שמופיע בהדפסת הרשימה |
| self._x | תכונה פרטית, בהסכמה בין מתכנתים |
מילון מונחים:
| עברית | English | ערבית |
|---|---|---|
| טיפוס מורכב | Compound type | النوع المركّب |
| הכלה | Containment | الاحتواء |
| הפניה | Reference | المرجع |
| תרשים UML | UML diagram | مخطّط UML |
| תרשים מחלקה | Class diagram | مخطّط الفئة |
| הרשאת גישה | Access modifier | محدِّد الوصول |
| ציבורי | Public | عامّ |
| פרטי | Private | خاصّ |
| שיוך | Association | الارتباط |
| הרכבה | Composition | التركيب |
| קיבוץ | Aggregation | التجميع |
| ריבוי | Multiplicity | التعدّدية |
| העמסה | Overloading | التحميل الزائد |
6.0 , 24 , 12.09 1 , True FalseSwitch תכונה שמצביעה על הלוח. (ג) הלוח עצמו. (ד) עצמים מן המחלקה Switch.4.2 , 7.650 30 , 2 1שאלות לתרגול:
Point ו-Circle כמו בסעיף 9.2. מה מדפיסה התוכנית?c = Circle(Point(1, 2), 3) c.center.move(2, 2) print(c) print(c.center.x + c.radius) c.center = Point(0, 0) print(c.contains(Point(3, 0)))
center (3, 4), radius 3 , 6 , True__str__ של ההדק מחזירה למשל pin 2 INPUT. צרו לחצן ושנו את מצב ההדק שלו דרך הלחצן.class Pin:
def __init__(self, number, mode):
self.number = number
self.mode = mode
def __str__(self):
return "pin " + str(self.number) + " " + self.mode
class Button:
def __init__(self, label, pin):
self.label = label
self.pin = pin
def __str__(self):
return self.label + " on " + str(self.pin)
b = Button("Start", Pin(2, "INPUT"))
b.pin.mode = "OUTPUT"
print(b)Start on pin 2 OUTPUTclass Motor:
def __init__(self, volts):
self.volts = volts
class Robot:
def __init__(self, name):
self.name = name
self.motors = []
def add(self, motor):
self.motors.append(motor)
def count(self):
return len(self.motors)class Relay:
def __init__(self, pin):
self.pin = pin
def __str__(self):
return "Relay on " + str(self.pin)
relays = [Relay(4), Relay(5)]
print(relays)__repr__, והוא אינו מוגדר. נשנה את שם הפעולה, ואז גם print(relays[0]) תעבוד:class Relay:
def __init__(self, pin):
self.pin = pin
def __repr__(self):
return "Relay on " + str(self.pin)
relays = [Relay(4), Relay(5)]
print(relays)
print(relays[0])[Relay on 4, Relay on 5] , Relay on 4class Cell:
def __init__(self, volts):
self.volts = volts
class Pack:
def __init__(self, cell):
self.cell = cell
c = Cell(3.7)
p1 = Pack(c)
p2 = Pack(c)
p3 = Pack(Cell(3.7))
p1.cell.volts = 3.2
print(p2.cell.volts, p3.cell.volts, c.volts)3.2 3.7 3.2. העצמים p1 ו-p2 מצביעים על אותו תא c, ול-p3 יש תא משלו.Wire שמחזיקה שתי נקודות מן המחלקה Point, ויש לה length() ו-resistance(), בהנחה שההתנגדות של החוט היא 0.02 Ω ליחידת אורך.class Wire:
ohm_per_m = 0.02
def __init__(self, start, end):
self.start = start
self.end = end
def length(self):
return self.start.distance(self.end)
def resistance(self):
return round(self.length() * Wire.ohm_per_m, 3)
w = Wire(Point(0, 0), Point(6, 8))
print(w.length(), w.resistance())
w.end.move(-6, 0)
print(w.length(), w.resistance())10.0 0.2 , 8.0 0.16Bank לנגדים המחוברים במקביל, עם רשימת נגדים ופעולה total לפי החוק:class Resistor:
def __init__(self, ohms):
self.ohms = ohms
class Bank:
def __init__(self):
self.parts = []
def add(self, r):
self.parts.append(r)
def total(self):
s = 0
for r in self.parts:
s += 1 / r.ohms
return round(1 / s, 2)
b = Bank()
b.add(Resistor(100))
b.add(Resistor(100))
print(b.total())
b.add(Resistor(50))
print(b.total())50.0 , 25.0Board מסעיף 9.5 פעולה remove(pin) שמוחקת את הנורית שעל ההדק, ומחזירה True או False. השתמשו ב-led_at ואל תכתבו את החיפוש מחדש.def remove(self, pin):
led = self.led_at(pin)
if led is None:
return False
self.leds.remove(led)
return TrueFalse , True , Uno [green:False]class Lamp:
def __init__(self, volts, amps):
self.volts = volts
self.amps = amps
def watts(self):
return self.volts * self.amps
class Heater:
def __init__(self, volts, ohms):
self.volts = volts
self.ohms = ohms
def watts(self):
return self.volts ** 2 / self.ohms
loads = [Lamp(12, 0.5), Heater(12, 24), Lamp(12, 1)]
total = 0
for load in loads:
total += load.watts()
print(total, "W")24.0 W. לכל מחלקה פעולה watts משלה, כפי שעשינו עם __str__.Station פעולה sort_by_value() שממיינת את החיישנים בסדר עולה לפי הערך, במיון בועות מפרק 6. בזמן ההחלפה, מה בעצם עובר ברשימה?def sort_by_value(self):
n = len(self.sensors)
for i in range(n - 1):
for j in range(n - 1 - i):
a = self.sensors[j]
b = self.sensors[j + 1]
if a.value() > b.value():
self.sensors[j] = b
self.sensors[j + 1] = aBattery ו-Backup, האיבר הראשון אחרי המיון הוא: Backup: 11.44 VNetwork שמחזיקה רשימה מן המחלקה Station, ויש לה total_alarms(). ציירו את התרשים של שלוש המחלקות.class Network:
def __init__(self):
self.stations = []
def add(self, station):
self.stations.append(station)
def total_alarms(self):
total = 0
for st in self.stations:
total += st.alarm_count()
return totalYard, היא מדפיסה: 2center=Point(0, 0). מה מדפיסה התוכנית, ולמה? תקנו אותה.class Circle:
def __init__(self, radius, center=Point(0, 0)):
self.center = center
self.radius = radius
a = Circle(5)
b = Circle(8)
a.center.move(3, 4)
print(b.center)(3, 4). ערך ברירת המחדל נוצר פעם אחת, בהגדרת הפעולה, ולכן שני המעגלים חולקים נקודה אחת. נקבע אותו ל-None, וניצור את הנקודה בתוך הפעולה:class Circle:
def __init__(self, radius, center=None):
if center is None:
center = Point(0, 0)
self.center = center
self.radius = radius
a = Circle(5)
b = Circle(8)
a.center.move(3, 4)
print(a.center, b.center)(3, 4) (0, 0)Circle פעולה copy() שמחזירה מעגל חדש ועצמאי לגמרי מן המקורי. הוכיחו בהרצה שהזזת העותק אינה מזיזה את המקור.def copy(self):
twin = Point(self.center.x, self.center.y)
return Circle(twin, self.radius)a = Circle(Point(1, 1), 4) b = a.copy() b.center.move(5, 5) print(a.center, b.center) print(a is b, a.center is b.center)
(1, 1) (6, 6) , False False. אילו כתבנו Circle(self.center, self.radius) עם הפעולה הבונה המקורית, ששומרת רק את ההפניה, היינו מקבלים מעגל חדש עם אותו מרכז ישן.