יסודות התכנות בשפת Python · פרק 8 — ייצוג טיפוס חדש על ידי מחלקה⌂📚🏠
פרק 8

ייצוג טיפוס חדש על ידי מחלקה

20 שעות לימוד

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

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

8.1 הגדרת המושג מחלקה

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

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

print(type(12))
print(type("LED"))
print(type([1, 2]))
print("LED".lower())
print([3, 1, 2].count(1))
פלט
<class 'int'>
<class 'str'>
<class 'list'>
led
1

כדי לראות שמחלקה אחת נותנת עצמים עצמאיים, נחזור אל Turtle מפרק 1 וניצור ממנה שני עצמים:

import turtle
a = turtle.Turtle()
b = turtle.Turtle()
a.color("red")
b.color("blue")
a.forward(100)
b.left(90)
b.forward(60)
print(a.position(), b.position())
print(a.heading(), b.heading())
print(type(a))
פלט
(100.00,0.00) (0.00,60.00)
0.0 90.0
<class 'turtle.Turtle'>
5010050
שני עצמים מאותה מחלקה: כל עצם זוכר את מיקומו, כיוונו וצבעו

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

למה לכתוב מחלקות משלנו?:

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

r1_name = "R1"
r1_ohms = 220
r2_name = "R2"
r2_ohms = 1000
print(r1_name, 12 / r1_ohms)
print(r2_name, 12 / r2_ohms)
פלט
R1 0.05454545454545454
R2 0.012

התוכנית עובדת, אבל קשה להגדיל אותה. עם עשרה נגדים נצטרך עשרות משתנים, ושום דבר בתוכנית לא אומר ש-r1_name ו-r1_ohms שייכים לאותו רכיב, או שהמשתנה r2_ohms הוא נגד. וגם החישוב חוזר בכל פעם שצריך אותו. מחלקה פותרת את שתי הבעיות יחד: היא מאגדת את נתוני הרכיב בעצם אחד, ומציבה לצדם את הפעולות שעובדות עליהם. כך "נגד" הופך לטיפוס חדש בתוכנית, כמו ש-str הוא טיפוס.

טעות נפוצה: מבלבלים בין מחלקה לעצם, ואומרים למשל שהערך של המחלקה "נגד" הוא 220 Ω. למחלקה אין ערך כזה: היא אומרת שלכל נגד יש ערך, ו-220 Ω הוא הערך של עצם אחד.
הערה: בשאלון כתוב מחלקה, עצם ופעולה בונה, וכך גם בספר. במעבדה ובמקורות אחרים תפגשו גם אובייקט ובנאי, והמשמעות זהה.
שאלה 8.1: קבעו בכל זוג מי המחלקה ומי העצם:
(א) "LED" ו-str
(ב) Turtle ו-a
(ג) "נורית" ו"הנורית האדומה שעל לוח הניסויים".

8.2 פיתוח מחלקה המייצגת טיפוס

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

  1. נקבע מה נרצה לייצג: נגד במעגל.
  2. נקבע את התכונות: שם הרכיב name, ערכו באוהם ohms, וההספק המרבי שהוא עומד בו בואט watts.
  3. נקבע את הפעולות: הזרם במתח נתון current, ההספק במתח נתון power, והאם המתח הזה בטוח is_safe.
  4. נכתוב את המחלקה.
  5. ניצור עצמים, ונבדוק את התוצאות על דוגמה שאנחנו יודעים את תשובתה.
I = V R
P = V2 R

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

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

    def current(self, volts):
        return volts / self.ohms

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

    def is_safe(self, volts):
        p = self.power(volts)
        return p <= self.watts


r1 = Resistor("R1", 400, 0.25)
r2 = Resistor("R2", 1000, 0.25)
print(r1.name, r1.ohms)
print(r2.name, r2.ohms)
print(r2.current(12))
print(r1.power(12), r1.is_safe(12))
print(r2.power(12), r2.is_safe(12))
print(type(r1))
פלט
R1 400
R2 1000
0.012
0.36 False
0.144 True
<class '__main__.Resistor'>
מחלקה: תיאור כלליclass Resistornameohmswattscurrent(volts)power(volts)is_safe(volts)שני עצמים: לכל אחד ערכים משלוr1name = "R1"ohms = 400watts = 0.25r2name = "R2"ohms = 1000watts = 0.25יצירהResistor("R1", 400, 0.25)Resistor("R2", 1000, 0.25)
מחלקה אחת ושני עצמים: המחלקה קובעת אילו תכונות ופעולות יש, והעצם נושא את הערכים

הבדיקה היא הצעד האחרון בפיתוח: במתח 12 V ובהתנגדות 1000 Ω נצפה לזרם של 0.012 A, וזה מה שהודפס בשורה השלישית. הרכיב r1 אינו עומד במתח הזה, כי ההספק שלו יעבור את 0.25 W. השורה האחרונה בפלט אומרת ש-Resistor הוא עכשיו טיפוס חדש, ו-__main__ פירושו שהוא מוגדר בתוכנית שלנו עצמה.

טעות נפוצה: הגדרת מחלקה לבדה לא עושה כלום: לא יוצרת נגד ולא מחשבת זרם. התוכנית פועלת כשיוצרים עצם ומזמנים את פעולותיו. והתוכנית רצה מלמעלה למטה, ולכן כותבים את המחלקה לפני השורה שמשתמשת בה לראשונה.
שאלה 8.2: מה מדפיסה התוכנית הבאה?
class Heater:
    def __init__(self, volts, ohms):
        self.volts = volts
        self.ohms = ohms

    def amps(self):
        return self.volts / self.ohms

h1 = Heater(230, 46)
h2 = Heater(12, 3)
print(h1.amps())
print(h2.amps())
print(h1.ohms + h2.ohms)

8.3 כותרת המחלקה, ההבדל בין משתנה במחלקה (class variable) לבין משתנה במופע (instance variable)

כותרת המחלקה היא השורה הראשונה בהגדרתה, ויש בה:

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

class Fuse:
    pass

