הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

חדש באתר: קורס ריאקט לייב - לומדים חודשיים עם מרצה ויוצאים עם פרויקט ביד

08/01/2020

הי חברים,

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

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

אני יכול לדלג על קטעים שלא מעניינים אותי.

אני יכול להתאמן בלי שאף אחד מסתכל.

ואני יכול לשאול שאלות בלי להרגיש שרק אני לא הבנתי.

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

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

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

הקורס הראשון יהיה React ומיועד למתכנתים שכבר יודעים לכתוב קוד Front End, מכירים JavaScript ברמה סבירה אבל מכל מיני סיבות לא הצליחו עדיין להיכנס לשוק העבודה למשרות ה Front End הטובות באמת. הקורס ייפתח באמצע מרץ ויימשך חודשיים.

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

לכל הפרטים על קורס ריאקט הקרוב מוזמנים לקפוץ לדף הנחיתה החדש שבניתי בקישור https://www.tocode.co.il/bundles/react

נתראה בקורס ינון

בואו נכתוב קומפוננטה שניתנת לעריכה ב React

07/01/2020

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

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

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

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

function EditableLabel_View(props) {
  const { text, startEdit } = props;
  return (<p onClick={startEdit}>{text}</p>);
}

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

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

function EditableLabel_Edit(props) {
  const { text, setText, done } = props;

  function doneIfEnter(e) {
    if (e.keyCode === 13) {
      done();
    }
  }

  return (
    <input
      type="text"
      value={text}
      onChange={(e) => setText(e.target.value)}
      onBlur={done}
      onKeyDown={doneIfEnter}
      />
  );
}

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

function EditableLabel(props) {
  const [text, setText] = useState(props.text);
  const [edit, setEdit] = useState(false);

  if (edit) {
    return <EditableLabel_Edit text={text} setText={setText} done={() => setEdit(false)} />
  } else {
    return <EditableLabel_View text={text} startEdit={() => setEdit(true)} />
  }
}

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

class App extends React.Component {
  render() {
    return (
      <div>
        <EditableLabel text="Hello world" />
      </div>
    );
  }
}

ואפשר לראות את הקסם כולו בפעולה בקודפן הבא:

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

function makeEditableComponent(View, Edit) {
  return function EditableLabel(props) {
    const [data, setData] = useState(props.defaultValue);
    const [edit, setEdit] = useState(false);

    if (edit) {
      return <Edit data={data} setData={setData} done={() => setEdit(false)} />
    } else {
      return <View data={data} startEdit={() => setEdit(true)} />
    }
  }
}

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

const EditableLabel = makeEditableComponent(EditableLabel_View, EditableLabel_Edit);

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

זה אף פעם לא משתלם להתחיל מאפס

06/01/2020

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

זאת שאלה טובה כי היא מחביאה הרבה ניואנסינים במיוחד המילה "שווה".

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

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

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

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

אבל... אני לא רוצה להיות מפתח Mobile כל החיים אבל ... כולם מדברים על Machine Learning אבל ... שמעתי ש Full Stack זה המקצוע הכי טוב למתכנתים אבל ... בדיקות אוטומציה זה משעמם

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

סדר פעולות

05/01/2020

  1. לבנות משהו פשוט לפי Tutorial.

  2. לבנות משהו קצת יותר גדול לבד.

  3. לקרוא ספר.

  4. לקרוא קוד של אחרים.

  5. לשמוע הרצאות מכנסים ביוטיוב.

  6. לעשות Refactoring לקוד שכבר כתבנו.

  7. לחזור לסעיף (2)

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

אקוסיסטם

04/01/2020

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

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

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

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

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

שתי שיטות ערבוב ב Python

03/01/2020

המודול random של Python מציע שתי דרכים לערבב אלמנטים ברשימה: הראשונה היא הפונקציה sample והשניה הפונקציה shuffle. בואו נראה את ההבדל ומתי נשתמש בכל שיטה.

בפייתון ברוב הפעמים שנרצה לשנות רשימה נשתמש במתודות של אוביקט הרשימה עצמו - לדוגמא arr.append, arr.reverse ו arr.clear. אחת הפקודות המעניינות של רשימות בהקשר זה היא [].sort שממיינת את הרשימה בסדר עולה:

arr = [10, 20, 7, 30] 
arr.sort()
arr

Out[23]: [7, 10, 20, 30]

והחברה שלה היא הפונקציה sorted שמקבלת רשימה ומחזירה רשימה חדשה ממוימת:

arr = [10, 20, 7, 30]
sorted(arr)
Out[30]: [7, 10, 20, 30]

