הטיפוס המינימלי

29/08/2023

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

def longer_than(n: int, items: list[str]) -> list[str]:
    return [I for I in items if len(i) > n]

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

print(longer_than(3, ((1, 2, 3), (2, 3, 4, 5, 6), range(10))))

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

def longer_than(n: int, items: Iterable[Sized]) -> list[Sized]:
    return [I for I in items if len(i) > n]


print(longer_than(3, ((1, 2, 3), (2, 3, 4, 5, 6), range(10))))

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

long_items = longer_than(3, ((1, 2, 3), (2, 3, 4, 5, 6), range(10)))
print(long_items[0][0])

הבעיה היא ש long_items הוא Iterable של Sized (כי זה מה שהפונקציה מחזירה), ו Sized לא מאפשר גישה דרך אינדקס. האמת היא ש long_items הוא בכלל Iterable של tuple, ולכן הקוד כן עובד אבל מערכת הטיפוסים לא רואה את זה.

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

T = TypeVar('T', bound=Sized)  
def longer_than(n: int, items: Iterable[T]) -> list[T]:
    return [I for I in items if len(i) > n]

long_items1 = longer_than(3, ((1, 2, 3), (2, 3, 4, 5, 6), range(10)))
print(long_items1[0][0])  

long_items2 = longer_than(3, ['abc', 'abcdefg', 'as'])
print(long_items2[0].capitalize())

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