f1 = Fuse()
f2 = Fuse()
print(type(f1))
print(f1 is f2)
פלט
<class '__main__.Fuse'>
False
טעות נפוצה: בלי הנקודתיים בסוף הכותרת התוכנית כלל לא מתחילה, ומתקבלת שגיאת התחביר SyntaxError.

שני סוגי משתנים:

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

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

R = Vsupply − VLED Imax
class Led:
    max_current = 0.02

    def __init__(self, color, drop):
        self.color = color
        self.drop = drop

    def resistor(self, supply):
        v = supply - self.drop
        return round(v / Led.max_current)

red = Led("red", 2.0)
blue = Led("blue", 3.2)
print(red.color, blue.color)
print(red.max_current, blue.max_current)
print(Led.max_current)
print(red.resistor(5), blue.resistor(5))
Led.max_current = 0.01
print(red.max_current, blue.max_current)
print(red.resistor(5), blue.resistor(5))
פלט
red blue
0.02 0.02
0.02
150 90
0.01 0.01
300 180
משתנה במחלקה: עותק אחדclass Ledmax_current = 0.02משתנים במופע: לכל עצםredcolor = "red"drop = 2.0bluecolor = "blue"drop = 3.2red.max_currentblue.max_current
כל עצם קורא את max_current מן המחלקה, כי אין לו משתנה בשם הזה

כשכותבים red.max_current פייתון מחפשת קודם בעצם, לא מוצאת את המשתנה, וקוראת אותו מן המחלקה. לכן כשעדכנו את Led.max_current התוצאה השתנתה בשני העצמים יחד.

משתנה במחלקהמשתנה במופע
איפה כותביםבגוף המחלקה, מחוץ לפעולותבתוך פעולה, עם self.
כמה עותקיםאחדאחד לכל עצם
איך מגיעים אליוLed.max_current או red.max_currentred.color בלבד
איך משניםבשם המחלקה: Led.max_current = 0.01בשם העצם: red.color = "pink"
מתי משתמשיםבערך ששייך לכל הטיפוסבערך ששייך לרכיב עצמו

ההפך לא עובד: משתנה במופע אינו נקרא דרך המחלקה, כי המחלקה לא יודעת לאיזה עצם התכוונו, ו-Led.color עוצר את התוכנית בשגיאה AttributeError.

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

    def __init__(self, name):
        self.name = name
        Relay.count = Relay.count + 1

k1 = Relay("K1")
print(Relay.count)
k2 = Relay("K2")
k3 = Relay("K3")
print(Relay.count, k1.count)
פלט
1
3 3
טעות נפוצה: השמה דרך העצם אינה משנה את המשתנה במחלקה, אלא יוצרת משתנה במופע חדש באותו שם אצל העצם הזה בלבד, והוא מסתיר אותו:
class Led:
    max_current = 0.02

red = Led()
blue = Led()
red.max_current = 0.03
print(red.max_current)
print(blue.max_current)
print(Led.max_current)
פלט
0.03
0.02
0.02

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

שאלה 8.3: מה מדפיסה התוכנית הבאה?
class Socket:
    volts = 230

    def __init__(self, room):
        self.room = room

a = Socket("lab")
b = Socket("office")
Socket.volts = 120
b.volts = 12
b.room = "store"
print(a.volts, a.room)
print(b.volts, b.room)
print(Socket.volts)

8.4 תכונות המחלקה

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

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

class Supply:
    def __init__(self, volts, limit):
        self.volts = volts
        self.limit = limit
        self.on = False

bench = Supply(12, 1.5)
small = Supply(5, 0.5)
print(bench.volts, bench.limit, bench.on)
bench.volts = 9
bench.on = True
print(bench.volts, bench.on)
print(small.volts, small.on)
פלט
12 1.5 False
9 True
5 False
טעות נפוצה: קריאה של תכונה שלא קיימת עוצרת את התוכנית בשגיאה AttributeError. כתיבה עם שגיאת הקלדה מסוכנת יותר, כי היא לא נותנת שום הודעה. הפקודה bench.volt = 9 יצרה תכונה חדשה בשם volt, ו-volts נשארה כפי שהייתה:
class Supply:
    def __init__(self, volts):
        self.volts = volts

bench = Supply(12)
bench.volt = 9
print(bench.volts)
print(bench.volt)
פלט
12
9

שני שמות לעצם אחד:

המשתנה לא מכיל את העצם עצמו, אלא מצביע עליו. לכן ההשמה same = bench אינה מעתיקה את העצם אלא נותנת לו שם שני, וכל שינוי דרך אחד השמות נראה גם דרך השני. לעומת זאת other = Supply(12) יצרה עצם אחר, גם אם ערכיו שווים. והאופרטור == בין שני עצמים של המחלקה שלנו שואל את מה ש-is שואל, כל עוד לא לימדנו אותו אחרת, ולכן נתן False:

class Supply:
    def __init__(self, volts):
        self.volts = volts

bench = Supply(12)
same = bench
other = Supply(12)
same.volts = 5
print(bench.volts, same.volts, other.volts)
print(bench is same, bench is other)
print(bench == other)
פלט
5 5 12
True False
False

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

הערה: שום דבר לא מונע מאיתנו לכתוב bench.volts = -50 מחוץ למחלקה, וכך נקבל ספק במתח חסר משמעות. בסעיף 8.7 נכתוב פעולה ששומרת על התכונה מערכים כאלה.
שאלה 8.4: מה מדפיסה התוכנית הבאה?
class Lamp:
    def __init__(self, watts):
        self.watts = watts
        self.hours = 0

desk = Lamp(9)
door = desk
door.hours = 5
door = Lamp(60)
door.hours = door.hours + 2
print(desk.watts, desk.hours)
print(door.watts, door.hours)

8.5 פעולות בונות (__init__), פרמטרים אופציונאליים בפעולה בונה

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

כשכותבים bench = Supply(12, 1.5) קורים שלושה דברים לפי הסדר:

  1. פייתון יוצרת עצם חדש וריק.
  2. היא מזמנת את __init__, ומעבירה אליה את העצם הזה ב-self ואת שאר הערכים בפרמטרים הבאים.
  3. היא מחזירה את העצם, והעצם מוצב במשתנה bench.

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

פרמטרים אופציונליים:

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

