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

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

הזדמנויות / דברים יותר דחופים

25/07/2023

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

סדר עדיפויות לעתים נדירות משאיר מקום להזדמנויות.

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

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

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

ובבקשה תשתמשו ב Chat GPT בתרגילים הבאים

24/07/2023

עכשיו כש Chat GPT הוא חלק מהחיים המקצועיים שלנו. ועכשיו כשאנחנו משתמשים ונשתמש בו בעבודה כל יום, בדיוק כמו בגוגל.

למה אנחנו עדיין חוששים להשתמש בו בקורסים? למה אנחנו עדיין נותנים תרגילים ש Chat GPT יפתור טוב יותר מהתלמידים? איך לא הגיע הזמן לעלות שלב?

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

משפט המפתח - בבקשה תיעזרו ב Chat GPT במהלך העבודה על התרגילים הבאים.

טיפ פייתון: ערך אקראי מתוך Literal

23/07/2023

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

וכך הטיפ של היום מתחיל בקוד הבא:

from typing import Literal
CountryCode = Literal["il", "al", "ar", "fr"]

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

from typing import Literal
import random
CountryCode = Literal["il", "al", "ar", "fr"]

def get_country_name(code: CountryCode):
    pass

random_country_code = random.choice(CountryCode.__args__)

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

הסוד לצאת מהמינוס

22/07/2023

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

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

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

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

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

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

ואולי ChatGPT זה כמו מחשבון?

21/07/2023

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

אולי המתכנתת שנעזרת ב Chat GPT היא כמו המתמטיקאית שנעזרת במחשבון? אולי מתכנתים צריכים לדעת רק את ה"מה" ולתת למחשב להבין לבד את ה"איך"? ואולי כך יראה המקצוע שלנו בעתיד?

אולי.

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

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

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

מה זה בכלל send של Generator בפייתון?

20/07/2023

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

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

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

from collections.abc import Generator
from itertools import islice

def fib() -> Generator[int, None, None]:
    a, b = 1, 1
    while True:
        yield a
        a, b = b, a + b

for i in islice(fib(), 10):
    print(i)

לא צריך הרבה בשביל להבין שה int הראשון הוא הטיפוס של הדבר שיוצא מ yield, אבל מאיפה הגיעו שני ה None-ים שאחריו?

אז נכון יש Generators שצריכים את send ו throw ובטח יש סיבות טכניות טובות למה לא להשתמש ב Any כברירת מחדל שם. אבל אין ספק שהיה יותר קל להסביר גנרטורים לאנשים אם הייתי יכול קודם ללמד את yield עם ה Type Hint שלו ורק אחר כך ללמד את send ו throw עם הטיפוסים שלהן.

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

עשר, מאה, אלף

19/07/2023

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

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

נניח שהחלטתם שחשוב לכם להגיע למקום גבוה ברשימה כזאת ואתם מתחילים להגיש PR למאגר ציבורי כל יום. אחרי שנה שלחתם 365 תרומות קוד והגעתם למקום 165 ברשימה. זו עליה של 685 מקומות בשנה - קצב מאוד מורגש. בשנה השניה אתם ממשיכים לתקתק עבודה אבל הפעם עליתם רק למקום ה 85, עלייה של 80 מקומות בלבד. זה הרבה אבל ממש לא ה 685 מקומות שעליתם בשנה הראשונה. ובשנה השלישית באותו קצב צפויה לכם עלייה של 30 מקומות בסך הכל, למקום ה 55.

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

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

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

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

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

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

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

הערך האמיתי של Redux Selectors

18/07/2023

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

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

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

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

function TodoCount() {
  const todos = useSelector(state => state.todos)
  return <p>{todos.length} things to do</p>
}

מבנה טוב יותר ייקח את כל הקריאות האלה ל useSelector לקובץ אחד, קובץ הסלקטורים. שם יהיה לנו למשל:

export const getTodos = state => state.todos;

ובתוך הקומפוננטה נכתוב רק:

function TodoCount() {
  const todos = useSelector(getTodos)
  return <p>{todos.length} things to do</p>
}

אפילו בלי Reselect ובלי ה Memoization הרווחנו המון - אם נקיד לכתוב את כל הסלקטורים במקום אחד, שינוי במבנה הסטייט או שינויי שמות יהיו הרבה יותר קלים למימוש ולא ידרשו שינוי בקומפוננטות.

היה שלום אפולו

17/07/2023

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

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

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

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

היה שלום אפולו ותודה על כל הלינקים.

תבנית לכתיבת בדיקה לבוט ב python-telegram-bot

16/07/2023

