פרק עשירי

כתיבת פונקציות

תוכן העניינים

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

age = input(‘Insert age; -1 to stop: ‘)

while age != ‘-1’:

    print(age)

    age = input(‘Insert age; -1 to stop: ‘)

 

(1) מבוא

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

i = 1

while i < 6:

    print(‘something’) 

    i = i + 1

כדי להשיג מטרה שווה למטרה שמשיג קוד זה, כלומר הדפסת המחרוזת ‘something’ חמש פעמים, היינו יכולים לכתוב את גוף הלולאה חמש פעמים, כך:

print(‘something’) 

print(‘something’)

print(‘something’)

print(‘something’)

print(‘something’)

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

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

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

(2) מתי נכתוב פונקציה? המשחק "עולמו של הארי פוטר"

קודם שנפנה ללמוד כיצד לכתוב פונקציה, נעיין בדוגמה אחת לכתיבת תכנית שמתעורר בה הצורך בכתיבת פונקציה. הדוגמה נוגעת למשחק המחשב “עולמו של הארי פוטר”. משחק זה מתבסס על קובץ נתונים המכיל מידע מגוון בנוגע לדמויות בסדרת הספרים: מספר סידורי של הדמות, שם משפחה, שם פרטי, מגדר, הבית בבית הספר “הוגוורטס” שהן משתייכות אליו, המין (Species) שהן משתייכות אליו, ועוד. לצורך הדיון שלנו נשתמש בגרסה מוקטנת של הקובץ, בשם characters.csv (לחצו על שמו כדי להורידו). הסיומת, csv, מציינת סוג קובץ שמקובל לשמור בו גליונות נתונים. כך נראה גליון הנתונים בקובץ characters.csv כאשר הוא נפתח בתכנה “אקסל”:

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

• שאלה 1 – כמה דמויות משתייכות לבית מסוים ב”הוגוורטס” (למשל Gryffindor)? 

• שאלה 2 – לכמה דמויות יש שם משפחה מסוים (למשל Potter)? 

• שאלה 3 – כמה דמויות משתייכות ל-Species מסוים (למשל Human)?

אם המשתמשת משיבה תשובה שגויה לשאלה 1 הסיבוב מסתיים, ולא נשאלות שתי השאלות הבאות; ואם המשתמשת משיבה תשובה שגויה לשאלה 2, הסיבוב גם כן מסתיים ולא נשאלת השאלה השלישית. בסוף הסיבוב כולו המשתמשת מקבלת הודעה על מספר השאלות שהשיבה עליהן נכון, ומתבקשת לומר אם ברצונה להמשיך לסיבוב נוסף או להפסיק את המשחק. נוסף על כך בתגובה לכל אחת מתשובותיה לשאלות 1–3, מוצגת למשתמשת רשימה של המספרים הסידוריים של הדמויות המקיימות את התנאי המוצג בשאלה (משתייכות לבית מסוים, בעלות שרביט מסוים, וכו’). לדוגמה נניח שהשאלה היא לאלו דמויות יש שם המשפחה Weasley. בגליון הנתונים יש מידע על ארבע דמויות ששם המשפחה שלהן הוא Weasley, והמספרים הסידורים שלהם הם 2, 6, 7 ו-8. לכן מוצגת הרשימה הזאת: 

[2, 6, 7, 8]

ביצירת הרשימה מבוצעת סדרת ההוראות הזאת:

        (1) מאותחלת רשימה ריקה, שמה (למשל) הוא ids

         (2) מתבצעת סריקה של כל שורה ושורה בגליון הנתונים, ועבור כל שורה

         (2.1)     אם במשתנה SURNAME בשורה הנוכחית מופיעה המחרוזת ‘Weasley’ 

         (2.1.1)            המספר הסידורי של הדמות בשורה הנוכחית מתוסף לרשימה ids 

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

         (1) עבור ערך מסוים val של משתנה (עמודה) var בגליון הנתונים 

              (למשל המחרוזת ‘Gryffindor’ במשתנה HOUSE)

         (2) מאותחלת רשימה ריקה, שמה (למשל) הוא ids

         (3) מתבצעת סריקה של כל שורה ושורה בגליון הנתונים, ועבור כל שורה

         (3.1)        אם במשתנה var בשורה הנוכחית מופיע הערך val 

                        (למשל אם במשתנה HOUSE מופיעה המחרוזת ‘Gryffindor’) 

         (3.1.1)                המספר הסידורי של הדמות בשורה הנוכחית מתוסף לרשימה ids

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

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

(3) אנטומיה של פונקציה ושל זימון פונקציה

נעיין בקטע קוד זה:

def computeGrade(grade): 

    newGrade = grade + 5

    return newGrade

לפניכם הגדרת פונקציה. תכלית הפונקציה: להוסיף ניקוד לציון נתון. 

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

• השורה מתחילה במילה השמורה def.

• לאחר מכן בא שם הפונקציה – כאן: computeGrade. 

