• בלוג
  • הזן של פייתון - מפורש טוב יותר ממובלע

הזן של פייתון - מפורש טוב יותר ממובלע

28/06/2023

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

1. קשירה דינמית של this

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

class Foo {
    constructor(name) {
        this.name = name;
    }

    hello() {
        console.log(`Hello ${this.name}`);
    }
}

ואז להשתמש בו עם:

const f = new Foo("ynon");
f.hello();

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

const x = f.hello;
x();

שמפעילה את הפונקציה עם undefined בתור this ולכן זורקת Exception, או הקריאה:

const bar = { name: 'tony' };
f.hello.call(bar);

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

2. קשירה קשיחה של this עם bind

הפונקציה bind ב JavaScript, או הגדרה של פונקציית חץ, מגדירות חיבור מסוג אחר ל this. נשים לב לקוד הבא:

class Foo {
    constructor(name) {
        this.name = name;
    }

    hello = () => {
        console.log(`Hello ${this.name}`);
    }
}


const f = new Foo("ynon");

const x = f.hello;
x();


const bar = { name: 'tony' };
f.hello.call(bar);

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

3. קשירת פונקציות למחלקות בפייתון

אבל לא באנו לדבר על JavaScript אלא על פייתון - האם פייתון היא באמת יותר Explicit או יותר Implicit בהיבט של קשירת פונקציות למחלקות?

נתחיל בכתיב ברירת המחדל:

class Foo:
    def __init__(self, name):
        self.name = name

    def hello(self):
        print(f"Hello {self.name}")

f = Foo("ynon")
f.hello()

הקוד כמעט מועתק מ JavaScript ועובד אותו דבר. כשננסה להפעיל את הפונקציה דרך משתנה אחר נראה שהפונקציה "מחוברת" למחלקה Foo והקוד עדיין יעבוד:

x = f.hello
x()

4. קשירה חלשה בפייתון

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

class Foo:
    def __init__(self, name):
        self.name = name

    def hello(self):
        print(f"Hello {self.name}")

class Bar:
    def __init__(self):
        self.name = "tony"


f = Foo("ynon")
b = Bar()

x = f.hello.__func__.__get__(b, Bar)
x()

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