class Battery:
    def __init__(self, name, capacity=7.0,
                 volts=12):
        self.name = name
        self.capacity = capacity
        self.volts = volts

main = Battery("Main")
back = Battery("Back", 2.2)
cell = Battery("Cell", 2.2, 3.7)
coin = Battery("Coin", volts=3)
print(main.name, main.capacity, main.volts)
print(back.name, back.capacity, back.volts)
print(cell.name, cell.capacity, cell.volts)
print(coin.name, coin.capacity, coin.volts)
פלט
Main 7.0 12
Back 2.2 12
Cell 2.2 3.7
Coin 7.0 3
טעות נפוצה: פרמטרי החובה באים ראשונים, אחרת פייתון לא יודעת איזה ערך שייך לאיזה פרמטר. לכן השורה def __init__(self, capacity=7.0, name): היא שגיאת תחביר. ואם לא מעבירים ערך לפרמטר חובה, התוכנית נעצרת בשגיאה TypeError.
טעות נפוצה: קו תחתון אחד במקום שניים ב-_init_ הופך אותה לפעולה רגילה שלא רצה ביצירה, ופייתון דוחה את הערכים שמעבירים. ושכחת self. הופכת את ההשמה למשתנה מקומי שאובד בסוף הפעולה, והתכונה לא נוצרת:
class Battery:
    def __init__(self, name):
        name = name

main = Battery("Main")
print(main.name)
פלט
AttributeError: 'Battery' object has no attribute 'name'
דוגמה 8.2
הפעולה הבונה היא המקום הטבעי לבדוק ערכים, כי ממנה מתחיל כל עצם. נייצג עמעם למנורה של 60 W. רמת התאורה היא בין 0 ל-100 אחוז, וכל ערך אחר פירושו כבוי. ההספק הוא תכונה שמחשבים מן הרמה, ולא מקבלים מבחוץ.
פתרון:
class Dimmer:
    def __init__(self, level=0):
        if level < 0 or level > 100:
            level = 0
        self.level = level
        self.watts = 60 * level / 100

value = int(input("Level: "))
hall = Dimmer(value)
print(hall.level, hall.watts)
wall = Dimmer(250)
print(wall.level, wall.watts)
print(Dimmer().level)
פלט
Level: 40
40 24.0
0 0.0
0

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

טעות נפוצה: ערך ברירת המחדל מחושב פעם אחת, בהגדרת הפעולה. לכן אם הוא רשימה, כל העצמים חולקים אותה:
class Probe:
    def __init__(self, name, data=[]):
        self.name = name
        self.data = data

p1 = Probe("P1")
p2 = Probe("P2")
p1.data.append(5)
print(p2.data)
print(p1.data is p2.data)
פלט
[5]
True

הפתרון הוא ליצור את הרשימה בתוך הפעולה הבונה, בשורה self.data = [], וכך כל עצם מקבל רשימה משלו.

שאלה 8.5: מה מדפיסה התוכנית הבאה?
class Motor:
    def __init__(self, volts=24, rpm=0):
        if rpm < 0:
            rpm = 0
        self.volts = volts
        self.rpm = rpm

m1 = Motor()
m2 = Motor(12)
m3 = Motor(rpm=-300)
m4 = Motor(48, 900)
print(m1.volts, m1.rpm)
print(m2.volts, m2.rpm)
print(m3.volts, m3.rpm)
print(m4.volts, m4.rpm)

8.6 אופרטור self

פעולה כתובה במחלקה פעם אחת, אבל היא עובדת על עצמים רבים. אז איך היא יודעת על איזה עצם לעבוד? התשובה היא הפרמטר הראשון, self: כשכותבים a.drain(2), פייתון מעבירה את העצם שלפני הנקודה אל self. הפקודות a.drain(2) ו-Cell.drain(a, 2) שקולות לגמרי, וגם בצורה השנייה השתמשנו בתוכנית הבאה:

class Cell:
    def __init__(self, name, charge):
        self.name = name
        self.charge = charge

    def drain(self, amount):
        self.charge = self.charge - amount
        print(self.name, self.charge)

a = Cell("A", 10)
b = Cell("B", 30)
a.drain(2)
b.drain(5)
Cell.drain(a, 1)
print(a.charge, b.charge)
פלט
A 8
B 25
A 7
7 25
a.drain(2)def drain(self, amount):self.charge = self.charge- amountaname = "A"charge = 10 → 8השתנהbname = "B"charge = 30לא השתנהselfb.drain(5)def drain(self, amount):self.charge = self.charge- amountaname = "A"charge = 8לא השתנהbname = "B"charge = 30 → 25השתנהself
הפעולה אחת, ו-self מצביע בכל זימון על העצם שלפני הנקודה

הכללים של self:

השם self.charge שונה לגמרי מ-charge, גם אם כותבים אותם באותה שורה. self.charge היא תכונה שנשארת בעצם, ו-charge הוא פרמטר שנעלם בסוף הפעולה, וכמוהו המשתנה המקומי before:

class Cell:
    def __init__(self, charge):
        self.charge = charge

    def add(self, charge):
        before = self.charge
        self.charge = self.charge + charge
        return self.charge - before

a = Cell(10)
print(a.add(4))
print(a.charge)
פלט
4
14
טעות נפוצה: שוכחים את self בהגדרת הפעולה. פייתון מעבירה אותו בכל זאת, ולפעולה מגיע פרמטר שלא ציפתה לו. ההודעה מדברת על אפס פרמטרים ועל פרמטר אחד, אף שלא העברנו כלום:
class Cell:
    def status():
        return "ready"

a = Cell()
print(a.status())
פלט
TypeError: Cell.status() takes 0 positional arguments but 1 was given
טעות נפוצה: ואם שוכחים את self. בתוך הפעולה, פייתון מחפשת משתנה רגיל בשם הזה, והתוכנית נעצרת בשגיאה NameError.
שאלה 8.6: מה מדפיסה התוכנית הבאה? שימו לב ש-pour מזמנת פעולה על שני עצמים שונים.
class Tank:
    def __init__(self, level):
        self.level = level

    def fill(self, liters):
        self.level = self.level + liters

    def pour(self, other, liters):
        self.fill(-liters)
        other.fill(liters)