• לאחר מכן מופיעים סוגר עגול פותח וסוגר עגול סוגר; לעתים יש בתוך הסוגריים שם משתנה אחד או יותר – כאן כתוב שם אחד: grade. 

• לבסוף בא הסימן נקודתיים. 

מתחת לשורה הראשונה של הפונקציה באה הוראה אחת או יותר; כאן יש שתי הוראות. הוראות אלו מוזזות ימינה לעומת השורה הראשונה (ממש כמו במבנה if ובמבני לולאות). נקרא למכלול ההוראות הללו בשם ‘גוף הפונקציה’ (function’s body). 

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

def computeGrade(grade): 

    newGrade = grade + 5

    return newGrade


userGrade = int(input(‘Please enter grade (0-100): ‘)) 

gradeWithFactor = computeGrade(userGrade) 

print(‘Grade with factor is’, gradeWithFactor) 

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

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

כאן מופיעות שלוש הוראות מחוץ לפונקציה computeGrade: 

• בביצוע ההוראה הראשונה, המשתמשת מכניסה ציון והוא מוצב במשתנה userGrade. 

• ההוראה השנייה משתמשת בפונקציה computeGrade כדי להוסיף ניקוד לציון userGrade ולהציב את הציון החדש למשתנה gradeWithFactor. מיד נרחיב את הדיבור בצעד זה. 

• ההוראה השלישית מדפיסה את הציון החדש. 

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

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

שלב א’ – העברת ארגומנטים לפרמטרים של הפונקציה

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

computeGrade(userGrade)

מעביר את הציון שהכניסה המשתמשת, לדוגמה 85, לפונקציה computeGrade. ערך המועבר לפונקציה בזימונהּּ מכונה ‘ארגומנט’ (argument), הוא מוצב במשתנה הנכתב בתוך הסוגריים העגולים ליד שם הפונקציה בהגדרתה:

def computeGrade(grade):

המשתנה grade, כמו משתנים אחרים המופיעים בתוך הסוגריים העגולים בהגדרת פונקציה, מכונה ‘פרמטר’ (parameter). פרמטרים של פונקציה הם המנגנון שבאמצעותו מועברים לה נתונים. אם הציון המועבר לפונקציה הוא 85, מתבצעת בזימון הוראת ההשמה הזאת: 

grade = 85

אנו איננו רואים את פעולת ההשמה הזאת במפורש; היא מתבצעת ‘מאחורי הקלעים’ ברגע זימון הפונקציה. לאחר שפעולת ההשמה מתבצעת הפונקציה יכולה להשתמש בפרמטר grade כיוון שהוצב בו ערך.

שלב ב’ – ביצוע גוף הפונקציה 

לאחר הצבת הארגומנט בפרמטר (או: הארגומנטים בפרמטרים – לכך נראה דוגמות להלן), הפונקציה מתחילה את פעולתה. במקרה שלפנינו ההוראה בגוף הפונקציה מוסיפה 5 לציון grade והתוצאה מוצבת במשתנה newGrade: 

 

newGrade = grade + 5

בדוגמה הועבר לפונקציה הערך 85 והוצב בפרמטר grade. לכן הערך שיוצב במשתנה newGrade הוא 90.

שלב ג’ – החזרת ערך מהפונקציה 

הרבה פעמים פונקציה מזומנת לצורך ביצוע פעולה שיש לה תוצאה, והתוצאה הזאת היא ערך אחד מסוג מסוים. לדוגמה הפונקציה computeGrade זומנה כדי להוסיף ניקוד לציון נתון. תוצאת הפעולה היא הציון בתוספת הניקוד. תוצאה זו נדרשת מחוץ לפונקציה, כדי להציגה למשתמשת. מכאן שהפונקציה צריכה להעביר את תוצאת החישוב אל מחוץ לה, ובייחוד אל המקום שזומנה ממנו. פעולת העברה זו מתוך הפונקציה אל המקום שזומנה ממנו נעשית באמצעות ההוראה return. למעשה ההוראה return מבצעת לא אחת אלא שלוש פעולות: היא מסיימת את פעילותו של זימון הפונקציה הנוכחי, גורמת לזרימת התכנית לשוב למקום שהפונקציה זומנה ממנו, ומחזירה למקום הזה ערך הנכתב לידה; הערך המוחזר נקרא ‘ערך החזרה’. לדוגמה ההוראה בסוף הפונקציה computeGrade: :

return newGrade

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

gradeWithFactor = computeGrade(userGrade)

כיוון שהציון שהוצב במשתנה newGrade הוא 90. ההוראה return תחזיר 90 לצד הימני של הוראת ההשמה. כלומר בסיכומו של דבר תתבצע ההוראה הזאת:

gradeWithFactor = 90

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

print(‘Grade with factor is’, gradeWithFactor)

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

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

(4) פרמטרים של פונקציה – מספרם, ערכי ברירת מחדל, ושינויים בהם

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

def computeGrade(grade, factor): 

    newGrade = grade + factor

    print newGrade

