תחביר מחלקה בפייתון#

טקסט המופיע למטה בסגול מציין קטעים המופיעים בסרטון

ניתן להיעזר בו כדי לחזור על התכנים או לעיין בהם שוב.

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

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

class ClassName:
    """documentation string"""

    def __init__(self):
        self.field1 = 0
        self.field2 = "value"

    def method_name1(self, arg1, arg2):
        # method code 
        
    def method_name2(self, arg1):
        # method code
  Cell In[2], line 11
    def method_name2(self, arg1):
    ^
IndentationError: expected an indented block after function definition on line 8

חשוב לדעת: עוד על self

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

הופעתו של self בתחילת כל מתודה איננה סתם מוסכמה, אלא מנגנון שמאפשר לנו להבדיל בין השדות וההתנהגויות של מופעים שונים שנוצרו מאותה מחלקה. כך למשל, אם ניצור שני אובייקטים ממחלקה “סטודנט”, לכל אחד מהם יהיו ערכים שונים לשדות כמו name או grade. השימוש ב־self.name או self.grade מבטיח שכל אובייקט ישמור על המידע הייחודי שלו, גם אם שניהם חולקים את אותה מתודה.

דוגמא#

כעת נממש מחלקה פשוטה המייצגת נקודה דו-מימדית:

כדי לייצג את היישות “נקודה דו מימדית”, נבנה טיפוס נתונים חדש, או מחלקה חדשה, בשם Point.
מקובל שבפייתון שמות של מחלקות מתחילים באות גדולה.

class Point:
    """represents a point in a 2D space.
    Attributes: x, y """

כעת נאתחל את שדות המחלקה.
זכרו כי השדות מאותחלים בד”כ בבנאי (__init__):

class ClassName:
    """documentation string"""

    def __init__(self):
        self.field1 = 0   # <-- A new field initialization
        self.field2 = "value" # <-- A new field initialization

שימו לב!

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

אילו שדות נצטרך לאובייקט הנקודה הדו-מימדית שלנו?

נקודה על מרחב דו מימדי יכולה להיות מוגדרת על ידי מיקומה בצירים x ו-y.
לכן, נאתחל בהתאמה שני שדות x וy בבנאי.

class Point:
    """represents a point in a 2D space.
    Attributes: x, y """

    def __init__(self, x, y):
        self.x = x
        self.y = y

שימו לב

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

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

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

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

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

p1 = Point(3.0, 4.0)
print(p1.x)
print(p1.y)
3.0
4.0

אז מה קרה כאן בעצם?

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

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

שימו לב

בקטע הקוד לעיל קיים הבדל מהותי בין המשתנים שמועברים כפרמטרים לבנאי (x, y) לבין השדות self.x ו-self.y. הפרמטרים הפורמליים x ו-y קיימים רק בזמן הקריאה לבנאי – הם לוקליים לבנאי, ולא ניתן לגשת אליהם מחוץ לו, אפילו במתודות אחרות של האובייקט. לעומת זאת, כאשר אנו כותבים self.x = x ו-self.y = y, אנחנו יוצרים שדות קבועים בתוך האובייקט הנוכחי. השדות האלה ילוו את האובייקט לכל אורך חייו, ויהיו נגישים בכל מתודה אחרת שנקרא על אותו מופע דרך self.

p1 = Point(3.0, 4.0)
x = p1.y
print(x)
print(p1.x)
4.0
3.0

מתודות#

כעת נתמקד בהגדרה ושימוש במתודות חדשות במחלקה.

ניתן להגדיר מתודה חדשה באמצעות המילה def בתוך ההזחה של המחלקה.

class ClassName:
    """documentation string"""

    def __init__(self):
        self.field1 = 0
        self.field2 = "value"

    def method_name1(self, arg1, arg2): #  <-- Definition of a new method
        # method code 
        
    def method_name2(self, arg1):
        # method code

תזכורת

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

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

זכרו כי ניתן להשתמש בself רק בתוך המחלקה, גישה לשדה או מתודה של אובייקט מחוץ למחלקה נעשית ישירות מתוך האובייקט (המופע של המחלקה).

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

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def get_str_point(self):             # New method
        return f'<{self.x},{self.y}>'
        
p1 = Point(3.0, 4.0)
print(p1.get_str_point())
<3.0,4.0>

נסו בעצמכם#

ממשו את המתודה distance של המחלקה Point. אשר מחזירה את המרחק בין הנקודה ממנה הופעלה המתודה לבין נקודה נוספת המועברת בקלט המתודה (other).

Calculate the distance between between one point to another
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def get_str_point(self):
        return f'<{self.x},{self.y}>'

    def distance(self, other):        
        return ((self.x - other.x)**2 + (self.y - other.y)**2)**0.5
