מה חדש ב Python 3.10


גירסה 3.10 של פייתון שוחררה באוקטובר 2021 עם מספר פיצ'רים קטנים ופיצ'ר אחד גדול ומעניין. את הוובינר בוידאו התחלנו עם החידושים הקטנים והמשכנו להשקיע את עיקר הזמן באותו פיצ'ר גדול ומעניין שנקרא Structural Pattern Matching. אלה היו עיקרי הדברים:

1. הודעות שגיאה טובות יותר

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

x = { "one": 1, "two": 2, "three": 3
print(x)

גירסה 3.9 של פייתון תתלונן על שורת ה print עם הודעת "תחביר לא תקין", אבל פייתון 3.10 כבר תכתוב:

  File "/Users/ynonp/work/courses/webinar-live-demos/20211202-python-310/better_error_messages.py", line 2
    x = { "one": 1, "two": 2, "three": 3
        ^
SyntaxError: '{' was never closed

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

2. פיצול with לשורות

פיצ'ר שני קצת יותר נישתי אבל עדיין מעניין הוא שבמצב שיש לנו with שמקבל מספר אוביקטים, אפשר לעטוף את כל תוכן ה with בסוגריים ולשבור את הטקסט למספר שורות. התוכנית הבאה עובדת בפייתון 3.10 אבל לא עובדת ב 3.8:

with (open('/etc/passwd') as fi,
      open('./copy', 'w') as fo):
        for line in fi:
            fo.write(line)

(ומה לגבי 3.9 ? שם מסתבר שהקוד עבד אבל לא בכל המקרים ובכל מקרה לא היה מתועד).

3. שימוש בסימן | כדי לבנות Union Types

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

x: str|int|float = 5

ופייתון יתייחס לזה כאילו כתבתי:

from typing import Union
x: Union[str,int,float] = 5

4. התאמת תבניות מבנית

פיצ'ר אחרון לוובינר והמורכב ביותר הוא כתיב נקי יותר לבלוקים של if/elif/else שנקרא Structural Pattern Matching. בכתיב זה אנחנו מתחילים במילה match, אחריה כותבים ערך שמתפקד כקלט ואז באינדנטציה מעבירים בלוקים שמתחילים במילה case ואחריה תבנית. כל תבנית מייצגת סוג של תנאי על הקלט, ופייתון "ייכנס" לבלוק שמתאים לתבנית שהתנאי שלה התקיים.

תנאי ראשון ופשוט ביותר הוא פשוט הערך של הדבר. אני יכול לכתוב:

x = 8
match x:
    case 4:
        print("it's 4")
    case 8:
        print("it's 8")

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

def do_something_with_strings_or_numbers(x):
    match x:
        case int(7):
            print("Boom!")
        case int()
            print(x+1)
        case str():
            print(f"Hello {x}")

הקוד ידפיס Boom אם הפונקציה הופעלה עם המספר 7, אחרת אם הפונקציה הופעלה עם מספר בתור פרמטר ידפיס את המספר שאחריו ואחרת אם הפונקציה הופעלה עם מחרוזת בתור פרמטר היא תדפיס את המילה Hello ואז את תוכן המחרוזת. תבנית כזאת מזכירה את isinstance בשילוב עם השוואה.

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

from math import sqrt

def sqrt_with_negatives(x: int):
    match x:
        case int() if x > 0:
            return sqrt(x)

        case int() if x < 0:
            return sqrt(-x)

        case 0:
            return 0

print(sqrt_with_negatives(10))
print(sqrt_with_negatives(-4))
print(sqrt_with_negatives(0))

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

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

def has_access(user, post):
    match user:
        case {"admin": True}:
            return True

        case {"id": post["author_id"]}:
            return True

        case _:
            return False

admin = { "name": "a", "admin": True, "id": 1 }
user1 = { "name": "b", "id": 2 }
user2 = { "name": "c", "id": 3 }

post = { "title": "Python Pattern Match", "id": 1, "author_id": user1["id"] }

print(has_access(admin, post))
print(has_access(user1, post))
print(has_access(user2, post))

אנחנו רואים שמשתמש שיש לו מפתח admin יכול לערוך את כל הפוסטים; משתמש שאין לו את המפתח הזה יכול לערוך רק פוסטים ששדה author_id שלהם זהה לשדה id שלו.

הבלוק האחרון case _ הוא בלוק default שתמיד יצליח "להתאים" ולכן תמיד יופעל אם אף אחד מהבלוקים שמעליו לא הופעל.

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

x = [10, 20, 30, 40]
match x:
    case [*_, 30, last]:
        print(f"after 30 I found {last}")

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

from datetime import datetime

match datetime.now():
    case datetime(year=2021, month=12, day=x):
        print(f"Today is December {x}")

5. לסיכום

פייתון 3.10 מוסיפה מספר תיקונים ופיצ'ר אחד של Structural Pattern Matching שמשנה את התחביר של השפה ומרגיש כמו משהו שאפשר יהיה להשתמש בו לעתים קרובות בקוד אמיתי.