בגרסתה זו הפונקציה computeGrade מקבלת שני פרמטרים: grade ו-factor. הפרמטר grade צריך לקבל ציון והפרמטר factor צריך לקבל תוספת ניקוד לציון. שני המשתנים האלה משמשים בחישוב הציון החדש newGrade. מכאן שזימון של הפונקציה computeGrade בגרסתה זו יראה אחרת מזימון הפונקציה computeGrade בגרסתה הקודמת – הפעם יועברו בזימון שני ארגומנטים. דוגמה:

gradeWithFactor = computeGrade(85, 3)

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

gradeWithFactor = computeGrade(factor = 3, grade = 85)

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

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

def getUserGrade(): 

  userGrade = int(input(‘Please enter grade (0-100): ‘) 

  while (userGrade < 0 or userGrade > 100): 

    userGrade = int(input(‘Grade not in range, Enter again: ‘)

  return userGrade

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

userGrade = getUserGrade()

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

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

def computeGrade(grade, factor = 5): 

    newGrade = grade + factor

    return newGrade

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

gradeWithFactor = computeGrade(85)

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

gradeWithFactor = computeGrade(85, 5)

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

gradeWithFactor = computeGrade(85, 3)

בזימון זה יוצב בפרמטר factor הארגומנט 3, והיא תחזיר 88.

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

def computeGrade(grade = 50, factor = 5): 

    newGrade = grade + factor

    return newGrade

זימון הפונקציה computeGrade בגרסתה זו יכול להראות כך:

gradeWithFactor = computeGrade()

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

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

def computeGrade(grade = 50, factor): 

    newGrade = grade + factor 

    return newGrade

כאן לא הוגדר ערך ברירת מחדל לפרמטר factor. אם נרצה להציב בו 3 ונכתוב את הזימון כך:

gradeWithFactor = computeGrade(3)

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

gradeWithFactor = computeGrade(factor = 3)

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

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

def computeGrade(grade, factor): 

    grade = grade + factor

    return grade

 

computeGrade(85, 5)

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

def computeGrade(grade = grade + factor, factor):

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

def computeGrade(5 + grade, factor):

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

(5) אי-קביעת סוג הארגומנטים

נשוב לגרסה זו של הפונקציה computeGrade:

def computeGrade(grade = 50, factor): 

    newGrade = grade + factor

    return newGrade

ונעיין בזימון הפונקציה הזה:

gradeWithFactor = computeGrade(“85”, “3”)

שלא כמו כל הזימונים של הפונקציה computeGrade שראינו עד כה – כולם העבירו אליה מספרים – הזימון הזה מעביר לפונקציה מחרוזות. אף על פי כן התכנית תרוץ ולא תיגרם שגיאה. בהגדרת הפונקציה אין דבר מה המציין שמן ההכרח להציב מספרים בפרמטרים. זאת ועוד: האופרטור + פועל על מחרוזות. במקרה שלפנינו הוא יופעל על הארגומנטים שקיבלה הפונקציה וייצור מחרוזת חדשה:

“85” + “3” → “853”

הפונקציה תחזיר את המחרוזת “853” בלי “לחוש” כלל שלא מילאה את ייעודה, הווה אומר ביצוע פעולה חשבונית! 

באופן שווה אפשר להעביר לפונקציה רשימות: 

gradeWithFactor = computeGrade([85], [3])

בזימון זה יופעל האופרטור + כך:

[85] + [3] → [85, 3]

והרשימה [3, 85]  תהיה ערך ההחזרה של הפונקציה. 

אי הקביעה של סוג הערך המועבר לפרמטר בשלב הגדרת הפונקציה היא ביטוי לתכונה חשובה של פייתון, תכונה שעמדנו עליה כבר בתחילת הספר: קביעת סוג (או: טיפוס) דינמית ( Dynamic typing; ראו פרק ראשון, סעיף 6). לתכונה זו יש יתרונות ויש חסרונות, ואין זה המקום להרחיב בהם את הדיון. נאמר בקצרה כי אף על פי שקביעת סוג דינמית יכולה ליצור קשיים אם מועברים לפונקציה ערכים מסוגים שהיא לא צריכה לטפל בהם והפונקציה עצמה אינה מזהה זאת, פעמים רבות קביעת סוג דינמית מועילה בכתיבת קוד כללי. עיינו למשל בקוד זה ובפלט של הרצתו: 

def mirror(val): 

    return val == val[::-1]

 

 

print(mirror(‘abba’)) 

print(mirror([‘a’, ‘b’, ‘b’, ‘a’]))

print(mirror((‘a’, ‘b’, ‘b’, ‘a’)))

>>>

True

True

True

הפונקציה mirror מקבלת ערך, מניחה שהוא רצף, ובודקת אם הרצף הוא תמונת ראי של עצמו, כלומר אם היפוך סדר האיברים בו יוצר רצף שווה. היא מבצעת את ההיפוך באמצעות הפעלת האופרטור [ ] בתחביר ההופך רצף (ראו פרק חמישי, סעיף 8). היפוך באופן זה תקין עבור מחרוזת, רשימה ורשומה. יוצא מכאן שהפונקציה תפעל באופן תקין הן אם תוצב מחרוזת בפרמטר val, הן אם תוצב בו רשימה, הן אם תוצב בו רשומה. ואמנם שלושת זימוני הפונקציה פועלים באופן תקין. אם כן במקרה זה צמחה תועלת מאי קביעת סוג הערך val בהגדרת הפונקציה: היא אפשרה לנו לכתוב פונקציה כללית החלה על מגוון סוגי רצפים.

(6) טווחי ראייה

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

def computeGrade(grade): 

    factor = 5 

    newGrade = grade + factor 

    return newGrade

print(factor)

>>>

   print(factor)

NameError: name ‘factor’ is not defined

הוראת ההדפסה מחוץ לפונקציה מנסה לגשת למשתנה factor. במבט ראשון אפשר לחשוב שאין קושי בכך, כיוון שהגדרת הפונקציה באה לפני הוראת ההדפסה. ואולם הקושי קיים גם קיים: המשתנה factor הוגדר בתוך הפונקציה. הגישה למשתנה זה היא בלתי אפשרית מחוץ לפונקציה. לכן תיגרם שגיאה מסוג NameError. לאור זאת נאמר כי ‘טווח הראייה (ה-Scope) של המשתנה factor הוא גוף הפונקציה’.

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

def computeGrade(grade):     

    newGrade = grade + factor 

    return newGrade

 

 

factor = 5

gradeWithFactor = computeGrade(85)

print(gradeWithFactor)

>>>

90

כאן אין מוגדר משתנה בשם factor בפונקציה computeGrade. אף על פי כן ההוראה הראשונה בגוף הפונקציה, כלומר חישוב הציון החדש, תתבצע בלי שגיאה. זאת כיוון שהוגדר משתנה בשם factor מחוץ לפונקציה, ומשתנה זה נמצא בטווח הראייה של הפונקציה. הפונקציה יכולה “לראות” את המשתנה הזה, ולכן פעולת החישוב של הציון החדש תשתמש בערך שיש במשתנה זה. 

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

def computeGrade(grade):     

    factor = 3

    newGrade = grade + factor 

    return newGrade

 

 

factor = 5

gradeWithFactor = computeGrade(85)

print(gradeWithFactor)

>>>

88

כאן מוגדר משתנה בשם factor הן בתוך הפונקציה הן מחוץ לה. בטווח הראייה של הפונקציה נמצא רק המשתנה factor שהוגדר בתוכה. לכן הוראת חישוב הציון החדש newGrade משתמשת במשתנה שהוגדר בתוך הפונקציה, ולכן הציון החדש הוא 88 ולא 85. 

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

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

(7) ארגומנטים שאפשר לשנותם במקום

עיינו בקטע קוד זה ובפלט שלו:

def computeGrade(grade): 

    grade = grade + 5

    return grade

 

 

userGrade = int(input(‘Please enter grade (0-100): ‘)) 

gradeWithFactor = computeGrade(userGrade) 

print(gradeWithFactor)

print(userGrade)

>>>

Please enter grade (0-100): 85

90

85

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

נניח כי בהרצת הקוד המשתמשת הכניסה את הציון 85. ציון זה הוצב במשתנה userGrade. בניסוח מדויק מזה: נוצר אובייקט המחזיק את המספר השלם 85 ושמו של האובייקט הוא userGrade.

לאחר מכן זומנה הפונקציה computeGrade והערך במשתנה userGrade הוצב בפרמטר grade. ההצבה האחרונה נתנה שם נוסף, מקומי לפונקציה, לאובייקט שמחוץ לפונקציה כונה userGrade; השם המקומי הוא grade, וכאמור אפשר להשתמש בו רק בתוך הפונקציה.

הפעולה הראשונה המתבצעת בפונקציה, כלומר הוספת 5 למספר השלם שמחזיק האובייקט grade, יוצרת אובייקט חדש המחזיק את התוצאה, 90, ומקבל את השם grade (כלומר שם זה משמש מעתה כינוי לאובייקט אחר מזה שכונה grade לפני ביצוע הפעולה).

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

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

def computeGrades(grades):     

    for i in range(len(grades)): 

        grades[i] = grades[i] + 5

    return grades 

 

userGrades = [85, 90, 95]

gradesWithFactor = computeGrades(userGrades)

print(gradesWithFactor)

print(userGrades)

>>>

[90, 95, 100]

[90, 95, 100]

הזרימה בקוד מתקדמת כך:

• הקוד מחוץ לפונקציה מתחיל בהגדרת רשימה של שלושה ציונים. שם הרשימה: userGrades. 

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

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

בתום הסריקה הפונקציה מחזירה את הרשימה grades המעודכנת, והרשימה הזאת מוצבת במשתנה gradesWithFactor. כיוון שהאובייקט המוחזר זהה לאובייקט שהוגדר מחוץ לפונקציה, הרי שכעת מחוץ לפונקציה יש לאובייקט הזה שני שמות – השם המקורי שניתן לו, כלומר userGrades, והשם החדש, gradesWithFractor.

לבסוף מודפסת הרשימה שיש במשתנה gradesWithFactor והרשימה שיש במשתנה userGrades. אם נעיין בפלט של הדפסת הרשימה userGrades, נראה כי תוכנה בשלב שהיא מודפסת, כלומר בסוף הקוד, זהה לתוכן הרשימה שיש במשתנה gradesWithFactor.

 

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

def computeGrades(grades):     

    for i in range(len(grades)): 

        grades[i] = grades[i] + 5

 

userGrades = [85, 90, 95]

computeGrades(userGrades)

print(userGrades)

>>>

[90, 95, 100]

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

def computeGrades(grades):     

    for i in range(len(grades)): 

        grades[i] = grades[i] + 5

    return grades 

 

userGrades = [85, 90, 95]

gradesWithFactor = computeGrades(userGrades[:])

print(gradesWithFactor)

print(userGrades)

>>>

[90, 95, 100]

[85, 90, 95]

גם בתחילת הקוד הזה מוגדר האובייקט userGrades:

ואולם בזימון הפונקציה computeGrades אין מועבר אליה האובייקט הזה אלא עותק שלו. עותק זה נוצר באמצעות הפעלת האופרטור [ ] על האובייקט userGrade:

userGrades[:]

ההוראה הזאת יוצרת אובייקט שהוא עותק של האובייקט userGrades.

בזימון הפונקציה ניתָן לאובייקט העותק שם – שם הפרמטר grades.

גוף הפונקציה משנה את האובייקט grades. שינויים אלה אינם משתקפים באובייקט שהוגדר מחוץ לפונקציה, userGrade.

האובייקט המוחזר מהפונקציה לקוד שמחוץ לה (בהוראת ה-return) הוא אובייקט העותק. מחוץ לפונקציה הוא מקבל את השם gradesWithFactor!

הוראות ההדפסה בסוף הקוד האחרון מדפיסות אפוא רשימות המוחזקות בשני אובייקטים שונים זה מזה.

(8) עוד על החזרת ערך מפונקציה

לצד הוראת return אפשר לכתוב ביטוי, וההוראה תחזיר את תוצאת הביטוי. דוגמה:

def computeGrade(grade):     

    return grade + 5

    

gradeWithFactor = computeGrade(85)

print(gradeWithFactor)

>>>

90

הביטוי grade + 5 חושב לפני שבוצעה הוראת ה-return. לאחר שחושב הוחזרה תוצאתו, 90. 

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

def computeGrade(grade):     

    if grade < 90: 

        return grade + 5

    return grade + 2

 

 

gradeWithFactor = computeGrade(85)

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

בהקשר הנוכחי כדאי לתת את הדעת גם לקוד הזה: 

def computeGrade(grade):     

    if grade < 90: 

        return True

    return False 

    

gradeWithFactor = computeGrade(85)

גוף הפונקציה מממש אלגוריתם כללי זה: 

    (1) אם ערכו של ביטוי לוגי הוא True

    (1.1)       יש להחזיר True

    (2) יש להחזיר False

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

def computeGrade(grade):     

    return grade < 90

    

gradeWithFactor = computeGrade(85)

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

def computeGrade(grade):     

    return grade + 5 

    

gradeWithFactor = computeGrade(85)

print(‘The new grade is: ‘ + gradeWithFactor)

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

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

def compute2Grades(grade, factor1, factor2):     

    return grade + factor1, grade + factor2

 

 

gradeWithFactors = compute2Grades(85, 3, 5)

print(gradeWithFactors[0])

print(gradeWithFactors[1])

>>>

88

90

לצד הוראת ה-return כאן כתבנו שני ערכים: ציון נתון בתוספת ניקוד אחת, והציון הזה בתוספת ניקוד אחרת. תנו דעתכם: שני המספרים המוחזרים – מוחזרים בתוך רשומה (על אף שהם לא נתחמו כאן בסוגריים עגולים). בדוגמה הפעלנו את האופרטור [ ] על הרשומה שהוחזרה מהזימון כדי לגשת אל שני הערכים שהיא מכילה. 

לבסוף נשוב ונציין כי אין הכרח לבצע הוראת return בפונקציה. דוגמה: 

def printGradeWithFactor(grade, factor): 

    print(grade + factor) 

 

 

computeGrade(85, 5)

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

(9) זימון פונקציה מפונקציה

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

def getUserGrade(): 

  userGrade = int(input(‘Please enter grade (0-100): ‘) 

  while (userGrade < 0 or userGrade > 100): 

    userGrade = int(input(‘Grade not in range, Enter again: ‘)

  return userGrade


def computeGrade(factor): 

    return getUserGrade() + factor


gradeWithFactor = computeGrade(5)

וכך מתקדמת זרימת הקוד הזה: 

• בשלב ראשון מזומנת הפונקציה computeGrade והמספר 5 מוצב בפרמטר factor. 

• אחר כך מתבצע גוף הפונקציה computeGrade – כאן: הוראת החזרה של ביטוי. הביטוי מחושב לפני שערכו מוחזר. 

• הביטוי עצמו מזמן פונקציה: getUserGrade. הפונקציה הזאת מקבלת ציון מהמשתמשת ומחזירה אותו. נניח שהציון הוא 85. המספר 85 מוחזר לפונקציה computeGrade והביטוי המופיע לצד הוראת ה-return מחושב. תוצאתו: 90. 

• הערך 90 מוחזר לצד ימני של הוראת השמה מחוץ לפונקציה, ומושם במשתנה gradewithFactor. 

(10) כתיבת מודולים

בפרק הראשון התוודענו למושג ‘מודול’ (module; ראו שם, סעיף 8). הסברנו כי מודול הוא אוסף של פונקציות וערכים קבועים, וכי לפונקציות ולערכים הנכללים בו יש זיקה אלו לאלו וייעוד מסוים מוגדר. בתור דוגמה למודול הצגנו את random, מודול שיש בו פונקציות לטיפול במספרים אקראיים. ראינו כי באמצעות ייבוא של המודול random תכניות יכולות ליצור מספרים אקראיים ולהשתמש בהם לצרכים מגוונים. 

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

התכנית שומרת את טבלת הנתונים בתוך רשומה בשם HPWorld. ברשומה זו יש רשומות פנימיות. כל רשומה פנימית מחזיקה מידע משורה אחת בקובץ. הנה הרשומה HPWorld:

HPWorld = (

(‘ID’, ‘SURNAME’, ‘FORENAME’, ‘GENDER’, ‘HOUSE’, ‘SPECIES’)

(‘1’, ‘Potter’, ‘Harry’, ‘Male’, ‘Gryffindor’, ‘Human’)

(‘2’, ‘Weasley’, ‘Ronald’, ‘Male’, ‘Gryffindor’, ‘Human’)

(‘3’, ‘Granger’, ‘Hermione’, ‘Female’, ‘Gryffindor’, ‘Human’)

(‘4’, ‘Dumbledore’, ‘Albus’, ‘Male’, ‘Gryffindor’, ‘Human’)

(‘5’, ‘Hagrid’, ‘Rubeus’, ‘Male’, ‘Gryffindor’, ‘Half-Human/Half-Giant’)

(‘6’, ‘Weasley’, ‘Fred’, ‘Male’, ‘Gryffindor’, ‘Human’)

(‘7’, ‘Weasley’, ‘George’, ‘Male’, ‘Gryffindor’, ‘Human’)

(‘8’, ‘Weasley’, ‘Ginny’, ‘Female’, ‘Gryffindor’, ‘Human’)

(‘9’, ‘Snape’, ‘Severus’, ‘Male’, ‘Slytherin’, ‘Human’)

        )

תנו דעתכם: הרשומה הפנימית ראשונה מכילה את שמות המשתנים (העמודות) בגליון הנתונים, ואילו שאר הרשומות הפנימיות מכילות את המידע עבור כל דמות ודמות. בנוסף, המספרים הסידוריים שמורים כאן בתור מחרוזות. 

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

 

 

הפונקציה tupleByVal – איתור רשומה לפי ערך במשתנה 

לפונקציה זו שלושה פרמטרים: 

• table – שם הרשומה המחזיקה את הנתונים שיש בגליון הנתונים

• var – שם משתנה מסוים בגליון הנתונים

• val – ערך מסוים המופיע במשתנה var; הפונקציה מניחה כי הערך val מופיע במשתנה var

הפונקציה תחזיר את הרשומה הפנימית הראשונה שמופיע בה הערך val במשתנה var.

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

tup = tupleByVal(table = HPWorld, 

                 var = ‘SURNAME’, 

                 val = ‘Potter’)

הזימון יחזיר את הרשומה הזאת:

(‘1’, ‘Potter’, ‘Harry’, ‘Male’, ‘Gryffindor’, ‘Human’)

נוכל לממש את הפונקציה כך:

def tupleByVal(table, var, val):

    ind = table[0].index(var) 

    for tup in table[1:]: 

        if tup[ind] == val: 

            return tup 

וכך תזרום הפונקציה: 

• ביצועה מתחיל באיתור האינדקס ind: זה האינדקס שבו נמצא שם המשתנה var ברשומה הפנימית הראשונה של table, כלומר הרשומה המכילה את שמות המשתנים בגליון הנתונים. למשל שם המשתנה ‘SURNAME’ נמצא באינדקס 1. 

• לאחר מכן הלולאה סורקת את שאר הרשומות הפנימיות בתוך table. 

• עבור כל רשומה פנימית מתבצעת בדיקה זו: האם הערך המופיע ברשומה באינדקס ind (האינדקס של המשתנה var ברשומה הפנימית הראשונה), זהה לערך val? אם הערכים זהים, הפונקציה מחזירה את הרשומה הפנימית הזאת. עבור גליון הנתונים השמור ברשומה HPWorld, הפונקציה תמצא כי באינדקס 1 ברשומה הפנימית השנייה מופיעה המחרוזת ‘Potter’. לכן הפונקציה תחזיר את הרשומה הזאת. 

(‘1’, ‘Potter’, ‘Harry’, ‘Male’, ‘Gryffindor’, ‘Human’)

 

הפונקציה valByVal – איתור ערך במשתנה אחד לפי ערך במשתנה אחר

לפונקציה ארבעה פרמטרים אלה: 

• table – שם הרשומה המחזיקה את הנתונים שיש בקובץ הנתונים

• var1 – שם משתנה מסוים בגליון הנתונים

• val – שם של ערך המופיע במשתנה var1; הפונקציה מניחה כי הערך val מופיע במשתנה var1

• var2 – שם נוסף של משתנה בגליון הנתונים

הפונקציה תחזיר את הערך המופיע במשתנה var2 ברשומה הראשונה שמופיע בה הערך val במשתנה var1. 

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

tup = valByVal(table = HPWorld, 

               var1 = ‘SURNAME’, 

               val = ‘Potter’,

               var2 = ‘GENDER’)

 

נוכל לממש את הפונקציה כך:

def valByVal(table, var1, val, var2):

    ind1 = table[0].index(var1) 

    ind2 = table[0].index(var2)

    for tup in table[1:]: 

        if tup[ind1] == val: 

            return tup[ind2]

וכך תזרום הפונקציה: 

• ביצועה יתחיל באיתור האינדקסים שבהם נמצאים שמות המשתנים var1 ו-var2 ברשומה הפנימית הראשונה של table, כלומר הרשומה המכילה את שמות המשתנים: השם var1 מופיע באינדקס ind1 ברשומה הזאת, ואילו השם var2 מופיע באינדקס ind2 ברשומה הזאת. למשל שם המשתנה ‘SURNAME’ נמצא באינדקס ind1 == 1 ואילו שם המשתנה ‘GENDER’ נמצא באינדקס ind2 == 3 . 

• לאחר מכן הלולאה סורקת את שאר הרשומות הפנימיות בתוך table. 

• עבור כל רשומה פנימית מתבצעת בדיקה זו: האם הערך באינדקס ind1 שווה לערך val? אם הערכים זהים, הפונקציה מחזירה את הערך הנמצא ברשומה באינדקס ind2 – כלומר במשתנה var2. למשל בדוגמת הנתונים שלפנינו הרשומה הראשונה שבה הערך באינדקס 1 הוא ‘Potter’ היא הרשומה הזאת: 

(‘1’, ‘Potter’, ‘Harry’, ‘Male’, ‘Gryffindor’, ‘Human’)

ברגע שמאותרת הרשומה הזאת הפונקציה מחזירה את הערך ‘Male’ הנמצא באינדקס 3 ברשומה. 

 

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

לפונקציה ארבעה פרמטרים אלה: 

• table – שם הרשומה המחזיקה את הנתונים שיש בקובץ הנתונים

• var1 – שם משתנה מסוים בגליון הנתונים

• val – שם של ערך המופיע במשתנה var1; הפונקציה מניחה כי הערך val מופיע במשתנה var1

• var2 – שם נוסף של משתנה בגליון הנתונים

הפונקציה מחזירה רשימה שמופיעים בה כל הערכים במשתנה var2 הנמצאים ברשומות שבהן מופיע הערך val במשתנה var1. 

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

tup = listByVal(table = HPWorld, 

                 var1 = ‘HOUSE’, 

                 val = ‘Gryffindor’,

                 var2 = ‘ID’)

ונוכל לממש את הפונקציה כך:

def listByVal(table, var1, val, var2):

    ind1 = table[0].index(var1) 

    ind2 = table[0].index(var2)

    lst = [] 

    for tup in table[1:]: 

        if tup[ind1] == val: 

            lst.append(tup[ind2]) 

    return lst 

וכך תזרום הפונקציה: 

• ממש כמו הפונקציה valByVal, גם הפונקציה listByVal מתחילה באיתור האינדקסים שבהם נמצאים שמות המשתנים var1 ו-var2 ברשומה הפנימית הראשונה של table, כלומר הרשומה המכילה את שמות המשתנים: השם var1 מופיע באינדקס ind1 ברשומה הזאת, ואילו השם var2 מופיע באינדקס ind2 ברשומה הזאת. לפי זימון הדוגמה השם ‘HOUSE’ מופיע ב- ind1 == 4 ואילו השם ‘ID’ מופיע באינדקס 0. 

• לאחר שאותרו האינדקסים האלה נוצרת רשימה ריקה בשם lst. עד לסוף ביצוע הפונקציה הרשימה הזאת תכיל את כל הערכים במשתנה var2 שמופיע בהם הערך val במשתנה var1 – בדוגמה: כל המספרים הסידוריים המופיעים במשתנה ‘ID’ ברשומות הפנימיות שבהן במשתנה ‘HOUSE’ מופיעה המחרוזת ‘Gryffindor’. 

• הלולאה סורקת את כל הרשומות הפנימיות בתוך table חוץ מהרשומה הראשונה. כשהיא מאתרת ברשומה מסוימת את הערך val באינדקס ind1 – בדוגמה: המחרוזת ‘Gryffindor’ באינדקס 4 – היא מוסיפה לרשימה lst את הערך הנמצא באינדקס ind2 – בדוגמה: המספר הסידורי המופיע באינדקס 0. לפי גליון הדוגמה, עד לסוף הלולאה זה יהיה תכנה של הרשימה lst:

[‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’]

לאחר ביצוע הלולאה מוחזרת הרשימה lst. 

 

כאמור שלוש הפונקציות האלה יכולות לשרת לא רק את התכנית “עולמו של הארי פוטר” אלא כל תכנית המטפלת בגליון נתונים השמור בתבנית שערוכה בה הרשומה HPWorld (רשומה של רשומות). לכן נשמור את שלוש הפונקציות הללו, ואולי גם פונקציות נוספות, בתור מודול מובחן. נעשה זאת באמצעות כתיבת הפונקציות בקובץ פייתון חדש. ניתן לו את השם csvTuple.py (אפשר לבחור שמות אחרים). 

תכנית המעוניינת להשתמש במודול csvTuple צריכה לייבא אותו במפורש. אפשר לעשות זאת כך: 

import csvTuple

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

csvTuple.tupleByVal(HPWorld, ‘House’, ‘Gryffindor’)

אפשר לייבא את כל רכיבי המודול כולם כך:

from csvTuple import *

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

tupleByVal(HPWorld, ‘House’, ‘Gryffindor’)

הערה אחרונה: כאמור המודול שאנו כותבים נשמר בקובץ. כמו בכל קריאה של קובץ, גם בקריאה של קובץ שהוא מודול שאנו כתבנו יש לציין במפורש את תיקיית העבודה שהקובץ נמצא בה. יש יותר מדרך אחת לעשות זאת. נסתפק כאן בציון שיטה שכבר ראינו בפרק השביעי (סעיף 4) – שימוש בפונקציה chdir של המודול os. לדוגמה אם שמרנו את המודול בתיקיה Python בכונן C נכתוב כך:

import os 

os.chdir(r’C:\Python’) 

from csvTuple import *

לפני ביצוע הייבוא של רכיבי המודול מוגדרת תיקיית העבודה בתור התיקייה שבה נמצא קובץ המודול ולכן הייבוא יתבצע באופן תקין.

(11) סיכום

בפרק הראשון הסברנו כי תהליך של כתיבת תכנית מחשב הפותרת בעיה נתונה מורכב משני שלבים. בשלב הראשון אנו מתכננים את האלגוריתם שבאמצעותו נפתור את הבעיה ושעל פיו נכתוב את התכנית. בשלב השני נכתוב את התכנית לפי האלגוריתם שפיתחנו. בפרק הנוכחי, חוץ מלמידה כיצד לכתוב פונקציות ועיון בזימון פונקציות, קנינו גם אופן התבוננות חדש על כתיבת תוכנות. מעתה ואילך בכתיבת קוד לא רק נשאף שהאלגוריתם והתכנית שנכתוב על פיו יפתרו את הבעיה הנתונה, אלא גם נחתור להרכיב את התכנית מיחידות. לכל יחידה, או פונקציה, יהיה תפקיד ברור, ורצוי שיהיה לה קיום עצמאי מחוץ לתכנית מחשב זו או אחרת. אם נציב מטרה זו לנגד עינינו נשיג את אחת התכונות החשובות ביותר לתוכנת מחשב: מודולריות (modularity). מודולריות מאפשרת לשנות או להחליף בקלות יחידה אחת בתכנית, בלי לגעת ביחידות האחרות. כתיבת פונקציות היא אמצעי חשוב להקניית מודולריות לתכנית. לדוגמה אם הטיפול בגליון הנתונים בתכנית “עולמו של הארי פוטר” יבוסס על פונקציות, כגון אלו שיצרנו במודול csvTuple, וכל אחת מהן תטפל בפעולה מוגדרת, התכנית תהיה מודולרית, ויהיה קל לשפרה. כך למשל שכלול החיפוש של רשומה לפי ערך, שכלול החיפוש של ערך לפי ערך, ושכלול יצירת רשימה לפי ערך – כל אחת מפעולות אלו תבוצע ביחידה אחת מוגדרת של התכנית, בפונקציה אחת, ולא תשפיע בהכרח על הקוד של פונקציות אחרות. וכפי שציינו, כל אחת מפונקציות אלו תוכל לשמש בתור יחידה המרכיבה תכניות מחשב אחרות השומרות נתונים באופן דומה לשמירת הנתונים בתכנית “עולמו של הארי פוטר”. אמנם כתיבת פונקציות אינה האמצעי היחיד להקניית מודולריות לתכנית. בייחוד ראוי להזכיר אמצעי אחר: כתיבת תכניות לפי פרדיגמה המכונה “תכנות מונחה עצמים” (Object oriented programming).