x = Tank(50)
y = Tank(10)
x.pour(y, 20)
y.fill(5)
print(x.level, y.level)

8.7 פעולות המחלקה, כולל פרמטרים עם ערך ברירת מחדל

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

דוגמה 8.3
נייצג סוללה. נתונה הקיבולת הכוללת שלה full, 10 Ah, ונעקוב אחר המטען שנשאר בה left. הפעולות is_low ו-use עובדות גם בלי להעביר את כל הפרמטרים: סף ברירת המחדל לסוללה כמעט ריקה הוא 20 אחוז, ומשך ברירת המחדל הוא שעה אחת.
פתרון:
class Battery:
    def __init__(self, name, full):
        self.name = name
        self.full = full
        self.left = full

    def percent(self):
        return 100 * self.left / self.full

    def is_low(self, limit=20):
        return self.percent() < limit

    def use(self, amps, hours=1):
        self.left -= amps * hours
        if self.left < 0:
            self.left = 0

    def recharge(self):
        self.left = self.full

pack = Battery("Pack", 10)
print(pack.percent(), pack.is_low())
pack.use(2)
print(pack.percent(), pack.is_low())
pack.use(1.5, 4.5)
print(pack.left, pack.percent())
print(pack.is_low(), pack.is_low(10))
pack.use(amps=3, hours=9)
print(pack.left)
pack.recharge()
print(pack.left)
פלט
100.0 False
80.0 False
1.25 12.5
True False
0
10

נשים לב לשלושה דברים בפלט. ב-use יש פרמטר חובה אחד ופרמטר אופציונלי אחד, ולכן כתבנו פעם use(2) ופעם use(1.5, 4.5). אחר כך is_low() בערך ברירת המחדל מול is_low(10) בסף אחר. ובזימון use(amps=3, hours=9) השתמשנו בשמות הפרמטרים, והמטען לא ירד מתחת לאפס בזכות התנאי שבתוך הפעולה.

פעולה ששומרת על תכונה:

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

class Supply:
    MAX = 30

    def __init__(self):
        self.volts = 0

    def set_volts(self, volts):
        if volts < 0 or volts > Supply.MAX:
            return False
        self.volts = volts
        return True

bench = Supply()
print(bench.set_volts(12), bench.volts)
print(bench.set_volts(50), bench.volts)
print(bench.set_volts(-3), bench.volts)
פלט
True 12
False 12
False 12

עצם כפרמטר, ועצם כתוצאה:

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

Rs = R1 + R2
Rp = R1 · R2 R1 + R2
class Resistor:
    def __init__(self, name, ohms):
        self.name = name
        self.ohms = ohms

    def series(self, other):
        name = self.name + "+" + other.name
        total = self.ohms + other.ohms
        return Resistor(name, total)

    def parallel(self, other):
        name = self.name + "|" + other.name
        both = self.ohms + other.ohms
        total = self.ohms * other.ohms
        return Resistor(name, total / both)

r1 = Resistor("R1", 100)
r2 = Resistor("R2", 300)
r3 = Resistor("R3", 50)
s = r1.series(r2)
print(s.name, s.ohms)
n = r1.parallel(r2).series(r3)
print(n.name, n.ohms)
print(r1.ohms, r2.ohms)
פלט
R1+R2 400
R1|R2+R3 125.0
100 300

הפעולה series עובדת על שני עצמים: self הוא זה שלפני הנקודה, ו-other הוא זה שבסוגריים. ומכיוון שהתוצאה היא עצם, אפשר לכתוב אחריה מיד נקודה נוספת, כמו ב-r1.parallel(r2).series(r3). שני הנגדים המקוריים לא השתנו, כפי שמראה השורה האחרונה.

טעות נפוצה: בלי סוגריים לא מזמנים את הפעולה אלא רק מזכירים את שמה, ולכן הביטוי pack.percent אינו נותן מספר. ותכונה ופעולה באותו שם מתנגשות: אם מציבים ב-self.power וכותבים גם פעולה power(), התכונה מסתירה את הפעולה אצל העצם הזה. לכן נותנים להן שמות שונים.
טעות נפוצה: פעולה מצווה שלא כותבת return מחזירה את הערך None, ואין טעם לשמור את התוצאה שלה:
class Cell:
    def __init__(self, charge):
        self.charge = charge

    def drain(self, amount):
        self.charge = self.charge - amount

a = Cell(10)
result = a.drain(4)
print(result)
print(a.charge)
פלט
None
6
שאלה 8.7: מה מדפיסה התוכנית הבאה?
class Timer:
    def __init__(self, limit=60):
        self.limit = limit
        self.time = 0

    def tick(self, seconds=1):
        self.time = self.time + seconds

    def left(self):
        return self.limit - self.time

    def done(self):
        return self.left() <= 0

t = Timer(10)
t.tick()
t.tick(4)
print(t.time, t.left(), t.done())
t.tick(seconds=5)
print(t.time, t.left(), t.done())
print(Timer().limit)

8.8 העמסת הפעולה __repr__ או __str__ לצורך החזרת מחרוזת לתיאור העצם

מה קורה כשמדפיסים עצם של המחלקה שלנו?

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

r1 = Resistor("R1", 220)
print(r1)
פלט
<__main__.Resistor object at 0x000001F2A4C8>

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

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

הפעולה __str__ מקבלת רק את self, ומחזירה מחרוזת שמתארת את העצם. הפקודות print(r1) ו-str(r1) מזמנות אותה מעצמן:

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

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

r1 = Resistor("R1", 220)
print(r1)
text = str(r1)
print(len(text))
print("Part:", r1)
print("Part: " + str(r1))
פלט
R1 = 220 ohm
12
Part: R1 = 220 ohm
Part: R1 = 220 ohm
טעות נפוצה: כותבים print במקום return. ההדפסה קורית, ואז לא חוזר כלום, והתוכנית נעצרת:
class Resistor:
    def __init__(self, ohms):
        self.ohms = ohms

    def __str__(self):
        print(self.ohms, "ohm")

r1 = Resistor(220)
print(r1)
פלט
220 ohm
TypeError: __str__ returned non-string (type NoneType)