p1 = Point(2,-3)
p2 = Point(4,5)
print(p1.distance(p2)) # Should print 8.246..

הערה

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

ניצור כעת פונקציה חדשה (לא מתודה) המזיזה נקודה point (מסוג Point) בdx לאורך ציר ה-x וב-dy לאורך ציר הy:

def move_point(point, dx, dy):
    point.x += dx
    point.y += dy
p1 = Point(3.0, 4.0)
print(p1.get_str_point())
move_point(p1, 2, -1)
print(p1.get_str_point())

הפונקציה הזו איננה מתודה, כלומר לא משויכת לאובייקט ואינה חלק מהמחלקה. לכן, במקום להשתמש בself, נעביר את האובייקט עצמו לפונקציה (point), וניגש ממנו לשדות x ו-y.

שימו לב

השימוש במילה self נעשה אך ורק בתוך מחלקה, כאשר פונים אל האובייקט שאותו יוצרים (בבנאי) או האובייקט עליו מופעלת המתודה (במתודות אחרות).
מחוץ למחלקה לעולם לא נשתמש בself, אלא נפנה לאובייקט הקונקרטי ישירות (דרך המשתנה המכיל אותו).

נסו בעצמכם#

כתבו את הפונקציה middle_point(p1,p2) המקבלת שני אובייקטים מסוג Point ומחזירה את הנקודה האמצעית בין שתי נקודות - גם היא מסוג Point.

def middle_point(p1, p2):
    # Write you code here
    pass

p1 = Point(1,1)
p2 = Point(3,5)
p3 = middle_point(p1,p2)
print(p3.get_str_point()) # printed value be <2.0,3.0> 

דוגמא מסכמת: ייצוג מעגל בפייתון#

טקסט המופיע למטה בסגול מציין קטעים המופיעים בסרטון

ניתן להיעזר בו כדי לחזור על התכנים או לעיין בהם שוב.

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

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

  • center - מטיפוס המחלקה Point שיצרנו למעלה.

  • radius - מטיפוס float.

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

שם מתודה

תיאור

קלט

ערך החזר

__init__

הבנאי

center (Point), radius (int/float)

-

print_circle

הדפסת ייצוג המעגל (מרכז המעגל והרדיוס) למסך

-

-

in_circle

בדיקה האם הנקודה נופלת בתוך המעגל

אובייקט מטיפוס Point

True / False

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

מימוש המחלקה Circle בפייתון:#

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

class Circle:
    """ represents a circle shape
    attributes: center, radius"""
    def __init__(self, center, radius):
        self.center = center
        self.radius = radius

    def print_circle(self):
        print('center:', self.center.x, self.center.y, \
              ', radius:', self.radius)

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

c = Circle(Point(1,2), 1)
c.print_circle()
center: 1 2 , radius: 1

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

class Circle:
    def __init__(self, center, radius):
        self.center = center
        self.radius = radius

    def print_circle(self):
        print('center:', self.center.x, self.center.y, \
              ', radius:', self.radius)

### new code here ###
    def in_circle(self, p):
        return self.center.distance(p) < self.radius
#####################
c = Circle(Point(1,1), 2)
print(c.in_circle(Point(0,0)))
print(c.in_circle(Point(2,3)))    

תרגול: בניית מלבן#

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

המחלקה תיקרא Rectangle ותייצג מלבן במישור דו־ממדי. למחלקה יהיו השדות הבאים:

  • width - מספר שלם או עשרוני (int או float) המתאר את רוחב המלבן.

  • height -מספר שלם או עשרוני (int או float) המתאר את גובה המלבן.

  • corner - אובייקט מטיפוס Point המייצג את הפינה השמאלית־עליונה של המלבן.

הניחו שהמלבן מקביל לציר ה-x, כלומר צלעותיו מונחות במקביל לצירים של מערכת הצירים.

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

  • __init__(self, width, height, corner) - בנאי שמקבל את רוחב המלבן, גובהו ואת הפינה השמאלית־עליונה (אובייקט מסוג Point) ומאתחל את השדות המתאימים.

  • get_str(self) - מתודה המחזירה מחרוזת המכילה את פרטי המלבן (רוחב, גובה ומיקום הפינה השמאלית־עליונה).

  • get_center(self) - מתודה שמחזירה אובייקט מטיפוס Point המייצג את מרכז המלבן.

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

class Rectangle:
    """represents a rectangle in a 2D space.
    attributes: width, height, lower-left corner"""
    def __init__(self, width, height, corner):
        # Write you code here
        pass

    def get_str(self):
        # Write you code here
        pass

    def get_center(self):
        # Write you code here
        pass
rect = Rectangle(100, 200, Point(0,0))
print(rect.get_str())
print(rect.get_center())
rect = Rectangle(100, 200, Point(0,0))
center = rect.get_center()
print(center.get_str_point())