בעיה עם העברת מספר בלתי מוגבל של פרמטרים עם כוכבית

שלום רב, תחילה אומר שלא ראיתי התייחסות לעניין זה באשכול של שבוע 5.

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

לייק 1

כאשר פונקציה מקבלת args* פייתון מתייחס לargs כtuple של n פרמטרים שהפונקציה תקבל בפועל.
בעצם ציינת את זה בעצמך.
במקרה הנ"ל העברת list - רק פרמטר אחד. אז הtuple (args) כולל רק איבר אחד.
כאשר הtuple כולל איבר אחד - הפסיק עדיין יופיע שזה חלק מהסינטקסיס - אין איבר ריק בצד שני, יש רק איבר אחד שהוא list.

אז אם אתה רוצה שהפונקציה תקבל iterable תן לה לקבל parameters ואין צורך להשתמש בargs* :slight_smile:

לייק 1

קשה לענות על השאלה הזו בקצרה.

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

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

בפייתון קיימת הפונקציה range, שיודעת לטפל ב־3 מקרים:

  1. אם הועבר רק ארגומנט אחד (end), הפונקציה מחזירה את כל המספרים מ־0 ועד end, לא כולל end.
  2. אם הועברו שני ארגומנטים (start, end) הפונקציה מחזירה את כל המספרים מ־start ועד end, לא כולל את end.
  3. אם הועברו שלושה ארגומנטים (start, end, jumps) הפונקציה מחזירה את כל המספרים מ־start ועד end, לא כולל end, בקפיצות של jumps. לדוגמה: עבור start = 1, end = 7, jumps = 2 יחזרו המספרים 1, 3 ו־5.

אשמח אם לפני שאתה ממשיך לקרוא תעצור ותנסה לממש את זה בעצמך :slight_smile:

הנה המימוש שלי, במגבלות הידע שצברנו עד שבוע 5:

def range_jumps(start, end, jumps):
    numbers = []
    current = start
    while start <= current < end:
        numbers.append(current)
        current += jumps
    return numbers

המימוש די פשוט: מקבלים התחלה, סוף וקפיצות. מתחילים ב־start, קופצים ב־jumps עד שמגיעים ל־end. בדרך צוברים את המספרים שעברנו עליהם.
נוכל לכתוב גם 2 פונקציות בשמות range_until ו־range_between. נשתמש בפונקציה range_jumps כדי למנוע שכפול קוד:

def range_until(end):
    return range_jumps(0, end, 1)


def range_between(start, end):
    return range_jumps(start, end, 1)

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

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

בהצלחה!

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

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

האם ישנו פיתרון אלגנטי יותר?

def range(start = 0, end = None ,jump = 1):
    if end == None:
        start, end = 0, start
    current = start
    range_list = []
    while current < end:
        range_list.append(current)
        current += jump
    return range_list

print(list(range(5)))
print(list(range(5,10)))
print(list(range(0, 10, 2)))

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

האם, לדעתך, היית יכול להשתמש בחתימת הפונקציה: def range(*params): כדי לפשט את הפונקציה שכתבת?

אם מצאת את השאלה קשה מדי, הנה כמה רמזים:

  1. לחץ על החץ כדי לראות את רמז 1

    השתמש ב־3 הפונקציות range_until, range_between ו־range_jumps.

  2. לחץ על החץ כדי לראות את רמז 2

    מה ההבדל העיקרי בקריאה לפונקציות האלו?

  3. לחץ על החץ כדי לראות את רמז 3

    צור פונקציה בשם range ש״בוחרת״ לאיזו פונקציה לקרוא.

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

def range(*params):

   start, jump = 0, 1
   if len(params) == 1:
       end = params[0]   # <- - - - - - - -  parmas = (x,)
   elif len(params)== 2:
       start,end = params
   else:
       start, end, jump = params

   current = start
   range_list = []
   while current < end:
       range_list.append(current)
       current += jump
   return range_list

print(list(range(5)))
print(list(range(5,10)))
print(list(range(0, 10, 2)))

השיטה שהצעת:


def range_jumps(start, end, jumps):
    numbers = []
    current = start
    while start <= current < end:
        numbers.append(current)
        current += jumps
    return numbers

def range_until(end):         # <<<<<<<<<<<<<<<
    return range_jumps(0, end, 1)

def range_between(start, end):
    return range_jumps(start, end, 1)

def range(*params):
    if len(params) == 1:
        return range_until(*params)     # <- - - -  params = (x,)
    elif len(params) == 2:
        return range_between(*params)
    elif len(params) == 3:
        return range_jumps(*params)
    else:
        return None

print(range(5))
print(range(5,10))
print(range(0, 10, 2))

פתאום שמתי לב שיש משהו שלא ברור לי עם השימוש ב *args כשיש לי רק איבר אחד שמועבר לפונקציה. :sweat_smile:
ערכתי את קטע הקוד בפוסט מעליי והוספתי חצים כדי שיהיה ניתן לראות במה מדובר.

מדוע בקטע הקוד הראשון כאשר מועבר לי מידע רק לגבי end, על מנת לא לקבל שגיאה הייתי צריך להגדיר
end = params[0], כי הרי params הינו tuple
אולם מדוע בקטע הקוד השני כאשר העברנו את אותו tuple עם איבר אחד ריק לפונקציה range_until(end)… פייתון יידע לפרק את הtuple ולקחת רק את החלק ב ישנו מידע.

סליחה מראש על כל השאלות, כידוע ‘תורה היא וללמוד אני צריך’ :slight_smile:

כיוון שאתה מפרק אותו בקריאה עצמה: אתה משתמש ב־range_until(*end).
הכוכבית לפני המילה end משמעה ״פרק את ה־iterable ששמו end לכלל איבריו, והכנס את האיבר הראשון שלו לפרמטר הראשון, את השני לפרמטר השני וכן הלאה״.