ספריית python-telegram-bot מציעה ממשק מאוד מקיף לבניית בוטים לטלגרם. הספריה מגיעה עם אינסוף דוגמאות ותיעוד מפורט, ולמרות זאת הדף על כתיבת בדיקות בויקי שלהם כמעט ריק. אחרי קצת חפירה בקוד שלהם הגעתי לתבנית הבאה לבדיקת יחידה ב pytest לבוט שכתוב ב python-telegram-bot. נתחיל בקוד ואחריו ההסברים:

from telegram import Message, MessageEntity, User, Chat, Update
import pytest
from unittest.mock import MagicMock
from datetime import datetime
import client.telegram.bot as bot

class AsyncMock(MagicMock):
    async def __call__(self, *args, **kwargs):
        return super(AsyncMock, self).__call__(*args, **kwargs)



@pytest.mark.asyncio
async def test_create_user(monkeypatch):
    with monkeypatch.context() as m:
        app = bot.create_application()
        await app.initialize()

        message = Message(
            message_id=0,
            date=datetime.utcnow(),
            chat=Chat(3, "private"),
            from_user=User(first_name='Misses Test', id=123, is_bot=False),
            text="/start",
            entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
                                    offset=0, length=len('/start'))]
        )
        message.set_bot(app.bot)

        send_message = AsyncMock()
        m.setattr(app.bot, "_send_message", send_message)

        await app.process_update(Update(update_id=0, message=message))
        assert 'Hello World' in send_message.call_args[0][1]['text']

והקוד עבור הבוט המתאים, מתוך קובץ שבדוגמה שלנו נקרא client/telegram/bot.py יהיה:

async def start_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text(f"Hello World")


def create_application():
    # Create the Application and pass it your bot's token.
    token = os.environ.get("TELEGRAM_BOT_TOKEN")
    application = Application.builder().token(token).build()

    application.add_handler(CommandHandler('start', start_conversation))
    return application


def main() -> None:
    """Start the bot."""
    application = create_application()
    # Run the bot until the user presses Ctrl-C
    application.run_polling()


if __name__ == "__main__":
    print("Starting...")
    main()

בואו נראה מה היה לנו:

  1. קוד הבדיקה הוא פונקציה אסינכרונית. בשביל שזה יעבוד יש להתקין את הפלאגין pytest-asyncio.

  2. הבדיקה צריכה לזייף הודעת /start ואז צריכה לוודא שהבוט ענה בהודעת Hello World. בשביל זה אנחנו ממש יוצרים את הבוט (תחילת קוד הבדיקה) ומפעילים את app.initialize שזו פונקציה של python-telegram-bot שדואגת לכל מיני איתחולים. בחיבור רגיל לטלגרם קריאה ל run_polling מפעילה את ה initialize אוטומטית.

  3. הפונקציה process_update היא נקודת הכניסה לבוט והיא מקבלת אוביקט הודעה ומנתבת אותו לפונקציית הטיפול המתאימה לפי הכללים שהגדיר הבוט (אלה פקודות ה add_handler שרואים בקוד הבוט). בקוד הבדיקה משתמשים בה כדי לשלוח את הודעת ה /start.

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

  5. הפקודה set_bot היא שמאפשרת בתוך קוד הבוט לגשת ל update.message.reply_text, ולכל שאר הפונקציות של הבוט דרך אוביקט ההודעה.

  6. בשביל לקבל בחזרה לבדיקה את הפרמטרים שנשלחו לפונקציות שליחת ההודעה (אלה שהיו אמורים להישלח לטלגרם בהרצה אמיתית של הבוט), אני יוצר MagicMock ושומר אותו בשדה _send_message של אוביקט הבוט. הפונקציה _send_message נועדה לשימוש פנימי ולכן אפשר לדרוס אותה. ניסיון לדרוס פונקציות ציבוריות של הבוט ייכשל בגלל הקוד הבא בתוך ספריית python-telegram-bot באוביקט הבוט:

def __setattr__(self, key: str, value: object) -> None:
    """Overrides :meth:`object.__setattr__` to prevent the overriding of attributes.

    Raises:
        :exc:`AttributeError`
    """
    # protected attributes can always be set for convenient internal use
    if key[0] == "_" or not getattr(self, "_frozen", True):
        super().__setattr__(key, value)
        return

    raise AttributeError(
        f"Attribute `{key}` of class `{self.__class__.__name__}` can't be set!"
    )
  1. אחרי דריסת _send_message נשאר רק לשלוח את ההודעה ל process_update ולוודא את ההודעה שהבוט שולח דרך בדיקה של הקריאות שבוצעו ל Mock בשורה האחרונה של הבדיקה.