והערך שחוזר חייב להיות מחרוזת, גם אם התכונה עצמה מספר. לכן return self.ohms נותן את אותה שגיאה, וממירים בעזרת str.

הפעולה __repr__:

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

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

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

    def __repr__(self):
        text = str(self.ohms)
        return "Part(" + text + ")"

part = Part(220)
print(part)
print(repr(part))
print([part, Part(470)])
פלט
220 ohm
Part(220)
[Part(220), Part(470)]
מה כותביםמה מזומן
print(x) , str(x)__str__, ואם אין אז __repr__
repr(x)__repr__
print([x, y])__repr__ לכל איבר ברשימה

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

בניית מחרוזת בעזרת הקידומת f:

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

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

    def __str__(self):
        return f"R = {self.ohms} ohm"

r1 = Resistor(220)
print(r1)
print(f"Twice: {r1.ohms * 2}")
פלט
R = 220 ohm
Twice: 440
שאלה 8.8: מה מדפיסה התוכנית הבאה?
class Bulb:
    def __init__(self, watts):
        self.watts = watts
        self.on = False

    def switch(self):
        self.on = not self.on

    def __str__(self):
        text = str(self.watts) + " W: "
        if self.on:
            return text + "ON"
        return text + "OFF"

b = Bulb(40)
print(b)
b.switch()
print(b)
print("Bulb " + str(b))
▶ניסוי חי — מעבדת המחלקותכותבים מחלקה, יוצרים עצמים ועוקבים אחר התוכנית צעד אחר צעד. המפרש של המעבדה מכיר משתנה במחלקה, ערכי ברירת מחדל, __str__, __repr__ והקידומת f. הוא שונה מפייתון בכך שעצם בלי __str__ מודפס בשם המחלקה בלבד, בלי כתובת. אין בו קלט מן המשתמש, ולכן כותבים את הערך ישירות במקום input. בדף המעבדה כתוב "אובייקט" ו"בנאי" במקום עצם ופעולה בונה.
⚠
המעבדה אינה זמינה כרגע. החומר בפרק שלם גם בלעדיה: ממשיכים לקרוא, וחוזרים לניסוי מאוחר יותר או פותחים אותו ישירות מן הקישור שמתחת למסגרת.
https://fahed-lab.com/electronics/python_basics#classes
פותחים את המעבדה בדף נפרד ↗ fahed-lab.com/electronics/python_basics#classes
פרויקט הספר: תחנת המדידה · שלב 8 — מחלקה לחיישן

בפרק 2 חישבנו את מתח הסוללה ואת הטמפרטורה בשתי שורות דומות, ולכל אחת המשתנים שלה. עכשיו נראה מה משותף: לכל חיישן יש שם, יחידה וקריאה גולמית מן הממיר, ויש לו מקדם scale שהופך את המתח בכניסת הממיר לערך האמיתי: 4 למחלק המתח שעל הסוללה, ו-100 לחיישן הטמפרטורה שנותן 10 mV לכל מעלה. הצעד step שייך לממיר עצמו, והממיר אחד לכל החיישנים, ולכן הוא משתנה במחלקה.

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):
        volts = self.raw * Sensor.step
        return round(volts * self.scale, 2)

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

battery = Sensor("Battery", "V", 4)
temp = Sensor("Temp", "C", 100)
print(battery)
battery.read(int(input("ADC battery: ")))
temp.read(int(input("ADC temp: ")))
print(battery)
print(temp)
print(battery.value() > 11.5)
פלט
Battery: 0.0 V
ADC battery: 620
ADC temp: 82
Battery: 12.12 V
Temp: 40.08 C
True

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

נסו בעצמכם: בלוח ESP32 הממיר הוא בן 12 סיביות והמתח שלו 3.3 V. איזו שורה אחת צריך להוסיף כדי שכל החיישנים יעבדו עם הממיר הזה? ובפרק הבא החיישנים האלה ייכנסו אל מחלקת התחנה עצמה.

עיקרי הפרק:

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

פקודהמה היא עושה
class Led:כותרת המחלקה, ומתחתיה גופה בהזחה
max_current = 0.02בגוף המחלקה: משתנה במחלקה
def __init__(self, a, b=1):הפעולה הבונה, ו-b פרמטר אופציונלי
self.x = aתכונה, כלומר משתנה במופע
led = Led(2.0)יצירת עצם
led.x Led.max_currentגישה לתכונה, ולמשתנה במחלקה
led.f() led.f(3) led.f(n=3)זימון פעולה, ו-n יכול לקבל ערך ברירת מחדל
def __str__(self):מה ש-print מדפיס לעצם: מחזירה מחרוזת
def __repr__(self):תיאור העצם למתכנת ובתוך רשימות
f"{self.x} V"מחרוזת עם ביטויים בסוגריים מסולסלים

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

עבריתEnglishערבית
מחלקהClassالفئة
עצםObjectالكائن
מופעInstanceالنسخة
תכונהAttributeالصفة
פעולהMethodالعمليّة
כותרת המחלקהClass headerرأس الفئة
משתנה במחלקהClass variableمتغيّر الفئة
משתנה במופעInstance variableمتغيّر النسخة
פעולה בונהConstructorعمليّة البناء
פרמטר אופציונליOptional parameterوسيط اختياري
ערך ברירת מחדלDefault valueقيمة افتراضية
העמסהOverloadingالتحميل الزائد
התשובות הסופיות לשאלות הפרק
8.1 (א) str מחלקה ו-"LED" עצם , (ב) Turtle מחלקה ו-a עצם , (ג) הראשון מחלקה והשני עצם.
8.2 5.0 , 4.0 , 49
8.3 120 lab , 12 store , 120
8.4 9 5 , 60 2
8.5 24 0 , 12 0 , 24 0 , 48 900
8.6 30 35
8.7 5 5 False , 10 0 True , 60
8.8 40 W: OFF , 40 W: ON , Bulb 40 W: ON

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

רמה ראשונה — בסיסי
1.
מה מדפיסה התוכנית?
class Fan:
    speed = 1200

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

