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

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

שתי שיטות ערבוב ב 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 ושווה לבדוק אותה לפני שמתחילים לבנות מנגנון לבד בעולם האמיתי.

מה ההבדל בין git rm cached ל git reset

30/12/2019

הגיעה אליי שאלה מעניינת הבוקר במייל וחשבתי לשתף את התשובה כאן לטובת כולם. והשאלה: מה ההבדל בין הפקודה git rm --cached לבין הפקודה git reset, מתי אשתמש בכל אחת?

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

הפקודה git add לוקחת קובץ מתיקיית העבודה ומעתיקה אותו לאותה תיקיה זמנית של "הכנה לקומיט" שקראתי לה Staging Area. הפקודה git reset לוקחת קובץ מהריפוזיטורי ומעתיקה אותו לאותה תיקיה זמנית. לכן אפשר לחשוב על שתי פקודות אלה בתור הפכים. נכין רגע מאגר לדוגמא:

$ mkdir demo
$ cd demo
$ git init
$ echo one > demo.txt
$ git add demo.txt
$ git commit -m 'commit 1'

אז כשאני יוצר קובץ חדש בתיקיית העבודה אני יכול להשתמש ב git add כדי להוסיף אותו לתיקיה הזמנית:

$ echo two > two.txt
$ git add two.txt 
$ git ls-files --stage
100644 5626abf0f72e58d7a153368ba57db4c673c0e171 0   demo.txt
100644 f719efd430d52bcfc8566a43b2eb655688d38871 0   two.txt

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

$ git reset two.txt
$ git ls-files --stage
100644 5626abf0f72e58d7a153368ba57db4c673c0e171 0   demo.txt

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

לעומתה הפקודה git rm הרבה יותר דומה לפקודה git add, רק שבעוד ש git add מעתיקה קובץ מתיקיית העבודה ל Staging Area, תפקיד הפקודה git rm הוא להעתיק את ה"אין קובץ", כלומר למחוק קובץ מה Staging Area.

נזכור שהדרך הרגילה שלנו להוציא קובץ מה Staging Area היא לדרוס את הקובץ ולהחליף אותו בגירסא ששמורה בריפוזיטורי, וזה מה שעושה git reset. אבל אם אנחנו רוצים למחוק קובץ לחלוטין זה לא באמת יעזור להעתיק את הגירסא מהריפוזיטורי. במילים אחרות בדוגמא שלנו הקובץ demo.txt נמצא ב Staging Area וגם ב Repository. אם אפעיל git reset עליו לא יקרה כלום:

$ git ls-files --stage
100644 5626abf0f72e58d7a153368ba57db4c673c0e171 0   demo.txt

$ git reset demo.txt
$ git ls-files --stage
100644 5626abf0f72e58d7a153368ba57db4c673c0e171 0   demo.txt

בכל מקרה הוא נשאר ב Staging Area וייכנס לקומיט הבא. הפקודה git rm היא הדרך שלנו להעתיק את העובדה שהקובץ לא קיים אל ה Staging Area. שימו לב להשפעה שלה על המאגר שלנו:

$ git rm demo.txt
$ git ls-files --stage
$ ls
two.txt

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

ומה לגבי git rm --cached ? אם הגעתם עד לפה הסיפור שלו הוא הכי פשוט מכולם. לפעמים אנחנו תקועים עם קובץ או תיקייה שאנחנו רוצים להוציא מהריפוזיטורי אבל להשאיר אצלנו בתיקיית העבודה. דוגמא קלאסית היא תיקיית node_modules: בטעות הוספנו תיקיה זו למאגר ועכשיו אנחנו רוצים להוציא אותה, אבל התיקיה כן עוזרת לי בעבודה השוטפת ולכן אני לא רוצה למחוק אותה מהדיסק. הפעלת git rm -r node_modules לא באה בחשבון כיוון שהיא תביא למחיקת התיקיה מהדיסק. אבל, אם נוסיף את --cached נקבל גירסא של git rm שלא מוחקת את הקובץ או התיקיה מהדיסק אלא רק מה Staging Area.

