בשיעור הקודם למדנו על ירושה – מנגנון תכנותי שמאפשר יצירת מחלקה על בסיס תכונותיה ופעולותיה של מחלקה אחרת.
סקרנו באילו מקרים נכון לבצע ירושה, ודיברנו על חילוקי הדעות ועל הסיבוכים האפשריים שירושה עלולה לגרום.
חקרנו את היכולת של תת־מחלקה להגדיר מחדש פעולות של מחלקת־העל וקראנו לכך "דריסה" של פעולה.
דיברנו גם על הפונקציה super שמאפשרת לנו לקרוא בקלות לפעולות במחלקת־העל.
ירושה היא כר פורה לשיח בין תיאורטיקנים של מדעי המחשב.
נכתבו עליה מילים רבות, והיא נושא מרכזי בדיונים על הנדסת תוכנה.
במחברת זו נעמיק ונסקור שימושים וטכניקות שהתפתחו מתוך רעיון הירושה במחלקות.
הרעיון הבסיסי ביותר בירושה הוא העובדה שיכולה להיות יותר מרמה אחת של ירושה.
קרי: אם מחלקה ב ירשה ממחלקה א, מחלקה ג יכולה לרשת ממחלקה ב.
בקטע הקוד הבא, לדוגמה, המחלקה יונק (Mammal) יורשת מהמחלקה "חיה" (Animal).
המחלקות עטלף (Bat) וארנב (Rabbit) יורשות מהמחלקה "יונק".
In [ ]:
class Animal:
pass
class Mammal(Animal):
pass
class Bat(Mammal):
pass
class Rabbit(Mammal):
pass
במקרה כזה, המחלקות Bat ו־Rabbit ירשו הן את התכונות והפעולות של המחלקה Mammal, והן את אלו של Animal.
אפשר לקבל את שרשרת מחלקות־העל של מחלקה מסוימת לפי סדרן בעזרת class_name.mro()
:
In [ ]:
Bat.mro()
בפנייה לפעולה כלשהי, תחפש פייתון את הפעולה במחלקה הראשונה שמופיעה ב־MRO.
אם הפעולה לא מופיעה שם, תיגש פייתון למחלקה שאחריה, כך עד שהיא תגיע ל־object שתמיד יהיה בראש השרשרת.
אם הפעולה לא קיימת באף אחת מהמחלקות שמופיעות ב־MRO, פייתון תזרוק NameError.
מבחינה הנדסית, מומלץ להימנע ככל האפשר מירושה מרובת רמות כשאין בכך צורך ממשי.
ירושה מרובת רמות תגדיל את הסיכוי לתסמונת מחלקת־העל השברירית, תקשה על בדיקת התוכנית ותיצור בעיות תחזוקה בעתיד.
כל חיה משמיעה צליל האופייני לה: כלב נובח, פרה גועה ויונה הומה.
נממש מחלקה עבור כל חיה:
In [ ]:
class Dog:
def __init__(self, name, gender):
self.name = name
self.gender = gender
def make_sound(self):
print("Woof")
def __str__(self):
return f"I'm {self.name} the dog!"
class Cow:
def __init__(self, name, gender):
self.name = name
self.gender = gender
def make_sound(self):
print("Muuuuuuuuuu")
def __str__(self):
return f"I'm {self.name} the cow!"
class Dove:
def __init__(self, name, gender):
self.name = name
self.gender = gender
def make_sound(self):
print("Kukukuku!")
def __str__(self):
return f"I'm {self.name} the dove!"
print(Dove("Rexi", "Female"))
נשים לב שצורת כל המחלקות דומה – מקרה קלאסי לירושה:
In [ ]:
class Animal:
def __init__(self, name, gender):
self.name = name
self.gender = gender
class Dog(Animal):
def make_sound(self):
print("Woof")
def __str__(self):
return f"I'm {self.name} the dog!"
class Cow(Animal):
def make_sound(self):
print("Muuuuuuuuuu")
def __str__(self):
return f"I'm {self.name} the cow!"
class Dove(Animal):
def make_sound(self):
print("Kukukuku!")
def __str__(self):
return f"I'm {self.name} the dove!"
print(Dove("Rexi", "Female"))
לעיתים קרובות אנחנו רוצים לממש קבוצת מחלקות שיש להן את אותן תכונות ופעולות – בדיוק כמו במקרה של Dog, Cow ו־Dove.
במקרה כזה נפנה באופן טבעי לירושה, שבה מחלקת־העל תכיל את התכונות והפעולות המשותפות לכל המחלקות.
החיסרון במחלקת Animal הוא שכעת אפשר ליצור ישירות ממנה מופעים.
זה לא מה שהתכוונו שיקרה. המחלקה הזו קיימת רק כדי לייצג רעיון מופשט של חיה, ולאפשר ירושת תכונות ופעולות ממקור אחד.
מטרת התוכנית היא לאפשר לבעלי חיים להשמיע קול, ולכן אין משמעות ביצירת מופע מהמחלקה Animal – שהרי ל"חיה" כרעיון מופשט אין קול.
חיסרון נוסף הוא שמחלקות יכולות לרשת ממחלקת Animal מבלי לממש את הפעולה make_sound.
אם היינו מתכנתים פיסת קוד כללית שמטפלת בחיות, ייתכן שזה היה תקין, אבל לא זה המקרה בקוד שלמעלה.
מטרת התוכנה שלנו הייתה מלכתחילה לייצג קולות של חיות, ומחלקה שיורשת מ־Animal ולא מממשת את make_sound היא פתח לבאגים בעתיד.
הפתרון לשתי הבעיות שהוצגו כאן נקרא בהנדסת תוכנה "מחלקה מופשטת" (Abstract Class).
זהו מצב שבו אנחנו משתמשים במחלקת־על ל־3 צרכים:
נהוג לכתוב במחלקת־העל המופשטת את כותרות הפעולות שעל תת־המחלקות שיורשות ממנה לממש:
In [ ]:
class Animal:
def __init__(self, name, gender):
self.name = name
self.gender = gender
def make_sound(self):
pass
def __str__(self):
pass
class Dog(Animal):
def make_sound(self):
print("Woof")
def __str__(self):
return f"I'm {self.name} the dog!"
class Cow(Animal):
def make_sound(self):
print("Muuuuuuuuuu")
def __str__(self):
return f"I'm {self.name} the cow!"
class Dove(Animal):
def make_sound(self):
print("Kukukuku!")
def __str__(self):
return f"I'm {self.name} the dove!"
print(Dove("Rexi", "Female"))
הרעיון של מחלקה מופשטת כה נפוץ, שהמודול הפייתוני abc (קיצור של Abstract Base Class) מאפשר לנו להגדיר מחלקה כמופשטת.
נציץ בתיעוד של המודול וננסה לעקוב אחריו, למרות כל השטרודלים המוזרים שיש שם:
@abstractmethod
.
In [ ]:
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def __init__(self, name, gender):
self.name = name
self.gender = gender
@abstractmethod
def make_sound(self):
pass
@abstractmethod
def __str__(self):
pass
class Dog(Animal):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def make_sound(self):
print("Woof")
def __str__(self):
return f"I'm {self.name} the dog!"
class Cow(Animal):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def make_sound(self):
print("Muuuuuuuuuu")
def __str__(self):
return f"I'm {self.name} the cow!"
class Dove(Animal):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def make_sound(self):
print("Kukukuku!")
def __str__(self):
return f"I'm {self.name} the dove!"
print(Dove(name="Rexi", gender="Female"))
חדי העין ישימו לב שהחזרנו את פעולת האתחול __init__
לכל תתי־המחלקות.
זה קרה כיוון שהגדרנו את __init__
כפעולה מופשטת במחלקת Animal.
להגדרה של פעולה כמופשטת שתי השלכות מיידיות:
עכשיו כשנרצה ליצור מופע מ־Animal, נגלה שזה בלתי אפשרי, כיוון ש־Animal.__init__
מוגדרת כמופשטת:
In [ ]:
Animal(name="Rexi", gender="Female")
אם ננסה ליצור תת־מחלקה שיורשת מ־Animal ולא מממשת את אחת הפעולות המופשטות שלה, נגלה שלא נוכל ליצור ממנה מופעים.
פייתון תזרוק שגיאה שאחת הפעולות המופשטות לא מוגדרת בתת־המחלקה:
In [ ]:
class Fox(Animal):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# What does the fox say?
# (there is no make_sound method)
def __str__(self):
return f"I'm {self.name} the fox!"
Fox(name="Ylvis", gender="Male")
עוד דבר מעניין שכדאי לשים לב אליו הוא השימוש הכבד ב־**kwargs
.
כיוון שהפכנו את __init__
למופשטת, אנחנו חייבים לממש אותה בכל תתי־המחלקות שיורשות מ־Animal.
למרות זאת, ה־__init__
"המעניינת" שעושה השמות לתוך תכונות המופע היא זו של מחלקת־העל Animal,
זו שמקבלת את הפרמטרים name ו־gender ומשנה לפיהם את מצב המופע.
ביצירת מופע של אחת מהחיות, נרצה להעביר את הפרמטרים name ו־gender שמיועדים ל־Animal.__init__
.
אבל ביצירת מופע של יונה, של פרה או של כלב אנחנו נקרא בפועל ל־__init__
שמימשו תתי־המחלקות.
בשלב הזה, תתי־המחלקות צריכות למצוא דרך לקבל את הפרמטרים הרלוונטיים ולהעביר אותם למחלקת־העל שעושה השמות ל־self.
כדי להעביר את כל הפרמטרים שמחלקת־העל צריכה, גם אם חתימת הפעולה Animal.__init__
תשתנה בעתיד, אנחנו משתמשים ב־**kwargs
.
השימוש במחלקות מופשטות נפוץ בתכנות כדי לאפשר הרחבה נוחה של התוכנה.
מערכות שמאפשרות למתכנתים להרחיב את יכולותיהן בעזרת תוספים, לדוגמה, ישתמשו במחלקות מופשטות כדי להגדיר למתכנת דרך להתממשק עם הקוד של התוכנה.
בחנות הפרחים של מושניק מוכרים זרי פרחים.
נמכר עם אגרטל או בלעדיו, ויש בו 3–30 פרחים מסוגים שונים: ורדים, סייפנים וסחלבים.
לכל אחד מהפרחים צבע שונה.
המחיר של סייפן הוא 4 ש"ח ושל סחלב 10 ש"ח.
המחיר של ורד נקבע לפי הצבע שלו: ורד לבן עולה 5 ש"ח, וורד אדום עולה 6 ש"ח.
עבור זר עם אגרטל, על הלקוח להוסיף 20 ש"ח.
כדי למשוך לקוחות חדשים, מדי פעם מושניק נותן הנחה על הזרים.
לכל אחת מההנחות מושניק מצרף הערה שמסבירה מה הסיבה שבגינה ניתנה ההנחה.
ישנם שני סוגים של הנחות בחנות: הנחה באחוזים והנחה שקלית.
לכל זר יכולות להיות כמה הנחות, אשר יחושבו לפי סדר צירופן.
לדוגמה, אם הזר עלה 200 ש"ח ומושניק החליט לתת הנחה של 10 אחוזים, הזר יעלה כעת 180 ש"ח.
אם מושניק החליט לתת הנחה נוספת על הזר, הפעם של 30 ש"ח, מחירו של הזר יהיה כעת 150 ש"ח.
אפשרו ללקוחות שרוצים חשבונית עבור רכישתם לקבל פירוט מודפס של חישוב המחיר של הזר.
ישר ולעניין – יש לי חדשות משוגעות עבורכם: כל מחלקה יכולה לרשת מיותר ממחלקה אחת!
ניצור מחלקה המייצגת כלי נשק, ומחלקה אחרת המייצגת כלי רכב.
מחלקת כלי הנשק תכלול את כוח כלי הנשק (strength) ופעולת תקיפה (attack).
מחלקת כלי הרכב תכלול את המהירות המרבית של כלי הרכב (max_speed) ופעולה של הערכת זמן הגעה משוער (estimate_arrival_time).
אם נרצה ליצור מחלקת טנק, לדוגמה, ייתכן שנרצה לרשת גם ממחלקת כלי הנשק וגם ממחלקת כלי הרכב.
פייתון מאפשרת לנו לעשות זאת בקלות יחסית:
In [ ]:
class Weapon:
def __init__(self, strength, **kwargs):
super().__init__(**kwargs)
self.strength = strength
def attack(self, enemy):
enemy.decrease_health_points(self.strength)
class Vehicle:
def __init__(self, max_speed, **kwargs):
super().__init__(**kwargs)
self.max_speed = max_speed
def estimate_arrival_time(self, distance):
return distance / (self.max_speed * 80 / 100)
class Tank(Weapon, Vehicle):
def __init__(self, name, **kwargs):
super().__init__(**kwargs)
self.name = name
בדוגמה שלמעלה מחלקת Tank יורשת את התכונות ואת הפעולות, הן של מחלקת Vehicle והן של מחלקת Weapon.
כל מה שהיינו צריכים לעשות זה לציין בסוגריים שאחרי המחלקה Tank את כל המחלקות שמהן אנחנו רוצים לרשת, כשהן מופרדות בפסיק זו מזו.
ניצור טנק לדוגמה, ונראה איך הוא קיבל את התכונות של כל מחלקות־העל שמהן הוא ירש:
In [ ]:
tank = Tank(name="Merkava Mk 4M Windbreaker", max_speed=64, strength=17.7)
distance = 101 # km to Eilat
# לטנק יש תכונות של טנק, של כלי נשק ושל כלי רכב
print(f"Tank name: {tank.name}")
print(f"Hours from here to Eilat: {tank.estimate_arrival_time(distance)}")
print(f"Weapon strength: {tank.strength}")
לפני שנסביר בהרחבה איך הכול עובד, נעצור רגע כדי להגיד שירושה מרובה היא נושא שיחה נפיץ.
בגלל הסיבוכיות והשכבות הנוספות שהוא מוסיף לכל מופע, תמצאו מתכנתים רבים שמתנגדים בתוקף לרעיון הירושה המרובה.
נציג בזריזות את אחת הבעיות הקלאסיות שנובעות מירושה מרובה – בעיית היהלום.
בבעיה זו מוצגת תת־מחלקה שיורשת מ־2 מחלקות־על, שהן עצמן תת־מחלקות של מחלקה נוספת.
לדוגמה: מחלקת "כפתור" יורשת ממחלקת "לחיץ" וממחלקת "מלבן", שיורשות ממחלקת "אובייקט".
אם גם "לחיץ" וגם "מלבן" מימשו את פעולת __str__
אבל מחלקת "כפתור" לא, באיזו גרסה של __str__
מחלקת "כפתור" צריכה להשתמש?
ומה בנוגע למצב שבו גם "לחיץ" וגם "אובייקט" מימשו את הפעולה?
כפי שוודאי הצלחתם להבין, ירושה מרובה יכולה להכניס אותנו להרבה "פינות" שלא מוגדרות היטב.
מהסיבה הזו פייתון החליטה לעשות מעשה, וליישר בכוח את העץ.
נביט בשרשרת הירושה של מחלקת Tank:
In [ ]:
Tank.mro()
אף על פי שטכנית היינו מצפים לראות עץ שבו Weapon ו־Vehicle נמצאות באותה רמה ומצביעות על Tank,
בפועל פייתון "משטחת" את עץ הירושה כך ש־Tank יורשת מ־Weapon שיורשת מ־Vehicle.
הסדר של המחלקות לאחר השיטוח נקבע לפי אלגוריתם שנקרא C3 Linearization, אבל זה לא משהו שבאמת חשוב לדעת בשלב הזה.
רעיון נפוץ המבוסס על ירושה מרובה הוא Mixin.
Mixin היא מחלקה שאין לה תכלית בפני עצמה, והיא קיימת כדי "לתרום" תכונות ופעולות למחלקה שתירש אותה.
לרוב נשתמש בשתי Mixins לפחות, כדי ליצור מהן מחלקות מורכבות יותר, שכוללות את הפעולות והתכונות של כל ה־Mixins.
לדוגמה: ניצור מחלקת "כפתור" (Button) שיורשת ממחלקת "מלבן גדול" (LargeRectangle) וממחלקת "לחיץ" (Clickable).
In [ ]:
class Clickable:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.clicks = 0
def click(self):
self.clicks = self.clicks + 1
class LargeRectangle:
def __init__(self, width=10, height=5, **kwargs):
super().__init__(**kwargs)
self.width = width
self.height = height
def size(self):
return self.width * self.height
class Button(Clickable, LargeRectangle):
pass
ניצור כפתור ונראה שהוא אכן קיבל את התכונות והפעולות משתי המחלקות:
In [ ]:
buy_now = Button()
buy_now.click()
print(f"Button size (from LargeRectangle class): {buy_now.size()}")
print(f"Button clicks (from Clickable class): {buy_now.clicks}")
בחלק משפות התכנות האחרות ל־Mixins יש תחביר מיוחד, אולם בפייתון משתמשים פשוט בירושה מרובה.
מהסיבה הזו, בין היתר, ההבדל בין Mixins לבין ירושה מרובה עלול להיראות מעורפל מעט.
הדגש ב־Mixins הוא שהן מוגדרות כארגז תכונות או פעולות שאפשר לרשת, והן לא מיועדות לכך שיצרו ישירות מהן מופעים.
אפשר להגיד שכל מקרה של Mixins משתמש בירושה מרובה, אך לא כל מקרה של ירושה מרובה כולל Mixins.
שחמט הוא משחק שבו שני שחקנים מתמודדים זה מול זה על לכידת מלכו של היריב.
לכל אחד מהשחקנים יש צבא שמורכב מכלי משחק, ועליהם לעשות בצבא שימוש מושכל כדי להשיג יתרון על פני השחקן השני.
כדי להבדיל בין כלי המשחק של השחקנים, כלי המשחק של שחקן אחד לבנים ואילו של השחקן השני שחורים.
ממשו לוח של משחק שחמט בגודל 8 על 8 משבצות.
בשחמט 6 סוגים של כלי משחק: רגלי, צריח, פרש, רץ, מלך ומלכה.
בפתיחת המשחק, שורות 1 ו־2 מלאות בכליו של השחקן הלבן ושורות 7 ו־8 מלאות בכליו של השחקן השחור.
הכלים מסודרים על הלוח כדלקמן:
חוקי התנועה של הכלים מפורטים להלן:
כלי בתנועה לא יכול "לדלג" מעל כלי משחק אחרים – אם כלי נמצא בדרכו, הכלי שבתנועה לא יוכל לגשת למשבצות שנמצאות מעבר לאותו כלי חוסם.
יוצא דופן לכלל זה הוא פרש – שיכול לדלג מעל כלים אחרים.
אם הכלי החוסם הוא כלי של האויב, הכלי בתנועה רשאי לעבור ולעמוד על המשבצת של הכלי החוסם (נקרא גם "להכות אותו"), ולהסיר את הכלי החוסם מהמשחק.
יוצא דופן לכלל זה הוא רגלי – הוא לא יכול להכות חייל שחוסם אותו מלפנים, והוא כן יכול להכות חייל שנמצא באלכסון הימני או השמאלי של כיוון ההתקדמות שלו.
השחמטאים שביניכם יכולים להתעלם כרגע ממהלכים "מיוחדים", כמו הכאה דרך הילוכו, הצרחה או הכתרה.
כמו כן, בינתיים נתעלם מהחוק שקובע שאם המלך מאוים, השחקן חייב לבצע מהלך שיסיר ממנו את האיום.
ממשו מחלקת Board שתכיל תכונה בשם board.
ביצירת הלוח, ייווצר לוח תקני מאויש בכלי משחק כפי שהוסבר לעיל.
לצורך כך, צרו מחלקה כללית בשם Piece המייצגת כלי משחק בשחמט.
לכל כלי משחק צבע (color), השורה שבה הוא נמצא (row) והעמודה שבה הוא נמצא (column).
צרו גם מחלקות עבור כל אחד מהכלים: Pawn (רגלי), Rook (צריח), Knight (פרש), Bishop (רץ), Queen (מלכה) ו־King (מלך).
אפשרו לכל אחד מכלי המשחק לזוז על הלוח לפי החוקיות שתוארה מעלה.
דאגו שקריאה ל־Board תדפיס לוח ועליו כלי משחק לפי המצב העדכני של הלוח.
נפתח ונאמר שאם אתם אמיצים במידה מספקת – אנחנו ממליצים לכם לנסות לפתור את התרגיל בעצמכם.
הוא בהחלט לא פשוט ועלול לקחת לא מעט זמן, ולכן הפתרון שלנו מוגש פה:
In [ ]:
from abc import ABC, abstractmethod
class Color:
"""Describe the game pieces' color"""
BLACK = 0
WHITE = 1
def enemy_of(color):
"""Return the opponent color."""
if color == Color.BLACK:
return Color.WHITE
return Color.BLACK
class Board:
"""Create and maintain the game board."""
# Some functions below will not work well with altered board size.
BOARD_SIZE = (8, 8)
def __init__(self):
self.reset()
def get_square(self, row, col):
"""Return the game piece by its position on board.
If there is no piece in this position, or if the position does
not exist - return False.
"""
if self.is_valid_square(row, col):
return self.board[row][col]
def set_square(self, row, col, piece):
"""Place piece on board."""
self.board[row][col] = piece
def is_valid_square(self, row, column):
"""Return True if square in board bounds, False otherwise."""
row_exists = row in range(self.BOARD_SIZE[0])
column_exists = column in range(self.BOARD_SIZE[1])
return row_exists and column_exists
def is_empty_square(self, square):
"""Return True if square is unoccupied, False otherwise.
An empty square is a square which has no game piece on it.
If the square is out of board bounds, we consider it empty.
"""
return self.get_square(*square) is None
def _generate_back_row(self, color):
"""Place player's first row pieces on board."""
row_by_color = {Color.BLACK: 0, Color.WHITE: self.BOARD_SIZE[0] - 1}
row = row_by_color[color]
order = (Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook)
params = {'color': color, 'row': row}
return [order[i](col=i, **params) for i in range(self.BOARD_SIZE[0])]
def _generate_pawns_row(self, color):
"""Place player's pawns row on board."""
row_by_color = {Color.BLACK: 1, Color.WHITE: self.BOARD_SIZE[0] - 2}
row = row_by_color[color]
params = {'color': color, 'row': row}
return [Pawn(col=i, **params) for i in range(self.BOARD_SIZE[0])]
def get_pieces(self, color=None):
"""Yield the player's pieces.
If color is unspecified (None), yield all pieces on board.
"""
for row in self.board:
for square in row:
if square is not None and (color in (None, square.color)):
yield square
def reset(self):
"""Set traditional board and pieces in initial positions."""
self.board = [
self._generate_back_row(Color.BLACK),
self._generate_pawns_row(Color.BLACK),
[None] * self.BOARD_SIZE[0],
[None] * self.BOARD_SIZE[0],
[None] * self.BOARD_SIZE[0],
[None] * self.BOARD_SIZE[0],
self._generate_pawns_row(Color.WHITE),
self._generate_back_row(Color.WHITE),
]
def move(self, source, destination):
"""Move a piece from its place to a designated location."""
piece = self.get_square(*source)
return piece.move(board=self, destination=destination)
def __str__(self):
"""Return current state of the board for display purposes."""
printable = ""
for row in self.board:
for col in row:
if col is None:
printable = printable + " ▭ "
else:
printable = printable + f" {col} "
printable = printable + '\n'
return printable
class Piece(ABC):
"""Represent a general chess piece."""
def __init__(self, color, row, col, **kwargs):
super().__init__(**kwargs)
self.color = color
self.row = row
self.col = col
self.moved = False
self.directions = set()
def is_possible_target(self, board, target):
"""Return True if the move is legal, False otherwise.
A move is considered legal if the piece can move from its
current location to the target location.
"""
is_target_valid = board.is_valid_square(*target)
is_empty_square = board.is_empty_square(target)
is_hitting_enemy = self.is_enemy(board.get_square(*target))
return is_target_valid and (is_empty_square or is_hitting_enemy)
@abstractmethod
def get_valid_moves(self, board):
"""Yield the valid target positions the piece can travel to."""
pass
def get_position(self):
"""Return piece current position."""
return self.row, self.col
def is_enemy(self, piece):
"""Return if the piece belongs to the opponent."""
if piece is None:
return False
return piece.color != self.color
def move(self, board, destination):
"""Change piece position on the board.
Return True if the piece's position has successfully changed.
Return False otherwise.
"""
if not self.is_possible_target(board, destination):
return False
if destination not in self.get_valid_moves(board):
return False
board.set_square(*self.get_position(), None)
board.set_square(*destination, self)
self.row, self.col = destination
self.moved = True
return True
def get_squares_threatens(self, board):
"""Get all the squares which this piece threatens.
This is usually just where the piece can go, but sometimes
the piece threat squares which are different than the squares
it can travel to.
"""
for move in self.get_valid_moves(board):
yield move
@abstractmethod
def __str__(self):
pass
class WalksDiagonallyMixin:
"""Define diagonal movement on the board.
This mixin should be used only in a Piece subclasses.
Its purpose is to add possible movement directions to a specific
kind of game piece.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.directions.update({
(-1, -1), (1, -1),
(-1, 1), (1, 1),
})
class WalksStraightMixin:
"""Define straight movement on the board.
This mixin should be used only in a Piece subclasses.
Its purpose is to add possible movement directions to a specific
kind of game piece.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.directions.update({
(0, -1),
(-1, 0), (1, 0),
(0, 1),
})
class WalksMultipleStepsMixin:
"""Define a same-direction, multiple-step movement on the board.
This mixin should be used only on a Piece subclasses.
Its purpose is to allow a piece to travel long distances based on a
single-step pattern.
For example, the bishop can move diagonally up to 7 squares per
turn (in an orthodox chess game). This mixin allows it if the
`directions` property is set to the 4 possible diagonal steps. It
does so by overriding the get_valid_moves method and uses the
instance `directions` property to determine the possible step for
the piece.
"""
def get_valid_moves(self, board, **kwargs):
"""Yield the valid target positions the piece can travel to."""
for row_change, col_change in self.directions:
steps = 1
stop_searching_in_this_direction = False
while not stop_searching_in_this_direction:
new_row = self.row + row_change * steps
new_col = self.col + col_change * steps
target = (new_row, new_col)
is_valid_target = self.is_possible_target(board, target)
if is_valid_target:
yield target
steps = steps + 1
is_hit_enemy = self.is_enemy(board.get_square(*target))
if not is_valid_target or (is_valid_target and is_hit_enemy):
stop_searching_in_this_direction = True
class Bishop(WalksDiagonallyMixin, WalksMultipleStepsMixin, Piece):
"""A classic Bishop chess piece.
The bishop moves any number of blank squares diagonally.
"""
def __str__(self):
if self.color == Color.WHITE:
return '♗'
return '♝'
class Rook(WalksStraightMixin, WalksMultipleStepsMixin, Piece):
"""A classic Rook chess piece.
The rook moves any number of blank squares straight.
"""
def __str__(self):
if self.color == Color.WHITE:
return '♖'
return '♜'
class Queen(
WalksStraightMixin, WalksDiagonallyMixin, WalksMultipleStepsMixin, Piece,
):
"""A classic Queen chess piece.
The queen moves any number of blank squares straight or diagonally.
"""
def __str__(self):
if self.color == Color.WHITE:
return '♕'
return '♛'
class Pawn(Piece):
"""A classic Pawn chess piece.
A pawn moves straight forward one square, if that square is empty.
If it has not yet moved, a pawn also has the option of moving two
squares straight forward, provided both squares are empty.
Pawns can only move forward.
A pawn can capture an enemy piece on either of the two squares
diagonally in front of the pawn. It cannot move to those squares if
they are empty, nor to capture an enemy in front of it.
A pawn can also be involved in en-passant or in promotion, which is
yet to be implemented on this version of the game.
"""
DIRECTION_BY_COLOR = {Color.BLACK: 1, Color.WHITE: -1}
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.forward = self.DIRECTION_BY_COLOR[self.color]
def _get_regular_walk(self):
"""Return position after a single step forward."""
return self.row + self.forward, self.col
def _get_double_walk(self):
"""Return position after a double step forward."""
src_row, src_col = self.get_position()
return (src_row + self.forward * 2, src_col)
def _get_diagonal_walks(self):
"""Returns position after a diagonal move.
This only happens when hitting an enemy.
It could also happen on "en-passant", which is
unimplemented feature for now.
"""
src_row, src_col = self.get_position()
return (
(src_row + self.forward, src_col + 1),
(src_row + self.forward, src_col - 1),
)
def is_possible_target(self, board, target):
"""Return True if the Pawn's move is legal, False otherwise.
This one is a bit more complicated than the usual case.
Pawns can only move forward. They also can move two ranks
forward if they have yet to move. Not like the other pieces,
pawns can't hit the enemy using their regular movement. They
have to hit it diagonally, and can't take a step forward if the
enemy is just in front of them.
"""
is_valid_move = board.is_valid_square(*target)
is_step_forward = (
board.is_empty_square(target)
and target == self._get_regular_walk()
)
is_valid_double_step_forward = (
board.is_empty_square(target)
and not self.moved
and target == self._get_double_walk()
and self.is_possible_target(board, self._get_regular_walk())
)
is_hitting_enemy = (
self.is_enemy(board.get_square(*target))
and target in self._get_diagonal_walks()
)
return is_valid_move and (
is_step_forward or is_valid_double_step_forward or is_hitting_enemy
)
def get_squares_threatens(self, board, **kwargs):
"""Get all the squares which the pawn can attack."""
for square in self._get_diagonal_walks():
if board.is_valid_square(*square):
yield square
def get_valid_moves(self, board, **kwargs):
"""Yield the valid target positions the piece can travel to.
The Pawn case is a special one - see is_possible_target's
documentation for further details.
"""
targets = (
self._get_regular_walk(),
self._get_double_walk(),
*self._get_diagonal_walks(),
)
for target in targets:
if self.is_possible_target(board, target):
yield target
def __str__(self):
if self.color == Color.WHITE:
return '♙'
return '♟'
class Knight(Piece):
"""A classic Knight chess piece.
Can travel to the nearest square not on the same rank, file, or
diagonal. It is not blocked by other pieces: it jumps to the new
location.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.directions.update({
(-2, 1), (-1, 2), (1, 2), (2, 1), # Upper part
(-2, -1), (-1, -2), (1, -2), (2, -1), # Lower part
})
def get_valid_moves(self, board, **kwargs):
super().get_valid_moves(board, **kwargs)
for add_row, add_col in self.directions:
target = (add_row + self.row, add_col + self.col)
if self.is_possible_target(board, target):
yield target
def __str__(self):
if self.color == Color.WHITE:
return '♘'
return '♞'
class King(WalksStraightMixin, WalksDiagonallyMixin, Piece):
"""A classic King chess piece.
Can travel one step, either diagonally or straight.
It cannot travel to places where he will be threatened.
"""
def _get_threatened_squares(self, board):
"""Yield positions in which the king will be captured."""
enemy = Color.enemy_of(self.color)
for piece in board.get_pieces(color=enemy):
for move in piece.get_squares_threatens(board):
yield move
def is_possible_target(self, board, target):
"""Return True if the king's move is legal, False otherwise.
The king should not move to a square that the enemy threatens.
"""
is_regular_valid = super().is_possible_target(board, target)
threatened_squares = self._get_threatened_squares(board)
return is_regular_valid and target not in threatened_squares
def get_valid_moves(self, board, **kwargs):
super().get_valid_moves(board, **kwargs)
for add_row, add_col in self.directions:
target = (add_row + self.row, add_col + self.col)
if self.is_possible_target(board, target):
yield target
def get_squares_threatens(self, board):
"""Get all the squares that this piece may move to.
This method is especially useful to see if other kings fall
into this piece's territory. To prevent recursion, this
function returns all squares we threat even if we can't go
there.
For example, take a scenario where the White Bishop is in B2,
and the Black King is in B3. The White King is in D3, but it is
allowed to go into C3 to threaten the black king if the white
bishop protects it.
"""
for direction in self.directions:
row, col = self.get_position()
row = row + direction[0]
col = col + direction[1]
if board.is_valid_square(row, col):
yield (row, col)
def __str__(self):
if self.color == Color.WHITE:
return '♔'
return '♚'
בשבועיים האחרונים אנחנו משתמשים לא מעט במחלקות כדי לתרגל רעיונות חשובים בתכנות.
עם הכוח באה האחריות – למרות כל הטכניקות המגניבות שלמדנו, כדאי להימנע מלסבך הנדסית את הקוד שלנו יתר על המידה.
על כביש מדברי חשוך, כשמשב רוח קריר בשערכם, אתם מבחינים באור המבליח באופק. ראשכם נהיה כבד עליכם ואתם מחליטים לחנות להלילה.
אתם ניגשים לדלפק הקבלה במלון הסמוך, פוגשים בקבלה את הילברט, ומבקשים ממנו לשכור חדר.
הילברט שואל אתכם אם אתם "טובים במחשבים", ומבקש שתעזרו לו לכתוב את התוכנה לניהול בית העסק שלו.
לכל חדר במלון קליפורניה נתונים כמפורט:
בחדר יחיד אפשר להכניס עד מיטה אחת, בחדר זוגי ובחדר מלכותי שתיים, ובחדר שופרא דשופרא יש מקום לעד 3 מיטות.
אפשר להוסיף מיטה לכל חדר או להוציא ממנו מיטה, כל עוד יש בחדר לפחות מיטה אחת ומספר המיטות אינו גבוה מהתפוסה המרבית של החדר.
בינתיים, עד שהילברט ימצא תעלול על־אנושי כלשהו, מספר החדרים במלון שלו מוגבל.
צרו מחלקה שתנהל את החדרים במלון שלו.
המחלקה תכיל את הפעולה search_room שבהפעלתה תחזיר להילברט את החדר הפנוי הראשון שהיא מוצאת.
חלק מהמשתמשים מעדיפים ללון בקומה מסוימת או בחדר מסוג מסוים. אפשרו להעביר לפונקציה את הפרמטרים הללו ולחפש לפיהם.
על כל חדר שאינו שופרא דשופרא יוכל הלקוח לבצע פעולת upgrade.
הפעולה תעביר אותו לחדר בדרגה אחת גבוהה יותר, אם יש חדר כזה פנוי.
חניכים שובבים (לא אתם חלילה) החליטו לדלג על התרגיל לדוגמה, שמדגים בצורה מאלפת שימוש ברעיונות של מחלקה מופשטת וב־Mixins.
כדי לוודא שהם מבינים את החומר, השתמשו במימוש שבדוגמה (או במימוש שכתבתם בעצמכם, כמובן) ועזרו להם לממש שחמט גנואים.
בשחמט גנואים רוחבו של הלוח הוא 11 משבצות ואורכו 10 משבצות.
בשורה 1, בעמודה ז (או g בגרסה האנגלית) מוצב גנו, ובעמודות ח ו־ט (h ו־i באנגלית) מוצבים גמלים.
בשורה 10, בעמודה ה (או e בגרסה האנגלית) מוצב גנו, ובעמודות ג ו־ד (c ו־d באנגלית) מוצבים גמלים.
בשורה 1, בעמודה ה (או e בגרסה האנגלית) מוצבת המלכה, ובעמודות ג ו־ד (c ו־d באנגלית) מוצבים רצים.
בשורה 10, בעמודה ז (או g בגרסה האנגלית) מוצבת המלכה, ובעמודות ח ו־ט (h ו־i באנגלית) מוצבים רצים.
שאר הכלים מסודרים באותו הסדר.
גמלים נעים בצורה דומה לפרשים: 3 צעדים אנכית ואז צעד אחד אופקית, או 3 צעדים אופקית ואז צעד אחד אנכית.
גנואים יכולים לבחור בכל תור אם הם רוצים לנוע כגמל או כפרש.
שני כלי המשחק, גמל וגנו, יכולים לדלג מעל כלים אחרים מבלי להיחסם.
תוכלו להשתמש בתווים ⛀ ו־⛂ כדי לייצג גמלים ובתווים ⛁ ו־⛃ כדי לייצג גנואים.
בגרסה זו של המשחק ישנה הרחבה קלה לתנועת הרגלים הבסיסית.
אם רגלי לא נע מעולם, הוא יכול לנוע עד 3 צעדים במעלה הלוח.
אם רגלי נע רק צעד אחד במעלה הלוח, הוא עדיין רשאי להתקדם 2 צעדים נוספים במעלה הלוח.
רגלי שהתקדם 2 צעדים או יותר רשאי לנוע רק צעד אחד במעלה הלוח בכל פעם.
נוסף על כך, בלי קשר לשחמט גנואים וסתם בשביל הכיף, ממשו חד־קרן.
חד־קרן הוא כלי שיכול לנוע בתור אחד מספר בלתי מוגבל של צעדי פרש לאותו הכיוון.
לצורך ייצוג חד־קרן לבן על הלוח תוכלו להשתמש בתו 🦄, ועבור השחור השתמשו ב־🐴 (זה מה יש).
וכן, אנחנו יודעים שהוא הורס את הדפסת הלוח. מותר לו. הוא חד־קרן.