a = Fan("A")
b = Fan("B")
b.speed = 900
Fan.speed = 1500
print(a.speed, b.speed, Fan.speed)
תשובות: 1500 900 1500
2.
במחלקה הזו שתי שגיאות שמונעות ממנה לעבוד. מצאו ותקנו אותן.
class Lamp:
    def __init__(self, watts):
        watts = watts

    def energy(hours):
        return self.watts * hours / 1000

desk = Lamp(9)
print(desk.energy(100))
תשובות: חסר self.watts = בפעולה הבונה, וחסר הפרמטר self בהגדרת energy:
class Lamp:
    def __init__(self, watts):
        self.watts = watts

    def energy(self, hours):
        return self.watts * hours / 1000

desk = Lamp(9)
print(desk.energy(100))
מדפיסה: 0.9
3.
כתבו מחלקה Fuse עם התכונה rating והפעולה blows(amps), שמחזירה True אם הזרם גדול מערך הנתיך.
תשובות:
class Fuse:
    def __init__(self, rating):
        self.rating = rating

    def blows(self, amps):
        return amps > self.rating

f = Fuse(16)
print(f.blows(16), f.blows(20))
מדפיסה: False True
4.
כתבו מחלקה Switch למתג שמתחיל פתוח, עם הפעולה toggle שהופכת את מצבו, ו-__str__ שמחזירה את שמו ואת מצבו.
תשובות:
class Switch:
    def __init__(self, name):
        self.name = name
        self.on = False

    def toggle(self):
        self.on = not self.on

    def __str__(self):
        if self.on:
            return self.name + ": ON"
        return self.name + ": OFF"

s = Switch("S1")
print(s)
s.toggle()
print(s)
מדפיסה: S1: OFF , S1: ON
רמה שנייה — בינוני
5.
כתבו מחלקה Capacitor עם קיבול ומתח שערך ברירת המחדל שלו 12 V, עם הפעולה charge, Q = C · V, הפעולה energy, E = C · V2 2, והפעולה __str__.
תשובות:
class Capacitor:
    def __init__(self, c, v=12):
        self.c = c
        self.v = v

    def charge(self):
        return self.c * self.v

    def energy(self):
        e = self.c * self.v ** 2 / 2
        return round(e, 4)

    def __str__(self):
        text = str(self.c) + " F, "
        return text + str(self.v) + " V"

c = Capacitor(0.001)
print(c)
print(c.charge(), c.energy())
d = Capacitor(0.002, 5)
print(d)
print(d.charge(), d.energy())
מדפיסה: 0.001 F, 12 V , 0.012 0.072 , 0.002 F, 5 V , 0.01 0.025
6.
כתבו מחלקה PulseCounter למונה פולסים: מתחילה מאפס, עם pulse שמוסיפה n פולסים וערך ברירת המחדל שלה 1, ועם reset.
תשובות:
class PulseCounter:
    def __init__(self):
        self.count = 0

    def pulse(self, n=1):
        self.count = self.count + n

    def reset(self):
        self.count = 0

c = PulseCounter()
c.pulse()
c.pulse()
c.pulse(5)
print(c.count)
c.reset()
c.pulse(2)
print(c.count)
מדפיסה: 7 , 2
7.
כתבו מחלקה Part לנגד שערכו ידוע ויש לו סבילות באחוזים, שערך ברירת המחדל שלה 5. בה low ו-high שנותנות את גבולות התחום, ו-accepts שבודקת ערך מדוד.
תשובות:
class Part:
    def __init__(self, ohms, tol=5):
        self.ohms = ohms
        self.tol = tol

    def low(self):
        d = self.ohms * self.tol / 100
        return self.ohms - d

    def high(self):
        d = self.ohms * self.tol / 100
        return self.ohms + d

    def accepts(self, x):
        return self.low() <= x <= self.high()

r = Part(1000)
print(r.low(), r.high())
print(r.accepts(1040), r.accepts(1060))
s = Part(1000, 1)
print(s.accepts(1040))
מדפיסה: 950.0 1050.0 , True False , False
8.
נרצה שכל לוח שמיוצר יקבל מספר סידורי משלו, החל מ-100. כתבו מחלקה Board שעושה זאת בלי להעביר את המספר ביצירה.
תשובות:
class Board:
    next_id = 100

    def __init__(self, model):
        self.model = model
        self.serial = Board.next_id
        Board.next_id = Board.next_id + 1

    def __str__(self):
        text = str(self.serial)
        return self.model + "-" + text

a = Board("ESP")
b = Board("UNO")
c = Board("ESP")
print(a, b, c)
print(Board.next_id)
מדפיסה: ESP-100 UNO-101 ESP-102 , 103. המספר הבא שייך לכל המחלקה, ולכן הוא משתנה במחלקה, והמספר של כל לוח הוא משתנה במופע.
9.
נתונה רשימת נוריות, ולכל נורית צבע, מתח וזרם. כתבו מחלקה, ותוכנית שמדפיסה את צבע הנורית בעלת ההספק הגבוה ביותר. שימו את העצמים ברשימה כמו בסעיף 8.8, וחפשו בה ערך קיצון כמו בסעיף 6.12.
תשובות:
class Led:
    def __init__(self, color, volts, amps):
        self.color = color
        self.volts = volts
        self.amps = amps

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

leds = [Led("red", 2.0, 0.02),
        Led("blue", 3.2, 0.02),
        Led("white", 3.0, 0.35)]
best = leds[0]
for led in leds:
    if led.power() > best.power():
        best = led
print(best.color, round(best.power(), 2))
מדפיסה: white 1.05
רמה שלישית — אתגר
10.
כתבו מחלקה Uptime ששומרת את זמן הפעולה של התחנה בשניות, עם tick(n=1), ועם __str__ שמחזירה את הזמן בצורה hh:mm:ss. פעולת עזר משלימה אפס לפני מספר בן ספרה אחת.
תשובות:
class Uptime:
    def __init__(self, seconds=0):
        self.seconds = seconds

    def tick(self, n=1):
        self.seconds = self.seconds + n

    def two(self, n):
        if n < 10:
            return "0" + str(n)
        return str(n)

    def __str__(self):
        h = self.seconds // 3600
        m = self.seconds % 3600 // 60
        s = self.seconds % 60
        text = self.two(h) + ":"
        text = text + self.two(m) + ":"
        return text + self.two(s)