בחזרה למאגר הדוגמא נחזיר את הקובץ demo.txt מהריפוזיטורי עם:

$ git restore -s HEAD demo.txt
$ git ls-files --stage
100644 5626abf0f72e58d7a153368ba57db4c673c0e171 0   demo.txt

ועכשיו נמחק אותו רק מה Staging Area בלי למחוק אותו מתיקיית העבודה:

$ git rm --cached demo.txt
$ ls
demo.txt two.txt
$ git ls-files --stage

למה (ואיך) להמשיך ללמוד טכנולוגיה לאורך שנים

29/12/2019

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

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

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

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

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

אבל- איך עושים את זה? מהי אותה קומה שניה? ואיך אני יודע שאני לא עושה פשוט "עוד מאותו דבר?". הנה שלושה טיפים שעוזרים לי להתמיד:

המשך קריאה

מנגנוני ניהול זיכרון ב Python

28/12/2019

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

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

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

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

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

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

a = []
a.append(a)

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

>>> len(a)
1
>>> len(a[0])
1
>>> len(a[0][0])
1
>>> len(a[0][0][0])
1
>>> len(a[0][0][0][0])
1
>>> len(a[0][0][0][0][0])
1

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

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

>>> a = []
>>> sys.getrefcount(a)
2

>>> b = {}
>>> sys.getrefcount(b)
2

>>> a = 10
>>> sys.getrefcount(a)
12

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

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

>>> a = 19821
>>> sys.getrefcount(a)
2

עכשיו אפשר לחזור למעגל שלנו:

>>> a = []
>>> a.append(a)
>>> sys.getrefcount(a)
3

לאוביקט יש הפעם 3 שמות: השניים הראשונים מוכרים לנו מהדוגמאות הקודמות והשם השלישי הוא ההצבעה הפנימית. מנגנון Reference Count לעולם לא יוכל למחוק משתנה זה מהזיכרון כי מספר השמות שלו לא ירד אף פעם ל-0. ההצבעה הפנימית תימחק רק כשנאפס אותה ידנית.

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

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

import sys, gc
a = []
a.append(a)
ida = id(a)
a = 10

# is there still an object with id == i ?
>>> ida in [id(x) for x in gc.get_objects()]
True

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

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

>>> gc.collect()
1

>>> ida in [id(x) for x in gc.get_objects()]
False

ועכשיו הרשימה עם המעגל נמחקה מהזיכרון.

בזכות השילוב בין שני המנגנונים הרבה יותר קשה לייצר זליגות זיכרון בתוכניות פייתון. כדאי לשים לב עם זאת שמנגנון ה Garbage Collector מתעורר בזמנים לא צפויים ועלול לפגוע בביצועים של התוכנית אם אנחנו רוצים לכתוב תוכנית שתגיב בזמן אמת לאירועים חיצוניים. לכן קיימת בפייתון הפקודה gc.disable שמבטלת את מנגנון ה Garbage Collection ומשאירה אותנו רק עם Reference Count. אם אתם בטוחים שאין לכם הצבעות מעגליות באפליקציה וממש צריכים ביצועי Real Time אולי זה משהו ששווה לבדוק אותו.

לא רוצה, לא יכול ולא יודע איך

27/12/2019

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

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

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

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

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

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

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

שגיאות זה לא אוכל

26/12/2019

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

  def self.track_event(user_token)
    return if user_token.nil?

    session = UserSession.active.find_by(token: user_token)
    session&.update_attribute('last_seen', Time.now)
  end

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

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

אגב האופרטור &. אומר שאם האוביקט אינו nil אז צריך להפעיל את הפונקציה, אבל אם הוא כן nil אז לא עושים כלום. האופרטור נקרא Optional chaining וגירסא שלו תגיע בקרוב גם ל JavaScript.

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

קוד סטארטר לפיתוח Dashboard ב React

25/12/2019

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

המשך קריאה