הפקודה arr.sort שינתה את הרשימה, ולעומתה sorted(arr) לא שינתה את הרשימה המקורית ורק החזירה רשימה חדשה.

באותו האופן אבל בתחביר קצת שונה יש לנו שתי דרכים לערבב את האלמנטים ברשימה. כאן במקום להכניס את shuffle כפונקציה של מערך ואת shuffled כפונקציה גלובלית נפרדת בחרו ב Python לייצר שתי פונקציות שונות בתוך המודול random.

הפונקציה הראשונה random.shuffle מקבלת רשימה ומשנה אותה במקום, כלומר:

In [31]: arr = [10, 20, 7, 30]                                           
In [32]: random.shuffle(arr)                                             
In [33]: arr                                                             

Out[33]: [20, 30, 10, 7]

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

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

In [34]: random.sample(arr, k=2)                                         
Out[34]: [10, 7]

In [35]: random.sample(arr, k=3)                                         
Out[35]: [30, 20, 10]

In [36]: random.sample(arr, k=2)                                         
Out[36]: [10, 30]
In [37]: random.sample(arr, k=2)                                         
Out[37]: [7, 30]

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

In [38]: random.sample(arr, k=len(arr))
Out[38]: [30, 7, 10, 20]

ומה לגבי זמן ריצה? לא מפתיע לגלות שהגירסא שמשנה את הרשימה In Place היא קצת יותר מהירה בעבודה על רשימות גדולות:

In [43]: a = [random.randint(0, 100) for _ in range(1_000_000)]          

In [44]: %timeit random.sample(a, k=len(a))                              
629 ms ± 24.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [45]: %timeit random.shuffle(a)                                       
538 ms ± 20.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

אז במי נבחר? כמו הרבה פעמים בתכנות התשובה הנכונה היא "זה תלוי" - אם יש לכם רשימה שערבוב האיברים בה הוא חלק מסדר של כמה פעולות שצריך לעשות ואחרי זה אף אחד לא יצטרך עוד את המידע אפשר להשתמש ב shuffle. לעומת זאת אם אתם צריכים לערבב חלק מרשימה שקיבלתם אבל יש עוד מקומות בקוד שעובדים עם אותו מידע סיכוי טוב שתעדיפו לא לשנות את המידע המקורי ולספוג את הפגיעה הקלה מאוד בזמן הריצה (ממילא כנראה לא תרגישו את זה).

הטעות הכי נפוצה של פרילאנסרים (ואיך להימנע ממנה)

02/01/2020

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

והנקודה היא ההבדל בין פרילאנס ליזם.

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

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

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

שניהם לא בכיוון.

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

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

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

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

מה שטוב לפרויקט

01/01/2020

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

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

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

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

  1. להוסיף לוחות זמנים בחוזה עבודה, עם קנסות על חריגה.

  2. לבחור לקוחות שכבר יש להם מערכת או שיודעים להרים מערכות.

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

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

  5. לשים את הדברים בצורה ברורה על השולחן, כלומר לדבר עם הלקוח כבר משיחת המכירה ולהסביר שהמטרה שלכם היא שתהיה מערכת באוויר עד תאריך עליו תסכמו.

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

על השגיאה got Future attached to a different loop ב Python

31/12/2019

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

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

import aiohttp, asyncio, aiofiles

async def download(url, to):
    print(f"Download {url} to {to}")
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            if resp.status == 200:
                f = await aiofiles.open(to, mode='wb')
                await f.write(await resp.read())
                await f.close()

הפונקציה עובדת ואפילו אפשר להשתמש בה כדי להוריד כמה URL-ים במקביל, למשל הקוד הבא מוריד מספר פוסטים מהבלוג:

async def main():
    await asyncio.wait([
        asyncio.create_task(download('https://www.tocode.co.il/blog/2019-12-git-rm-vs-reset', 'post1.html')),
        asyncio.create_task(download('https://www.tocode.co.il/blog/2019-12-keep-on-learning', 'post2.html')),
        asyncio.create_task(download('https://www.tocode.co.il/blog/2019-12-python-memory-management', 'post3.html')),
        ])

asyncio.run(main())

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

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

# DO NOT USE - CODE WITH BUG
import aiohttp, asyncio, aiofiles

throttle = asyncio.Semaphore(2)

async def download(url, to):
    async with throttle:
        print(f"Download {url} to {to}")
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as resp:
                if resp.status == 200:
                    f = await aiofiles.open(to, mode='wb')
                    await f.write(await resp.read())
                    await f.close()