t = Uptime(3599)
print(t)
t.tick()
print(t)
t.tick(7384)
print(t)
מדפיסה: 00:59:59 , 01:00:00 , 03:03:04
11.
כתבו מחלקה Signal ששומרת רשימת דגימות של אות, עם add, mean, peak ו-__str__. כל עצם צריך לקבל רשימה משלו.
תשובות:
class Signal:
    def __init__(self, name):
        self.name = name
        self.data = []

    def add(self, value):
        self.data.append(value)

    def mean(self):
        n = len(self.data)
        return sum(self.data) / n

    def peak(self):
        return max(self.data)

    def __str__(self):
        n = str(len(self.data))
        text = self.name + ": " + n
        return text + " samples"

s = Signal("A0")
for x in [2, 4, 9, 5]:
    s.add(x)
print(s)
print(s.mean(), s.peak())
מדפיסה: A0: 4 samples , 5.0 9
12.
כתבו מחלקה Reg8 לאוגר בן 8 סיביות: is_set(bit), set_bit(bit) ו-clear_bit(bit) בחשבון בלבד, ו-__str__ שנותנת את שמונה הסיביות מן הגבוהה לנמוכה.
תשובות:
class Reg8:
    def __init__(self, value=0):
        self.value = value

    def is_set(self, bit):
        v = self.value // 2 ** bit
        return v % 2 == 1

    def set_bit(self, bit):
        if not self.is_set(bit):
            self.value += 2 ** bit

    def clear_bit(self, bit):
        if self.is_set(bit):
            self.value -= 2 ** bit

    def __str__(self):
        text = ""
        for bit in range(7, -1, -1):
            if self.is_set(bit):
                text = text + "1"
            else:
                text = text + "0"
        return text

r = Reg8()
r.set_bit(0)
r.set_bit(3)
r.set_bit(3)
print(r, r.value)
r.clear_bit(0)
r.clear_bit(5)
print(r, r.value)
מדפיסה: 00001001 9 , 00001000 8. בודקים לפני שמוסיפים או מחסרים, ולכן שום דבר לא השתנה כשחזרנו על סיבית 3, וגם לא כשניקינו את סיבית 5.

שאלות בגרות לפרק 8

ארבע שאלות המחלקות הבאות הן מבחינת הבגרות. שמות כמו addNum ו-DoSomething הם השמות שבשאלון, ואנחנו משאירים אותם כפי שהם גם אם אינם בסגנון של הספר.
בגרות 2023 שאלה 11 — ממוצע חשבוני
בגרות 2023
סעיףהמבוקשתשובה
(א)פעולה בונה ללא פרמטרים שמאתחלת את שתי התכונות לאפס, ופעולה display שמדפיסה את ערכיהן.למטה
(ב)מה מדפיס קטע הקוד הנתון?Average is 0.0
(ג)שנו את addNum כך שרק ציונים מ-0 עד 100 ייכנסו לחישוב.למטה
הפתרון המלא: fahed-lab.com/exams/electronics-815381/815381/solutions/solution-2023

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

class Average:
    def __init__(self):
        self.sum = 0
        self.count = 0

    def addNum(self, num):
        self.sum = self.sum + num
        self.count = self.count + 1

    def calcAverage(self):
        return self.sum / self.count

    def display(self):
        print(self.sum, self.count)

arr = [90, 100, -40, -60, -90]
a = Average()
for item in arr:
    a.addNum(item)
x = a.calcAverage()
print("Average is", x)
a.display()
פלט
Average is 0.0
0 5

(ג) נעטוף את שתי השורות בתנאי, וכך מספר מחוץ לתחום לא נכנס לסכום ולא לכמות. נשנה רק את הפעולה addNum:

def addNum(self, num):
    if 0 <= num <= 100:
        self.sum = self.sum + num
        self.count = self.count + 1

עכשיו עוברים רק המספרים 90 ו-100, ואותו קטע מדפיס: Average is 95.0 , 190 2.

הערה: אם אף ציון תקין לא נכנס, count נשאר אפס, והחילוק גורם לשגיאה. השאלה לא דורשת לטפל במקרה הזה, אבל כדאי לשים לב אליו.
בגרות 2024 שאלה 11 — רשימת מספרים בהפרש קבוע
בגרות 2024
בגרות 2024
סעיףהמבוקשתשובה
(א) 1מה מדפיס קטע הקוד?False , True , False , True , False
(א) 2במשפט אחד: מה מבצעת DoSomething?בודקת אם המספר נופל על אחד מצעדי הרשימה, כלומר אם ההפרש בינו לבין start הוא כפולה של step.
(ב)הפעולה printRange מדפיסה את איברי הרשימה.למטה
(ג)הפעולה getNumberByIndex מחזירה את האיבר שבאינדקס נתון.למטה
הפתרון המלא: fahed-lab.com/exams/electronics-815381/815381/solutions/solution-2024

(א) הרשימה כאן היא 10 , 14 , 18. נריץ את הקטע, והוא מדפיס True עבור שני המספרים שנמצאים בה מבין אלה שבדקנו:

class ListClass:
    def __init__(self, start, end, step):
        self.start = start
        self.end = end
        self.step = step

    def DoSomething(self, number):
        if (number - self.start) % self.step != 0:
            return False
        return True

lst = ListClass(10, 21, 4)
print(lst.DoSomething(11))
print(lst.DoSomething(14))
print(lst.DoSomething(16))
print(lst.DoSomething(18))
print(lst.DoSomething(21))
פלט
False
True
False
True
False
הערה: הפעולה לא בודקת גבולות: המספרים 6 ו-22 מחוץ לרשימה, אבל ההפרש שלהם מ-start הוא כפולה של step, ולכן היא נותנת להם True: True True. בדיקה מלאה דורשת גם את התנאי self.start <= number <= self.end. ובנוסח השאלה כתוב steep, ובקוד step, והנכון הוא מה שבקוד.

