בעיה ט"ז

הוראות המופיעות בגוף של מבנה while ומתבצעות כל עוד תנאי הלולאה מתקיים צריכות להיות מוזזות ימינה ביחס לשורה הראשונה במבנה. הקוד התקין:
age = input(‘Insert age; -1 to stop: ‘)
while age != ‘-1’:
print(age)
age = input(‘Insert age; -1 to stop: ‘)
(א) כתבו את הפונקציה getPatientsDetails –
def getPatientsDetails(patientsDict):
הפונקציה קולטת נתונים בנוגע לנשים חולות קורונה שאושפזו בבית חולים.
לפונקציה פרמטר אחד: patientsDict, מילון. כל מפתח במילון הוא רשומת זיהוי של חולה. הרשומה מכילה שני ערכים, בסדר זה: שם בית חולים שהחולה אושפזה בו (מחרוזת טקסט) ומספר מזהה של החולה (מספר שלם). כל ערך במילון הוא רשומה המכילה שלושה ערכים, בסדר זה: גיל החולה (מספר שלם), מספר ימי אשפוז (מספר שלם), וערך בוליאני – True אם החולה הִבריאה או False אם החולה לא הבריאה. הנה דוגמה לתוכן אפשרי של המילון –
{(‘Hadassah’, 5423): (75, 13, False), (‘Shiba’, 6532): (65, 7, True), (‘Hadassah’, 9932): (12, 21, True)}
מילון דוגמה זה מכיל מידע על שלוש חולות. הזוג הראשון במילון מכיל פרטים על חולה שאושפזה בבית חולים הדסה ושהמספר המזהה שלה הוא 5423. חולה זו הייתה בגיל 75, אשפוזה נמשך 13 ימים, והיא לא החלימה מהמחלה.
הפונקציה קולטת נתונים על חולות קורונה ומוסיפה אותם למילון patientsDict. הקליטה נעשית בלולאה. בכל סיבוב של הלולאה המשתמשת תישָאל אם היא רוצה להכניס נתונים על חולה נוספת. אם היא אינה רוצה, הלולאה תסתיים. אם היא רוצה, היא תכניס נתונים מלאים על חולה חדשה, והמילון patientsDict יעודכן בנתונים שהיא הכניסה.
נתוני הגיל, מספר ימי האשפוז, והחלמה – כן או לא – של כל חולה וחולה ייקלטו מה-console בתור מחרוזת המכילה את כל הנתונים הללו מופרדים בפסיק זה מזה. הנה דוגמה למחרוזת קלט כזו –
75,13,False
כאן המשתמשת הקלידה 75, אחר כך פסיק, אחר כך 13, אחר כך פסיק, אחר כך False, ולבסוף הקישה על המקש ENTER.
אם המשתמשת מכניסה נתונים על חולה שנתוניה כבר קיימים במילון הפונקציה תוציא הודעה על כך.
בגמר ביצוע הלולאה הפונקציה תחזיר את המילון patientsDict המעודכן (או ללא שינוי, אם הפונקציה לא הכניסה אליו נתונים).
(ב) כתבו את הפונקציה recoveryByHospital –
def recoveryByHospital(patientsDict, recovered = 1):
הפונקציה מוצאת כמה חולות קורונה הבריאו או לא הבריאו בכל בית חולים.
לפונקציה שני פרמטרים אלה –
• patientsDict – מילון שהמבנה שלו תואר בסעיף א’
• recovered – פרמטר היכול לקבל אחד משני מספרים: 1 == החולה החלימה, או 0 == החולה לא החלימה; ערך ברירת המחדל שלו הוא 1
הפונקציה מחזירה את results, רשימה של רשימות. מספר הרשימות הפנימיות ברשימה results שווה למספר שמות בתי החולים השונים זה מזה המופיעים במילון patientsDict. בכל רשימה פנימית של results יש שני ערכים אלה, לפי הסדר: שם בית חולים המופיע במילון patientsDict, ומספר שלם k: אם recovered == 1 אז k הוא מספר החולים שאושפזו בבית החולים המופיע ברשימה הפנימית והחלימו; אם recovered = 0 אז k הוא מספר החולים שאושפזו בבית החולים המופיע ברשימה הפנימית ולא החלימו. הרשימות הפנימיות מסודרות בסדר אלף-בית עולה לפי שמות בתי החולים.
דוגמה אחת: אם נתון המילון patientsDict זה –
patientsDict = {(‘Hadassah’, 5423): (75, 13, False),
(‘Shiba’, 6532): (65, 7, True),
(‘Hadassah’, 9932): (12, 21, True)}
הזימון הזה –
recoveryByHospital(patientsDict, 1)
יחזיר את הרשימה הזאת –
[[‘Hadassah’, 1], [‘Shiba’, 1]]
ברשימה המוחזרת יש שתי רשימות פנימיות כיוון שבמילון patientsDict מופיעים שני שמות של בתי חולים – הדסה ושיבא. המספר השלם בשתי הרשימות הפנימיות הוא 1 כיוון שהארגומנט השני שמקבלת recoveryByHospital הוא 1, ולפי המילון patientsDict רק חולה אחד החלים בהדסה ורק חולה אחד החלים בשיבא.
דוגמה נוספת: עבור מילון הדוגמה patientsDict הזימון הזה –
recoveryByHospital(d, 0)
יחזיר את הרשימה הזאת –
[[‘Hadassah’, 1], [‘Shiba’, 0]]
גם כאן ברשימה המוחזרת יש שתי רשימות פנימיות, ומסיבה שווה: במילון patientsDict מופיעים שני שמות של בתי חולים – הדסה ושיבא. הארגומנט השני שמקבלת recoveryByHospital הוא 0. לפי המילון patientsDict רק חולה אחד לא החלים בהדסה, ולכן המספר השלם ברשימה של הדסה הוא 1. במילון patientsDict יש חולה אחד בשיבא, והוא החלים. לכן המספר השלם ברשימה של שיבא הוא 0.
בשתי הדוגמות הרשימות המוחזרות ממוינות בסדר עולה לפי שמות בתי החולים: קודם Hadassah ואחר כך Shiba.
פתרון
(א) בבואנו לפתור בעיה זו עלינו לבנות מילון בלולאה. בכמה וכמה בעיות קודמות בנינו מילון בלולאה, ובכל זאת בבעיה זו יש חידוש: המפתחות במילון הם אוספים. ספציפית כל מפתח הוא רשומה, סוג אוסף שהוא בלתי ניתן לשינוי במקום ולכן יכול לשמש בתור מפתח במילון. הקוד המוסיף זוג למילון patientsDict יכול להתחיל כך –
patientsDict[(hospitalName, patientCode)] = . . .
הוראה זו מפעילה את האופרטור [ ] על מילון כדי להוסיף לו זוג חדש. בתוך הסוגריים המרובעים כתבנו רשומה המציינת מפתח. הרשומה מכילה נתונים על שם בית החולים ועל קוד הזיהוי של החולה (נתונים אלה ייקלטו מהמשתמשת). הרשומה נוצרה באמצעות הנחת סוגריים עגולים משני צדיו של רצף ערכים (כאן שניים) המופרדים זה מזה בפסיקים.
הקליטה של הנתונים והכנסתם למילון נעשית בלולאה. זו לולאה מסוג “כל עוד” – ספציפית: כל עוד המשתמשת רוצה להכניס נתונים – ולכן בקוד נשתמש בלולאת while. שלד הלולאה ייראה כך –
proceed = True
while proceed:
if (input(“Get details of a new patient (‘y’ or ‘n’)? “) == ‘y’):
hospitalName = input(“Enter hospital name: “)
patientCode = int(input(“patient code: “))
patientDetails = input(“Age, days, recovery status: “)
. . .
else:
proceed = False
במימוש לולאת ה-while בחרנו במבנה של “לולאה לפני קליטה” (ראו לעיל, בעיה י”ב). לפני הלולאה הגדרנו את proceed (אפשר להשתמש בשם אחר), משתנה דגל (flag) המסמן אם יש להמשיך בלולאה אם לאו. לפני הלולאה הוא מאותחל ל-True, ותנאי הלולאה הוא שתוכן משתנה זה הוא True. תוכן המשתנה יהפוך ל-False אך ורק אם המשתמשת תכניס את המחרוזת ‘n’ כשתישאל אם ברצונה להכניס נתונים הנוגעים לחולה נוספת. אם כך יקרה יושם הערך False במשתנה proceed, ובתחילת הסיבוב הבא, כשייבדק תנאי הלולאה, יימצא שיש להפסיקה. המנגנון שתואר זה עתה מאפשר לנו להימנע מקליטת הנתונים על חולה פעם אחת לפני הלולאה (כפי שקורה במבנה “קליטה לפני לולאה”). הוראות הקליטה הראשונות תתבצענה רק אם המשתמשת תביע לפחות פעם אחת רצון להכניס נתונים, כלומר רק אם תכניס את המחרוזת ‘y’ לפחות בפעם הראשונה כשתישאל אם היא מעוניינת להכניס נתונים.
כפי שאפשר להיווכח מהקוד, בהצעה שלנו למימוש נקלט שם בית החולים בתור מחרוזת, קוד הזיהוי מומר למספר שלם, ושלושת הנתונים הנוספים – גיל, מספר ימי אשפוז, וערך המציין אם החולה החלימה – כל אלה נקלטים במחרוזת אחת. לפי המוסבר בבעיה עלינו לצפות כאן היא שהמשתמשת תכניס את הגיל, מספר ימי האשפוז והערך המציין אם החולה החלימה כשהם מופרדים זה מזה בפסיקים.
עתה נשלים את הקוד ונכתוב את שארית גוף הלולאה, כלומר הטיפול בנתונים הנקלטים בכל סיבוב וסיבוב. הטיפול נעשה בשלושה שלבים אלה:
• שלב ראשון – בדיקה אם במילון כבר כבר מפתח שנשמרים בו נתוני החולה שנקלטו זה עתה מהמשתמשת – כלומר שם בית החולים וקוד החולה – ואם קיים מפתח כזה, הוצאת הודעה שנתוני החולה יעודכנו, כנדרש בשאלה.
• שלב שני – פיצול המחרוזת patientDetails המכילה את הגיל, מספר ימי האשפוז, וציון ההחלמה (כן או לא), לשלושת רכיביה. זה שלב הכרחי כיוון שלפי הנדרש בשאלה, הנתונים הללו מוכנסים למילון כשהם נפרדים זה מזה.
• שלב שלישי – הכנסת נתוני החולה לתוך המילון בתור זוג חדש, או עדכון נתוני החולה אם כבר יש במילון זוג עבור חולה זו.
הנה הקוד המממש את השלב הראשון –
if patientsDict.get((hospitalName, patientCode), False):
print(“Patient’s details will be updated.”)
קוד זה בודק אם במילון patientsDict כבר יש נתונים על אודות החולה שפרטיה הוכנסו זה עתה (כלומר בסיבוב הנוכחי של הלולאה). הבדיקה נעשית באמצעות הפונקציה get. בתור ארגומנט ראשון העברנו אליה רשומה בתבנית של מפתחות המילון, כלומר רשומה שמופיעים בה שם בית החולים בתור מחרוזת וקוד החולה בתור מספר שלם, בסדר זה. אם הרשומה הזאת היא מפתח במילון יוחזר ערך שהוא אינו False, ותוצא הודעה על עדכון נתוני החולה. אם הרשומה אינה מפתח במילון, הפונקציה תחזיר את הערך שהועבר אליה בתור ארגומנט שני, כאן: False, ותנאי הוראת ה-if לא יתקיים. פירוש הדבר שפרטים על אודות חולה זו טרם הוכנסו למילון ולא תוצא כל הודעה.
במימוש השלב השני, פיצול המחרוזת patientDetails לשלושת רכיביה, נשתמש בפונקציה split. לאחר מכן, על יסוד שלושת הרכיבים שהתקבלו מהפיצול ניצור את הרשומה שתוכנס בתור ערך (value) לזוג במילון. הנה הקוד –
patientDetailsList = patientDetails.split(‘,’)
patientDetailsTuple = (int(patientDetailsList[0]),
int(patientDetailsList[1]),
eval(patientDetailsList[2])
לאחר פיצול המחרוזת patientDetails מתקבלת רשימה של שלוש מחרוזות, לדוגמה –
[’35’, ‘12’,’True’]
ההוראה השניה בקטע הקוד יוצרת רשומה, patientDetailsTuple, שיש בה שלושה ערכים, אגב המרת שלוש המחרוזות ברשימה שהחזירה הפונקציה split מערך מסוג מחרוזת לערך מסוג אחר. המחרוזת המכילה את הגיל והמחרוזת המכילה את מספר ימי האשפוז מומרות שתיהן למספרים שלמים, ואילו המחרוזת המציינת אם החולה החלימה מומרת לערך לוגי, כלומר ל-True או ל-False. ההמרה השלישית נעשית באמצעות הפונקציה eval. פונקציה זו מקבלת מחרוזת שיש בה ערך או ביטוי תקין בשפה ומחזירה את הערך או הביטוי הזה, לא בתוך מחרוזת. אפשר לקבל את הערך השלישי ברשומה גם באופנים אחרים, למשל באמצעות הביטוי הזה –
patientDetailsList[2] == ‘True’
אם המחרוזת השלישית ברשימת המחרוזות היא ‘True’ ביטוי זה יחזיר True, כנדרש; ואם מחרוזת זו היא ‘False’ הביטוי יחזיר False, גם כן כנדרש.
אם כן לאחר ביצוע הקוד האחרון המשתנה patientDetailsTuple מכיל רשומה שיש בה גיל, מספר ימי אשפוז, וערך לוגי המציין אם החלימה. בשלב השלישי והאחרון עלינו להכניס רשומה זו בתור ערך במילון patientsDict. ההוראה המבצעת זאת כבר הוצגה בחלקה בתחילת הדיון כאן, ומובאת כאן במלואה בגרסה הסופית של הפונקציה –
def getPatientsDetails(patientsDict):
proceed = True
while proceed:
if (input(“Get details of a new patient (‘y’ or ‘n’)? “) == ‘y’):
hospitalName = input(“Enter hospital name: “)
patientCode = int(input(“patient code: “))
patientDetails = input(“Age, days, recovery status: “)
if patientsDict.get((hospitalName, patientCode), False):
print(“Patient’s details will be updated.”)
patientDetailsList = patientDetails.split(‘,’)
patientDetailsTuple = (int(patientDetailsList[0]),
int(patientDetailsList[1]),
eval(patientDetailsList[2]))
patientsDict[(hospitalName, patientCode)] = patientDetailsTuple
else:
proceed = False
return patientsDict
נעיר לסיום כי בעיה זו מציגה בפנינו מצב שבו מועבר לפונקציה אוסף שאפשר לשנותו במקום – כאן: מילון – וגוף הפונקציה משנה את תכנו של אוסף זה. בדיון קודם במצב עניינים זה (ראו לעיל, בעיה ???) הערנו כי בטיפול בו יש לשקול אם רצוננו שהשינוי הזה ישתקף מחוץ לפונקציה. כאן מובן שזה רצוננו: הרי מטרת הפונקציה היא בדיוק להוסיף למילון המועבר לפונקציה או לעדכנו. כיוון שהשינויים שהפונקציה מבצעת במילון ישתקפו מחוץ לפונקציה, וכיוון שערך ההחזרה של הפונקציה הוא המילון שהשתנה, יוצא שאין כלל צורך להחזיר את המילון, כלומר שאין כל צורך לכתוב הוראת return. כך למשל, נניח שנמחק את הוראת ה-return מגוף הפונקציה ונריץ את הקוד הזה –
d = {(‘Hadassah’, 5423): (75, 13, False),
(‘Shiba’, 6532): (65, 7, True)}
getPatientsDetails(d)
print(d)
אם בתוך הפונקציה נעשו שינויים במילון d הוראת ההדפסה לאחר זימון הפונקציה תראה את השינויים האלה גם אם המילון לא הוחזר מהפונקציה באמצעות הוראת return (הרי לפי ההנחה כאן מחקנו הוראה זו). הנה פלט אפשרי של הוראת ההדפסה –
{(‘Hadassah’, 5423): (75, 13, False),
(‘Shiba’, 6532): (65, 7, True),
(‘Hadassah’, 9932): (12, 21, True)}
לפי פלט זה מתברר שבעת ביצוע הפונקציה, אגב הלולאה, המשתמשת הכניסה למילון d נתונים בנוגע לחולה אחת ויחידה שהקוד שלה הוא 9932 ושאושפזה בהדסה.
על אף האמור, מקובל לכתוב את הוראת ה-return גם במקרים שהפונקציה משנה במקום אוסף שקבלה בתור ארגומנט, כיוון שציון ברור של ערך ההחזרה של פונקציה הוא חלק בלתי נפרד מהגדרת פונקציה.
(ב) מתיאור הרשימה results עולה כי כדי לבנותה עלינו לדעת את שמות בתי החולים השונים זה מזה. מכאן שלפתרון יהיו שני שלבים: מציאת השמות הללו, ולאחר מכן בניית הרשימה. במימוש השלב הראשון אנחנו פוגשים בעיה שכבר טיפלנו בה קודם (בעיה ט’):
נתון אוסף של רצפים. יש ליצור אוסף ללא כפילויות מערכים הנמצאים באינדקס מסוים בכל רצף פנימי.
ספציפית עלינו לבנות קבוצה (set) משמות בתי החולים המופיעים (באינדקס 0) ברשומות שהן מפתחות המילון. הנה הצעה למימוש שלב זה –
hospitalNames = set()
for key in patientsDict:
hospitalNames.add(key[0])
הקוד ערוך בתבנית של יצירת אוסף חדש בלולאה לפי ערכים באוסף נתון (ראו לעיל, בעיה א’). הוא מתחיל ביצירת המשתנה hospitalNames ואתחולו לקבוצה ריקה. עד לסוף הקוד יכיל משתנה זה את כל שמות בתי החולים שיש במילון patientsDict בלי כפילויות. הלולאה סורקת מפתח-מפתח במילון. עבור מילון הדוגמה הזה –
d = {(‘Hadassah’, 5423): (75, 13, False),
(‘Shiba’, 6532): (65, 7, True),
(‘Hadassah’, 9932): (12, 21, True)}
נסרקים המפתחות (הרשומות) האלה –
(‘Hadassah’, 5423)
(‘Shiba’, 6532)
(‘Hadassah’, 9932)
בכל סיבוב של הלולאה מיתוסף הערך הראשון (באינדקס 0) ברשומה לקבוצה hospitalNames.
אפשר לקצר את הקוד באמצעות ביטוי Set Comprehension כך –
hospitalNames = {key[0] for key in patientsDict}
כשאוסף שמות בתי החולים בידינו, נוכל להמשיך לשלב הבא, כלומר ליצירת הרשימה results. גם אותה ניצור בלולאה בתבנית הקוד ששמשה אותנו זה עתה: כלומר נתחיל באתחול הרשימה results לרשימה ריקה, ואחר כך נוסיף אליה רשימות פנימיות בזו אחר זו. כיוון שבשאלה נאמר שהרשימות הפנימיות ברשימה results צריכות להיות ממוינות לפי שמות בתי החולים, נכניס אותן מראש לרשימה result לפי סדר מיון זה: נעשה זאת באמצעות סריקה של אוסף ממוין של שמות בתי החולים, אוסף המתקבל ממיון הקבוצה hospitalNames באמצעות הפונקציה sorted. אם כן חלק זה של הקוד יתחיל כך –
results = []
for name in sorted(hospitalNames):
בגוף הלולאה מוכנסות רשימות פנימיות לרשימה results, זו אחר זו. בכל רשימה פנימית יש שני ערכים. הערך הראשון הוא שם בית החולים הנסרק הנוכחי, זה המוצב במשתנה הלולאה name. הערך השני הוא אחד משניִם –
• מספר החולות המחלימות בבית החולים name, אם recovered == 1, או
• מספר החולות שלא החלימו בבית החולים name, אם recovered == 0.
מכאן יוצא כי כדי להכניס את הרשימות הפנימות לרשימה results עלינו לדעת, עבור כל שם בית חולים name, שני מספרים אלה –
• מספר החולות שהחלימו – מספר זה שווה למספר ההופעות של הערך True בערכים מסוימים של הזוגות במילון patientsDict: אלו הן הרשומות שאליהם ממופים מפתחות שהשם המוצב במשתנה name מופיע בהם. למשל נעיין במילון הדוגמה הזה –
d = {(‘Hadassah’, 5423): (75, 13, False),
(‘Shiba’, 6532): (65, 7, True),
(‘Hadassah’, 9932): (12, 21, True)}
נניח ש-name == ‘Hadassah’. יש שני מפתחות המכילים את השם הזה, וממילא גם שתי רשומות שהמפתחות האלה ממופים אליהם. רק ברשומה אחת מהשתיים מופיע הערך True. ולכן מספר המחלימות הכולל הוא 1.
• מספר החולות שלא החלימו – נוכל לקבל מספר זה באמצעות הפחתת מספר החולות שהחלימו ממספר החולות הכולל שאושפזו בבית החולים לפי המילון patientsDict. לפי הגדרת המילון מספר החולות הכולל שווה למספר המפתחות במילון שהשם המוצב במשתנה name מופיע בהם.
הנה הצעה לקוד המוצא, עבור כל שם בית חולים, את מספר החולות שאושפזו בו ואת מספר החולות שאושפזו בו והחלימו –
numOfRecoveries = 0
patientsNum = 0
for k, v in patientsDict.items():
if k[0] == name:
patientsNum += 1
if v[2]: numOfRecoveries += 1
הקוד מגדיר שני משתנים, numOfRecoveries ו-patientsNum. עד סוף ביצוע הקוד יחזיקו משתנים אלה את מספר המחלימות בבית חולים name ואת מספר החולות הכולל בבית חולים name. בכל סיבוב נסרק זוג במילון, ואם הערך הראשון במפתח של הזוג הוא שם בית החולים הנסרק הנוכחי – כלומר השם המוצב במשתנה הלולאה name – מוגדל ב-1 מספר החולות שאושפזו בבית חולים זה. אם בנוסף הערך בזוג מציין שהחולה החלימה, מוגדל ב-1 מספר החולות המחלימות בבית חולים זה.
כל שנותר עתה הוא להוסיף לרשימה results רשימה פנימית המכילה את שם בית החולים ואת הנתונים בנוגע למספר החולות שהחלימו בו או שלא החלימו בו. הקוד המבצע זאת מופיע כאן בתוך הפונקציה כולה –
def recoveryByHospital(patientsDict, recovered = 1):
hospitalNames = {key[0] for key in patientsDict}
results = []
for name in sorted(hospitalNames):
numOfRecoveries = 0
patientsNum = 0
for k, v in patientsDict.items():
if k[0] == name:
patientsNum += 1
if v[2]: numOfRecoveries += 1
if (recovered == 1):
results.append([name, numOfRecoveries])
else:
results.append([name, patientsNum – numOfRecoveries])
return results
אם recovered == 1 הערך השני ברשימה הפנימית המוכנסת לרשימה results הוא מספר החולות שהחלימו, ואם recovered == 0 הערך השני ברשימה הפנימית המוכנסת הוא ההפרש בין מספר החולות שאושפזו בבית החולים הנוכחי הנסרק לבין מספר החולות שהחלימו בבית חולים זה, כלומר מספר החולות שלא החלימו בבית חולים זה.