פיתרון פונקציונאלי ל Advent Of Code יום 8 חלק 1 בפייתון
החלק הראשון של יום 8 ב Advent Of Code היה ממש פשוט וכלל כמה טיפים לגבי פונקציית reduce בפייתון.
טיפים קצרים וחדשות למתכנתים
החלק הראשון של יום 8 ב Advent Of Code היה ממש פשוט וכלל כמה טיפים לגבי פונקציית reduce בפייתון.
הסתכלתי היום ברשימת החידושים של רובי 3.3, וכמו בהרבה מקרים של גירסאות חדשות של שפות תכנות, ולמרות שממש חיפשתי, לא היה שם אפילו פיצ'ר אחד שבשבילו היה שווה לשדרג.
וכן "שווה" לשדרג, כי לשדרג זה לא חינם. במקרה הכי טוב זה דורש התקנה של סביבת פיתוח חדשה, הרצה של סט בדיקות, ואז התקנה של הגירסה החדשה על השרת והעלאת גירסה. במקרה היותר נפוץ חלק מהבדיקות ייכשלו ונצטרך לתקן כמה באגים, ואחרי זה על השרת חלק מהדברים לא יעבדו ונצטרך להעלות תיקונים דחופים. כן יש עלות בשידרוג.
בנוסף יש עלות לימוד בשימוש בפיצ'רים החדשים, ואפילו אחרי הלימוד ייקח זמן להתרגל אליהם. אז כן תודה שהוספתם פונקציית overlap?
ל Range, אבל בכנות אני לא ממש צריך אותה ואם הייתי צריך כבר היה לי משהו משלי שעושה את זה.
ומכאן צריך לבחור דרך פעולה-
או ששידרוג זה משהו שעושים רק כשיש סיבה טובה, למשל כי איזה ספריה שאני צריך לא עובדת עם השפה בגירסה שיש לי.
או ששידרוג זה משהו שעושים כל X זמן בשביל לאפשר תמיד להשתמש בפיצ'רים שראיתי בתיעוד וב Best Practices הכי מודרניים, כשיתחשק לעשות את זה.
הבעיה בגישה הראשונה היא שכשכבר מגיע הצורך לשדרג, העלות היא מאוד גבוהה כי אין לנו מנגנון שידרוג וכי צריך להתקדם מספר גירסאות.
הבעיה בגישה השניה היא שצריך לעשות דברים גם כשלא מתחשק וקשה לראות את הערך שלהם.
אז כן אני שמח היום שאני יכול לכתוב ברובי קוד כזה:
3.1.1 :008 > x = 10
=> 10
3.1.1 :009 > y = 20
=> 20
3.1.1 :010 > data = {x:, y:}
=> {:x=>10, :y=>20}
אבל כשיצאה רובי 2.7 והייתי צריך להתרגל לכתיב הזה זה גם נראה לי מוזר.
שידרוג תוכנה, כמו הרבה דברים בחיים, הוא מהדברים האלה שאף פעם לא מתחשק לעשות אבל אתה תמיד שמח אחרי שהם נעשו.
הפוסט היום הוא יותר שאלה מאשר טיפ. יש לי כיוון איך לפתור את הבעיה אבל אשמח לשמוע דעות נוספות כי אני לא מרוצה ממנו, והסיפור מתחיל בדקורטורים ושיתוף מידע ביניהם לבין קוד של התוכנית הראשי.
בקריאת קוד, המחשבה "אין לי מושג למה כתבתי את זה ככה" יכולה להיות מדכאת. היא יכולה לרמוז שחלקים במערכת עובדים בגלל צירוף מקרים גלקטי, שהקוד שלי הוא לא באמת שלי ואולי אין לי את הכישורים והיכולות שצריך בשביל העבודה הזאת. או יותר גרוע, לייצר מעין שכנוע עצמי שכולם פשוט מדביקים כל היום קוד ש AI מייצר בלי להבין אותו ולכן אין סיבה להתאמץ.
במקום ליפול לבור הזה, בואו נשתמש ב "אין לי מושג למה כתבתי את זה" בתור דלת ששווה לעבור דרכה כדי ללמוד יותר על איך מערכות עובדות. איך לי מושג למה כתבתי את זה, ולכן אנסה-
למחוק את הפונקציה ולשכתב אותה מחדש בדרך הגיונית עבורי.
לבקש מחבר או מ AI להסביר לי את המנגנון, ואז להציע עוד כמה דרכים לכתוב את אותו דבר.
לכתוב מספיק בדיקות כדי לשבור את הקוד, ולהבין איך הוא נשבר ומתי הוא עובד.
נ.ב. הנה גם רעיון למוצר AI קטן - תוסף ל IDE שמזהה ומסמן קטעי קוד שנכתבו בלי הבנה מספיק טובה. אני בטוח הייתי שמח לנסות אותו על כמה פרויקטים שלי.
את יום 5 של Advent Of Code השנה לקח לי הרבה זמן לעבד, למרות שבסופו של דבר הפיתרון לא היה יותר מדי מסובך. כמו תמיד בסידרה הזאת נתחיל עם תיאור התרגיל ואז פיתרון בשפת סקאלה, עם כמה מילים על סקאלה עצמה.
אם יש דבר שהמוח האנושי שונא יותר מכל זה להודות בהפסד, מה שגורם בצורה אבסורדית דווקא ליותר הפסדים או הפסדים יותר גדולים.
כמו מהמר שלא מוכן להודות בהפסד ורק מעלה את סכום ההימור, כך מתכנתים עם כוונות טובות מצליחים להמשיך לעבוד על פיצ'ר שהוא בעצם חור שחור, בלי להבין שהקוד שכבר כתבנו הוא המלכודת ושום דבר טוב לא יצא מהסבך הזה.
יש ימים שהדבר הכי טוב שאפשר לעשות בשביל הפרויקט הוא git stash. כן סטאש, לא ליצור איזה בראנץ שעוד חודשיים מישהו יבוא ויציע "להחיות את הפיצ'ר", סטאש שלא רואים (האמיצות יכולות לנסות git restore, אבל לי אף פעם לא היה אומץ), ושלא מספרים עליו לאף אחד.
לקבור את הפיצ'ר, להוציא את הגירסה ולהמשיך הלאה. בספרינט הבא אולי נבנה את זה מחדש מאפס בדרך אחרת.
הלינטר של פייתון התרגז עליי היום כי כתבתי קוד כזה:
if type(x) == int:
...
ולא, לא עניין אותו אם אני משתמש ב type או ב isinstance הפעם ובכל מקרה באמת רציתי לבדוק את ה type. מה שהפריע לו היה דווקא ה ==
, כי בעולם של הלינטר השוואה בין טיפוסים תעבוד טוב יותר עם is, כלומר זה הקוד שהייתי צריך לכתוב:
if type(x) is int:
...
כי בעבודה עם טיפוסים זהות ושיוויון זה אותו דבר.
נו, הלכתי לבדוק אם יש הבדל בזמן ריצה בין שתי האפשרויות:
In [2]: %timeit type(8) == int
15.6 ns ± 0.091 ns per loop (mean ± std. dev. of 7 runs, 100,000,000 loops each)
In [3]: %timeit type(8) is int
13.4 ns ± 0.0661 ns per loop (mean ± std. dev. of 7 runs, 100,000,000 loops each)
וכן יש, אבל כל כך קטן שזה לא באמת משנה, מה שהופך את השאלה לעניין של טעם. ומאחר ועל טעם ועל ריח לא מתווכחים עם PEP8 אנחנו נשארים עם is כדי להשוות טיפוסים, כמו גם השוואות ל None.
נ.ב. במקומות יותר מעניינים השוואה עם is או ==
יכולה להיות בעלת משמעות, למשל בעבודה עם רשימות:
In [14]: [1, 2, 3] == [1, 2, 3]
Out[14]: True
In [15]: [1, 2, 3] is [1, 2, 3]
Out[15]: False
אבל במקרים כאלה בדרך כלל נשקיע יותר מחשבה בבחירת האופרטור הנכון שיתאים למה שהתוכנית אמורה לעשות.
הדוגמה היום היא בפייתון אבל הקונספט נכון בכל שפה. הקוד הבא הוא מימוש למשחק ניחוש מספרים. נכון הוא לא מתוחכם במיוחד אבל יעבוד בשביל הדוגמה. המחשב מגריל מספר סודי, אתה מנסה לנחש אותו והמחשב עונה ב"גדול מדי" או "קטן מדי" עד שמגיעים למספר הנכון. זה קוד שעובד:
def main():
secret_number = random.randint(1, 100)
while True:
next_guess = int(input("Next guess: "))
if next_guess == secret_number:
print("Bravo! You guessed it")
break
elif next_guess < secret_number:
print("Sorry, too small")
else:
print("Sorry, too large")
כל מה שצריך בשביל לבדוק את הקוד הוא לכתוב כמה Mock-ים, למשל לדרוס את input ואת random.randint ואז להסתכל מה הודפס. למשל הבדיקה הבאה מוודאת שכשמשתמש מנחש את המספר התוכנית תדפיס את ההודעה הנכונה ותצא מהלולאה:
def test_stop_when_guess_is_correct(capsys):
with patch('builtins.input', new=lambda _: "10\n"):
with patch('random.randint') as randint:
randint.return_value = 10
main()
assert capsys.readouterr()[0] == "Bravo! You guessed it\n"
זה נהיה קצת יותר מסובך כשרוצים לוודא את התשובה של המשחק לניחוש גבוה מדי:
def test_says_too_small_when_guessing_small_number(capsys):
with patch('builtins.input') as fake_input:
with patch('random.randint') as randint:
fake_input.side_effect = ["10\n"]
randint.return_value = 30
try:
main()
except StopIteration:
pass
assert capsys.readouterr()[0] == "Sorry, too small\n"
בתוכנית אמיתית עם הרבה יותר תלויות מערכת ה Mock-ים תצא מהר משליטה. הבדיקות יהיו מסובכות ויקשו על ריפקטורינג של הקוד, עד שבדיקות יתחילו להיכשל בצורה ספונטנית.
עכשיו ננסה תרגיל אחר והוא לארגן מחדש את הקוד כדי שלא נצטרך Mock-ים. בדיקות טובות עשויות להיראות כך:
def test_equal():
assert game.check_user_input(20, "20\n") == game.TEXT['=']
def test_too_large():
assert game.check_user_input(20, "50\n") == game.TEXT['>']
def test_too_small():
assert game.check_user_input(20, "10\n") == game.TEXT['<']
def test_stop_when_equal(capsys):
game.game(50, ["20\n", "40\n", "50\n", "80\n"])
assert capsys.readouterr()[0] == f"""{game.TEXT['<']}
{game.TEXT['<']}
{game.TEXT['=']}
"""
נשים לב שכל בדיקה הפעם בודקת בדיוק את מה שהיא צריכה בלי למלא את הפונקציה ב Boilerplate של דריסות. והקוד עצמו? הנה הגירסה החדשה שעושה בדיוק את אותו דבר ומתאימה לקוד הבדיקות החדש:
from typing import Callable
import random
import itertools
TEXT = {
"=": "Bravo! You guessed it",
"<": "Sorry, too small",
">": "Sorry, too large"
}
MIN = 1
MAX = 100
def check_user_input(secret_number: int, user_value: str) -> str:
if int(user_value) == secret_number:
return TEXT['=']
elif int(user_value) < secret_number:
return TEXT['<']
else:
return TEXT['>']
def input_stream():
while True:
yield input("Next Guess: ")
def secret_number_generator():
return random.randint(MIN, MAX)
def game(secret_number, inputs):
responses = (check_user_input(secret_number, i) for i in inputs)
for response in responses:
print(response)
if response == TEXT['=']:
break
if __name__ == "__main__":
game(secret_number_generator(), input_stream())
ונכון זה קוד של 41 שורות במקום 18. ואני מכיר גם את הטיעון שקל יותר לתחזק קוד קצר מאשר קוד ארוך, והוא נכון במידה מסוימת. הבעיה שאחרי שהקוד עובר אורך מסוים הוא כבר לא נחשב "קצר". קוד של 20,000 שורות עם בדיקות וחלוקה נכונה לפונקציות הוא יותר קל לתחזוקה מאשר קוד של 10,000 שורות מבולגנות.
כן אני מבין שיש מספרים גדולים ומספרים קטנים ושמספרים עד גודל מסוים קל לייצג בצורה מסוימת והחל מהגודל הזה המחשב צריך לייצג את המספרים אחרת. אבל עם שפות כמו פייתון וטייפסקריפט שכובשות את העולם סקאלה לא יכולה להמשיך לסבך אותי עם גדלים של מספרים.
הקוד של הקיטור היום מתחיל עם רעיון פשוט שנקרא Range:
scala> 10.until(20)
val res0: Range = Range 10 until 20
עם Range אפשר לעשות המון דברים למשל לגלות מה הסכום:
scala> 10.until(20).sum
val res2: Int = 145
עכשיו ננסה את אותו דבר עם מספרים גדולים:
scala> 2728902838.until(4728902838)
-- Error: ----------------------------------------------------------------------
1 |2728902838.until(4728902838)
|^^^^^^^^^^
|number too large
-- Error: ----------------------------------------------------------------------
1 |2728902838.until(4728902838)
| ^^^^^^^^^^
| number too large
נו ברור שאי אפשר לכתוב מספר גדול בתור ליטרל, אז אני מוסיף לו l קטנה כדי שיהיה Long, ואז מקבל הודעת שגיאה אחרת:
scala> 2728902838.until(4728902838l)
-- Error: ----------------------------------------------------------------------
1 |2728902838.until(4728902838l)
|^^^^^^^^^^
|number too large
-- [E007] Type Mismatch Error: -------------------------------------------------
1 |2728902838.until(4728902838l)
| ^^^^^^^^^^^
| Found: (4728902838L : Long)
| Required: Int
|
| longer explanation available when compiling with `-explain`
הפעם הודעת השגיאה כבר רומזת על הצעד הבא, ואני הופך גם את המספר הראשון ל Long בשביל לשמח את סקאלה:
scala> 2728902838l.until(4728902838l)
val res4: scala.collection.immutable.NumericRange.Exclusive[Long] = NumericRange 2728902838 until 4728902838
עובד? לא בדיוק. נשים לב שפקודת until של Int-ים החזירה משהו מטיפוס אחד ו until של Long-ים החזירה משהו מטיפוס אחר. ולמה זה מפריע? נמשיך לניסוי הבא עם רשימות:
scala> val l = List(a, b)
val l:
List[scala.collection.immutable.AbstractSeq[Long | Int] &
IndexedSeq[Long | Int]] = List(NumericRange 2728902838 until 4728902838, Range 10 until 20)
scala> a.start
val res0: Long = 2728902838
scala> l.head.start
-- [E008] Not Found Error: -----------------------------------------------------
1 |l.head.start
|^^^^^^^^^^^^
|value start is not a member of scala.collection.immutable.AbstractSeq[Long | Int] & IndexedSeq[Long | Int] - did you mean scala.collection.immutable.AbstractSeq[Long | Int] & IndexedSeq[Long | Int].last?
1 error found
למרות שגם ל a וגם ל b יש פונקציה start, ברגע שאני שם אותם יחד ברשימה אני מקבל רשימה של דברים יותר כלליים ולהם אין את הפונקציה start, ובאופן כללי אין יחסי ירושה בין שני סוגי הטווחים. וכן אני כנראה יכול להשתמש ב Structural Types בשביל לעקוף את הסיפור הזה אבל צריך לזכור מאיפה התחלנו - כל מה שרציתי היה לקבל פונקציות בסיסיות של טווח על מספרים שיעבדו אותו דבר למספרים גדולים וקטנים.
שורה תחתונה המאזן בין Int ל Long הוא בין יעילות (של Int) לגנריות (של Long). אנחנו ב 2024 והגיע הזמן ש Long תהיה ברירת המחדל.
חבר שואל - אני יוצא לריצה פעם בשבוע כבר כמה חודשים ועדיין הריצות קשות לי ואני לא מרגיש שיפור, כל ריצה מרגישה כמו הפעם הראשונה. מה עושים?
ופה יש רק שתי תשובות אפשריות:
או
עכשיו רק צריך להבין למה רוב הזמן אנחנו מתנהגים כאילו האפשרות הראשונה היא היותר הגיונית.