(ב) נתחיל מ-start, ונוסיף בכל סבב step, כל עוד לא עברנו את end. המספר 12 לא מופיע בדוגמה, כי הוא רק האיבר האחרון האפשרי, והצעד קופץ מ-11 ל-13. נדפיס עם הפרמטר end=" " כדי שהמספרים יישארו בשורה אחת. אפשר גם לכתוב את הלולאה for number in range(self.start, self.end + 1, self.step). (ג) לא צריך לולאה: האיבר שהאינדקס שלו index מרוחק index צעדים מן ההתחלה, והאינדקס של האיבר הראשון הוא 0:

def printRange(self):
    number = self.start
    while number <= self.end:
        print(number, end=" ")
        number = number + self.step
    print()

def getNumberByIndex(self, index):
    return self.start + index * self.step

עבור העצם ListClass(5, 12, 2) שתי הדוגמאות שבשאלה מדפיסות: 5 7 9 11 , 7.

בגרות 2025 שאלה 10 — מלבן
בגרות 2025
בגרות 2025
סעיףהמבוקשתשובה
(א)מה מדפיס קטע הקוד?למטה
(ב)הפעולה IsSquare מחזירה True אם המלבן הוא ריבוע, ואחרת False.self.length == self.width
(ג)ניצור שני מלבנים, 25 על 17 ו-25 על 25, ונבדוק כל אחד מהם.False , True
הפתרון המלא: fahed-lab.com/exams/electronics-815381/815381/solutions/solution-2025

(א) הפעולה הבונה בודקת את הערכים, כמו בסעיף 8.5. המספר −1 שלילי, ולכן מתבצע ענף else, והאורך והרוחב הופכים ל-2. הפעולה DoSomething מחשבת למעשה את שטח המלבן. אחר כך הלולאה עוברת על הערכים −1 , 0 , 1 , 2, וכולם משאירים את האורך חיובי, ולכן כל שינוי מתקבל. בהצגת התוצאה משתמשים בקידומת f מסעיף 8.8:

class Rectangle:
    def __init__(self, length, width):
        if length >= 0 and width >= 0:
            self.length = length
            self.width = width
        else:
            self.length = 2
            self.width = 2

    def AddToDimensions(self, lengthToAdd, widthToAdd):
        if (self.length + lengthToAdd > 0
                and self.width + widthToAdd > 0):
            self.length = self.length + lengthToAdd
            self.width = self.width + widthToAdd

    def DoSomething(self):
        return self.length * self.width

    def print_details(self):
        print(f"L={self.length} W={self.width}", end=" ")
        print(f"D1={self.DoSomething()}")

rect = Rectangle(-1, 1)
rect.print_details()

for i in range(-1, 3):
    rect.AddToDimensions(i, 0)
    rect.print_details()
פלט
L=2 W=2 D1=4
L=1 W=2 D1=2
L=1 W=2 D1=2
L=2 W=2 D1=4
L=4 W=2 D1=8

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

(ב) ההשוואה עצמה נותנת את הערך הנדרש, ולכן אין צורך ב-if. בשאלה כתוב true ו-false, ובפייתון שני הערכים נכתבים באות גדולה. (ג) ניצור את שני העצמים, ונזמן את הפעולה על כל אחד:

def IsSquare(self):
    return self.length == self.width
rect1 = Rectangle(25, 17)
rect2 = Rectangle(25, 25)
print(rect1.IsSquare())
print(rect2.IsSquare())

התוכנית מדפיסה: False , True.

בגרות 2026 שאלה 10 — כרטיס נסיעה
בגרות 2026
סעיףהמבוקשתשובה
(א)מה מדפיס קטע הקוד?למטה
(ב)הדפיסו את שם בעל הכרטיס עם היתרה הגבוהה מבין שני כרטיסים.פתרנו בסוף פרק 3
(ג)הוסיפו תכונה שסופרת נסיעות שבוצעו בהצלחה, ועדכנו את pay_for_trip ואת get_info.למטה
הפתרון המלא: fahed-lab.com/exams/electronics-815381/815381/solutions/solution-2026

(א) היתרה 55.5, והיא נעשית 75.5 אחרי הטעינה. התשלום הראשון מצליח, ונשאר 35.5. השני נכשל כי היתרה קטנה מ-40, ולכן חוזר False והיתרה לא משתנה. חילקנו את השורה של get_info כי היא ארוכה, והתוצאה אותה תוצאה:

class TravelCard:
    def __init__(self, owner_name, balance):
        self.owner_name = owner_name
        self.balance = balance

    def top_up(self, amount):
        if amount > 0:
            self.balance = self.balance + amount

    def pay_for_trip(self, cost):
        if self.balance >= cost:
            self.balance = self.balance - cost
            return True
        return False

    def get_info(self):
        text = self.owner_name + " has "
        text = text + str(self.balance)
        return text + " shekels left."

card = TravelCard("Guest", 55.5)
card.top_up(20)
print("Pay 40:", card.pay_for_trip(40))
print("Pay 40:", card.pay_for_trip(40))
print(card.get_info())
פלט
Pay 40: True
Pay 40: False
Guest has 35.5 shekels left.

(ב) את התוכנית הזו כתבנו בסוף פרק 3 במשתנים נפרדים. עכשיו ניצור שני עצמים c1 ו-c2, ונכתוב c1.balance במקום balance1, ו-c1.owner_name במקום name1, והתנאי נשאר כפי שהוא.

(ג) שלושה שינויים: התכונה החדשה trips מתחילה מאפס בפעולה הבונה, גדלה בתוך התנאי כי השאלה סופרת רק נסיעות ששולמו בהצלחה, ו-get_info מוסיפה אותה למחרוזת. נכתוב כאן רק את מה שהשתנה:

class TravelCard:
    def __init__(self, owner_name, balance):
        self.owner_name = owner_name
        self.balance = balance
        self.trips = 0

    def pay_for_trip(self, cost):
        if self.balance >= cost:
            self.balance = self.balance - cost
            self.trips = self.trips + 1
            return True
        return False

    def get_info(self):
        text = self.owner_name + " has "
        text = text + str(self.balance)
        text = text + " shekels left. Trips: "
        return text + str(self.trips)

אותו קטע מדפיס בשורה האחרונה: Guest has 35.5 shekels left. Trips: 1. נסיעה אחת, אף שניסינו פעמיים.