שלום אורח התחבר

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

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

מעדיפים לקרוא מהטלגרם? בקרו אותנו ב:@tocodeil

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

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

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

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

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

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

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

מסלול בוטקמפ ללימוד ריאקט ייפתח בתאריך 14.06.2020 למשך שלושה שבועות. מפגשי העבודה יתקיימו בימי ראשון ורביעי בין השעות 9:00 ל-11:00 בבוקר בתאריכים: 14/06, 17/06, 21/06, 24/06, 28/06, 01/07. מחיר הקורס 970 ש"ח וזה כולל חודש מנוי לתקופה של הקורס (אם אתם כבר מנויים החודש הזה יתווסף לכם אוטומטית לתקופת המנוי). הצטרפות לבוטקמפ בקישור: https://www.tocode.co.il/quickjoin2?term=15.

מסלול בוטקמפ ללימוד פייתון יפתח בתאריך 14.06.2020 למשך שלושה שבועות. מפגשי העבודה יתקיימו בימי ראשון ורביעי בין השעות 13:00 ל-15:00 בצהריים בתאריכים: 14/06, 17/06, 21/06, 24/06, 28/06, 01/07. מחיר הקורס 970 ש"ח וזה כולל חודש מנוי לתקופה של הקורס (אם אתם כבר מנויים החודש מתווסף אוטומטית לתקופת המנוי). הצטרפות לבוטקמפ פייתון בקישור: https://www.tocode.co.il/quickjoin2?term=16.

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

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

המשך קריאה...

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

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

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

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

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

הספריה pytest-asyncio היא פלאגין ל pytest שמוסיפה אפשרות לסמן בדיקות או Fixtures בתור בדיקות אסינכרוניות ובאופן אוטומטי תפעיל את הבדיקות האלה בתור קורוטינות ותעביר להן את ה event loop.

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

@pytest.mark.asyncio
async def test_luke_name(aiosession):
    data = await fetch(aiosession, 'https://swapi.co/api/people/1/')
    assert data['name'] == 'Luke Skywalker'

בדיקה זו משתמשת בפונקציית עזר בשם fetch (אסינכרונית גם היא) וב Fixture אסינכרוני בשם aiosession. הנה הקוד לשניהם:

@pytest.fixture()
async def aiosession():
    async with aiohttp.ClientSession() as session:
        yield session

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.json()

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

@pytest.fixture(scope='session')
def event_loop():
    loop = asyncio.get_event_loop()
    yield loop
    loop.close()


@pytest.fixture(scope='session')
async def aiosession():
    async with aiohttp.ClientSession() as session:
        yield session

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

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

המשך קריאה...

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

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

set a 10

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

set a 10
set b a

ישמור בתא a את הערך 10 ובתא b את הערך ששמור עכשיו ב a, כלומר גם 10.

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

class MyParser:
    # ...
    def set(self, reg, val):
        ival = self.read_register_or_value(val)
        self.memory[reg] = val

    def sub(self, reg, val):
        ival = self.read_register_or_value(val)
        self.memory[reg] -= ival

    def mul(self, reg, val):
        ival = self.read_register_or_value(val)
        self.memory[reg] *= ival

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

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

דרך אחת להפוך את הקוד לקצת יותר נעים לעין היא להמיר את הקריאה היזומה בקריאה ל Decorator:

class MyParser:
    @read_value_from_register
    def set(self, reg, val):
        self.memory[reg] = val

    @read_value_from_register
    def sub(self, reg, val):
        self.memory[reg] -= val

    @read_value_from_register
    def mul(self, reg, val):
        self.memory[reg] *= val

כל מה שצריך בשביל המעבר למבנה החדש הוא להגדיר את הפונקציה read_value_from_register מחוץ למחלקה בתור Decorator:

def read_value_from_register(f):
    def inner(self, reg, val):
        ival = self.read_register_or_value(val)
        f(self, reg, ival)

    return inner

מעניין לשים לב שבמקרה כזה הפונקציה f שעוברת כפרמטר היא עדיין לא Object Method אלא פונקציה פשוטה. היא עדיין לא קשורה לאוביקט מאחר וה Decorator נקרא לפני שנוצרו אוביקטים מסוג MyParser. מסיבה זו בהפעלת הפונקציה אנחנו חייבים להעביר את self בצורה יזומה בתור הפרמטר הראשון. מלבד תשומת הלב המיוחדת ל self, הגדרת Decorator למתודה (שנמצאת בתוך מחלקה) זהה לחלוטין להגדרת Decorator לפונקציה רגילה.

בפייתון PEP 249 מאפיין את הממשק הרצוי לדרייברים לבסיסי הנתונים השונים בפייתון. בואו נראה 3 דוגמאות לעבודה עם בסיס נתונים SQLite כדי לקבל מושג איך הממשק הזה נראה.

המשך קריאה...

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

המשך קריאה...