async def main():
    await asyncio.wait([
        asyncio.create_task(download('https://www.tocode.co.il/blog/2019-12-git-rm-vs-reset', 'post1.html')),
        asyncio.create_task(download('https://www.tocode.co.il/blog/2019-12-keep-on-learning', 'post2.html')),
        asyncio.create_task(download('https://www.tocode.co.il/blog/2019-12-python-memory-management', 'post3.html')),
        ])

asyncio.run(main())

והשגיאה לא איחרה לבוא:

Download https://www.tocode.co.il/blog/2019-12-git-rm-vs-reset to post1.html
Download https://www.tocode.co.il/blog/2019-12-keep-on-learning to post2.html
Task exception was never retrieved
future: <Task finished name='Task-4' coro=<download() done, defined at post.py:5> exception=RuntimeError("Task <Task pending name='Task-4' coro=<download() running at post.py:6> cb=[_wait.<locals>._on_completion() at /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/asyncio/tasks.py:507]> got Future <Future pending> attached to a different loop")>
Traceback (most recent call last):
  File "post.py", line 6, in download
    async with throttle:
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/asyncio/locks.py", line 97, in __aenter__
    await self.acquire()
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/asyncio/locks.py", line 496, in acquire
    await fut
RuntimeError: Task <Task pending name='Task-4' coro=<download() running at post.py:6> cb=[_wait.<locals>._on_completion() at /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/asyncio/tasks.py:507]> got Future <Future pending> attached to a different loop

מה קרה כאן? ההודעה אומרת שיש לנו Future Object שמחובר ל Event Loop אחר. פה המקום להזכיר ש asyncio מאפשר לנו ליצור מספר Event Loops במקביל. כל פעם שאנחנו יוצרים Task או Future Object אותו אוביקט מחובר לאיזושהי Event Loop. הקוד הראשי:

asyncio.run(main())

יוצר את ה Event Loop הראשון במערכת ומריץ בתוכה את הפונקציה main.

בחזרה לשגיאה - אנחנו יכולים לראות שהיא הופיעה רק בקובץ השלישי שאנחנו מנסים להוריד, לכן בוודאות היא הגיעה מה Semaphore, וספציפית מהניסיון להמתין על ה Semaphore (כי שתי הבקשות הראשונות עברו חלק). מזה קל להסיק שהדבר שאנחנו מנסים להמתין עליו אבל לא מחובר ל Event Loop הנוכחי הוא בדיוק ה Semaphore.

ואיך Semaphore (או כל אוביקט אחר) יודע לאיזה Event Loop הוא מחובר? ל asyncio יש פונקציה גלובלית בשם get_event_loop() שמחזירה בדיוק את זה. הבנאי של Semaphore קורא לפונקציה זו ושומר את ה Event Loop. אני מעדכן קצת את הקוד כדי לראות מה ה Event Loop שאותו בנאי יקבל ולהשוות אותה ל Event Loop הראשית של התוכנית:

import aiohttp, asyncio, aiofiles

print("Event Loop when creating the semaphore: ", id(asyncio.get_event_loop()))
throttle = asyncio.Semaphore(2)

async def main():
    print("Event Loop in main()", id(asyncio.get_event_loop()))

asyncio.run(main())

והנה התוצאה:

Event Loop when creating the semaphore:  4516635360
Event Loop in main() 4533746416

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

import aiohttp, asyncio, aiofiles

throttle = None

async def download(url, to):
    async with throttle:
        print(f"Download {url} to {to}")
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as resp:
                if resp.status == 200:
                    f = await aiofiles.open(to, mode='wb')
                    await f.write(await resp.read())
                    await f.close()

async def main():
    global throttle
    throttle = asyncio.Semaphore(2)
    await asyncio.wait([
        asyncio.create_task(download('https://www.tocode.co.il/blog/2019-12-git-rm-vs-reset', 'post1.html')),
        asyncio.create_task(download('https://www.tocode.co.il/blog/2019-12-keep-on-learning', 'post2.html')),
        asyncio.create_task(download('https://www.tocode.co.il/blog/2019-12-python-memory-management', 'post3.html')),
        ])

asyncio.run(main())

נ.ב. הרבה פעמים נרצה מנגנונים יותר מתוחכמים כדי להאט את התקשורת, למשל נרצה להגדיר שלא נוציא יותר מ-5 בקשות במקביל אבל גם בשום מקרה לא נוציא יותר מ 10 בקשות בשניה (גם אם השרת עונה ממש מהר לשאילתות שלנו). ספריה נחמדה שעוזרת ליצור Throttling מתוחכם יותר ל asyncio נקראת asyncio-throttle ושווה לבדוק אותה לפני שמתחילים לבנות מנגנון לבד בעולם האמיתי.