ToCode
Ir al canal en Telegram
1 421
Suscriptores
Sin datos24 horas
+37 días
-430 días
Archivo de publicaciones
1 421
# מבין מה ניסו לעשות שם
גם כשהקוד גרוע.
גם כשהוא לא עובד.
גם כשיש בעיית ביצועים קשה או שכל שינוי קטן דורש ריפקטורינג גדול.
בהתחלה היו מישהו או מישהי או כמה אנשים שחשבו על הדברים, באמת חשבו וניסו לדמיין איך זה יראה ואיזה שינויים יהיו או לא יהיו. היה מישהו שתכנן ומישהי שחשבה שוב, מישהו שנזכר במשהו שהוא למד מספר ומישהי שעבדה בתוך אילוצים שעכשיו אנחנו כבר לא מבינים.
הסוד להבין קוד הוא אמפתיה. להיות מסוגלים להגיד "אני מבין מה ניסו לעשות שם", "אני מבין למה כתבו את זה ככה".
אני מזהה את התבנית הזאת ומאיזה ספר היא הגיעה.
אני רואה את ה Design Pattern שלא ממומש בדיוק לפי הספר אבל גם ברור למה.
אני מבין לאיזה תרחישים הם התכוננו, אפילו שזה לא איך שדברים קרו בפועל.
ברור למה הם בחרו להשתמש במנגנון הזה. גם אני במקומם הייתי בוחר באותו מנגנון אפילו שעכשיו החיים שלי הרבה יותר קשים בגלל זה.
כי כשאנחנו מבינים, או מתאמצים באמת להבין, מה האנשים שהיו לפנינו חשבו, אנחנו מגדילים את הסיכוי להתחמק מטעויות דומות בבחירות שלנו.
1 421
# משולש האימה
אני רוצה לעשות תפקיד משמעותי ולהרוויח הרבה כסף, אז כנראה שאצטרך לעבוד המון שעות.
אני צריך את הזמן בשביל להיות עם המשפחה אבל לא רוצה להתפשר על השכר, אז כנראה שאצטרך להתפשר על העניין בתפקיד.
אני רוצה יותר זמן פנוי אבל לא על חשבון תחושת המשמעות, אז כנראה שאצטרך להרוויח פחות.
יש הרבה דרכים לסובב את האמונה הזו שנקראת "משולש האימה", אבל בגדול היא אומרת שבעבודה אפשר לקבל "כסף", "זמן פנוי" ו-"תחושת משמעות" ואנחנו צריכים לבחור 2 מתוך ה-3.
כמובן שכשיש לנו את משולש האימה בראש הוא הופך לנבואה שמגשימה את עצמה והמוח אלוף בלמצוא ראיות כדי להצדיק את הדברים שהוא כבר חושב. לכן בשביל לקבל את שלושת הדברים במשולש, צריכים קודם לשבור את המחסום הפנימי.
דרך אחת לחשוב על זה היא לשים לב שיש אנשים יותר ופחות פרודוקטיביים, וגם אנחנו בתקופות שונות בחיים היינו יותר או פחות פרודוקטיביים. היו פעמים שבמעט זמן הצלחת לעשות שינוי גדול בפרויקט, ופעמים אחרות שעבדת על משהו שבועות וחודשים עם תחושה של להיתקע במקום. אנשים שהצליחו לעשות משהו בעל משמעות בדרך כלל ניסו (והצליחו) לעשות יותר מדבר אחד כזה בתקופת החיים שלהם.
ועם זה אנחנו יכולים לארגן מחדש את משולש האימה - הקודקוד המוביל הוא עבודה בעלת משמעות, עבודה בה אנחנו משפרים ובונים דברים טובים יותר, בתוך האילוצים ומסגרת הזמן שיש לנו. שני הצירים האחרים (כסף ושעות) מושפעים מהפרודוקטיביות, או המיומנות שלנו. ככל שהעבודה שלי היא בעלת משמעות, ואני עושה אותה במיומנות גבוהה יותר, כך אני יכול בפחות שעות להגיע לתוצאות טובות יותר וגם להרוויח יותר.
1 421
סך הכל העבודה עם קלוז'ר נעימה בזמן כתיבת הקוד, אבל איתור שגיאות וארגון מחדש של התוכנית יכולים להיות מאתגרים. היכרות טובה עם Java והרגלי עבודה טובים (במיוחד כתיבת בדיקות) יכולים לעזור מאוד בעבודה על פרויקטי קלוז'ר גדולים.
1 421
# חמישה דברים שהייתי משנה בקלוז'ר
קלוז'ר היא שפה פונקציונאלית יחסית פשוטה ופופולרית (יחסית לשפה פונקציונאלית) שרצה בתוך ה JVM. בזכות תחביר ליספי ומבני נתונים מגוונים ובנויים בשפה, וכמובן בזכות העובדה שהכל Immutable, הרבה אנשים אוהבים אותה.
אני כתבתי פוסט מבוא לקלוז'ר בעבר כאן:
https://www.tocode.co.il/blog/2019-11-hello-clojure
וגם העברתי יחד עם חבר וובינר על קלוז'ר שמוקלט כאן:
https://www.tocode.co.il/past_workshops/95
אבל למרות כל הדברים הטובים שסיפרתי שם על קלוז'ר, יש גם כמה דברים שמפריעים לי בשפה במיוחד בעבודה על פרויקטים גדולים. ומאחר וריק היקי יוצר השפה בדיוק עזב עכשיו את העבודה בניובנק אולי חלק מהרעיונות פה ימצאו אוזן קשבת ופנויה לשינויים.
## חוסר יכולת להגדיר טיפוסים לפרמטרים וערכי החזר של פונקציות
אופנות משתנות עם הזמן ובתכנות אחת האופנות הכי משמעותיות שהשתנתה בשנים האחרונות היתה חוסר הרצון של מתכנתים להגדיר טיפוסים. אחרי שראינו כמה קל להגדיר טיפוסים בשפה כמו TypeScript אנחנו לא רוצים לחזור אחורה.
היום אנחנו יודעים שקומפיילר יכול להסיק המון מסקנות (Type Inference) גם בשפות דינמיות.
אנחנו יודעים שכתיב הטיפוסים לא צריך להיות מעיק, ולא חייבים לאפיין היררכיות ירושה מסובכות כדי לקבל ביטחון גדול יותר בקוד שלנו.
הבחירה של קלוז'ר להישאר מחוץ למשחק הטיפוסים היא כרגע המחסום המשמעותי ביותר בין קלוז'ר ליישומים גדולים.
## אקוסיסטם
האקוסיסטם של קלוז'ר מורכב ממעט פרויקטים גדולים ומשמעותיים ולצדם הרבה מאוד פרויקטי קוד פתוח שלא ממש מתוחזקים. וזאת בעיה כי בניגוד לפייתון, הרבה דברים בקלוז'ר כן דורשים התקנה של ספריות חיצוניות או שימוש בכלים של Java.
בין הדברים שלא נכללים בספריה הסטנדרטית יש לנו: עבודה עם http, קריאה וכתיבת JSON, CSV ו XML, עבודה עם קבצים מכווצים, חיפוש glob במערכת הקבצים, הצגת ממשק גרפי, עבודה עם אזורי זמן ועוד.
## קשה לדבג קוד פונקציונאלי
מצד אחד מאוד קל לכתוב קוד קלוז'ר כי העבודה דרך ה REPL אומרת שאנחנו כל הזמן "מנסים" דברים עד שמשהו עובד, אבל מצד שני אחרי שיש לי כבר פונקציה ומשהו בהפעלה לא עובד אני צריך להתאמץ כדי להבין מה קרה שם. לדוגמה נניח שיש לי פונקציה שמקבלת מידע על משתמש ומחזירה אם כתובת המייל שלו חוקית, משהו כזה:
(defn is-valid-email [user]
(let [email (:email user)]
(re-matches #".+\@.+\..+" email)))
וגיליתי שמשהו בה לא נכון, אז בשביל להפעיל אותה אני צריך שיהיה לי מילון של user. עכשיו נכון בדוגמה הקטנה זה משהו שמאוד קל ליצור, אבל בדוגמאות יותר גדולות כש user יכול להיות אוביקט שמייצג שאילתה מבסיס נתונים או קובץ אז יהיה יותר קשה ליצור אחד רק בשביל לנסות את הפונקציה.
ונכון תיאורטית אפשר תמיד לפרק את הקוד ליותר פונקציות קטנות שאת הפרמטרים שלהן יהיה קל ליצור בתוכנית בדיקה, אבל בעולם האמיתי כשמגיעים כבר לקוד עם תקלה היית רוצה פשוט לשים נקודת עצירה ולראות מה הערכים או להוסיף הודעות הדפסה, ובקוד ליספי לא תמיד יש איפה.
## צ'ט ג'י-פי-טי לא ממש יכול לעזור
בדומה לאקוסיסטם, גם התיעוד על קלוז'ר ברשת עדיין לא מספיק משמעותי ולכן כשאתם נתקעים קשה למצוא תשובות ב Stack Overflow ו Chat GPT לא ממש מצליח לעזור.
זה אולי לא ממש אשמת קלוז'ר אבל זה בהחלט משהו לקחת בחשבון לפני שבוחרים בה לפרויקט גדול.
## רצפים עצלים
אחת התכונות היפות של קלוז'ר היא גם אחת המבלבלות - וזה ה Lazyness של השפה. שימו לב לתוכנית הבאה:
(defn sum-numbers [nums]
(map println nums) ;; Let's print each number for debugging.
(reduce + nums))
(sum-numbers (range 10))
;; => 45
;; But where are the printlns?
הקוד מחזיר 45 כצפוי, אבל לא מדפיס את כל המספרים שקיבל כי map מחזירה Lazy Sequence שאף אחד לא משערך. גם התוכנית הזאת יכולה להפתיע כי למרות ה catch היא תזרוק Exception:
(defn safe-invert-seq [s]
(try (map #(/ 1 %) s)
;; For the sake of the example, let's return an empty list if we
;; encounter a division by zero.
(catch ArithmeticException _ ())))
(safe-invert-seq (range 10))
שוב בגלל שהשיערוך של המידע קורה אחרי שיצאנו מה try. כאן יש מאמר מאוד מקיף עם עוד הרבה דוגמאות כאלה.1 421
# יש לי רעיון מבריק!
״שומע יש לי פה קוד פייתון שסופר כמה פעמים פונקציה מסוימת נקראה בעזרת משתנה גלובאלי מסוג מספר. אני צריך להוסיף הדפסה כל פעם שמעדכנים את הערך״
״מה הבעיה? רק צריך לרשת מ int ולהוסיף ל
__add__ פקודת הדפסה. משהו כזה-
class LoggedAddInt(int):
def __new__(cls, *args, **kwargs):
args_without_name = {k: v for k, v in kwargs.items() if k != 'name'}
return super().__new__(cls, *args, **args_without_name)
def __init__(self, *args, **kwargs):
self.name = kwargs.get('name', 'New Value')
def __add__(self, other):
res = LoggedAddInt(super().__add__(other), name=self.name)
print(f"{self.name}: {res}")
return res
counter = LoggedAddInt(0, name="counter")
def do_something():
global counter
counter += 1
do_something()
do_something()
do_something()
וכן זה עובד, וכן איזה כיף שאנחנו יודעים ירושה והכל, אבל בעיניי קצת יותר מדי מבריק. הסיכוי לשימוש חוזר במנגנון כל כך ספציפי נמוך, וכמות הקוד שמחליפה הודעת הדפסה בודדת פשוט לא שווה את זה.
אבל בואו ניקח את זה עוד צעד קדימה - מה אם יש לי במערכת מאות מקומות בהם מעדכנים את המשתנה הזה? עכשיו להתחיל להוסיף הודעת הדפסה לכולם? ואחרי זה לחזור לעדכן כל פעם שנצטרך הודעה אחרת או מנגנון אחר? נו, סוג של.
מצד אחד אם צריך להוסיף הודעת הדפסה במאות מקומות במערכת שמעדכנים משתנה מסוים זה אומר שהמשתנה הזה מלכתחילה לא צריך להיות int פשוט, ועדיף לכל הצדדים שאותם מקומות במערכת ישתמשו בהפעלת מתודה בצורה מפורשת במקום באופרטור +=. במילים אחרות קלאס חדש שלא קשור בכלל ל int יעבוד יותר טוב ויראה כך:
class NotifyingCounter:
def __init__(self):
self.value = 0
def inc(self):
self.value += 1
print(f"counter: {self.value}")
counter = NotifyingCounter()
def do_something():
counter.inc()
do_something()
do_something()
do_something()
והאם זה הגיוני לשנות בכל המערכת את הקריאות לפורמט החדש? בטח. במבנה כזה הרבה יותר ברור מה אפשר ומה אי אפשר לעשות עם counter, מתי הוא ידפיס ואם נצטרך בעתיד לשנות שוב את ההתנהגות כשמוסיפים 1. מבנה כזה גם יותר קל לתחזוקה בהשוואה למחלקה שיורשת מ int. נכון ירושה מ int היתה חוסכת לנו לשנות קוד בתוך הפונקציות, אבל בטווח הרחוק זה בסך הכל האק, חיסכון בעבודה היום שעוד נתחרט עליו בעתיד.1 421
# אנשים מתים
הספר The Art Of Deception היה אחד הטובים והמשפיעים שקראתי. בספר קווין מיטניק מתאר דרך דיאלוגים מפורטים ואמינים איך אנחנו תמיד החוליה החלשה באבטחת מידע. הספר לימד אותי חשדנות בריאה ואת הערך של פרטיות.
ההרצאה שבעה הרגלים של עריכת טקסט אפקטיבית של בראם מולנאר לימדה אותי לא רק על וים אלא על הערך של שליטה בעורך הטקסט שלך והחשיבות של הקלדה אפקטיבית.
שניהם מתו צעירים בשבועות האחרונים וזו הזדמנות טובה להיזכר או להכיר, אם במקרה פספסתם.
1 421
# טיפ HTML: גם כפתור הוא קלט
כשאנחנו כותבים ב HTML אלמנט טופס והאלמנט הזה מכיל כפתורים, לכל כפתור יש מאפיין בשם type. אם לא נרשום אותו רוב הדפדפנים ישתמשו בערך submit בתור ברירת מחדל, מה שאומר שלחיצה על הכפתור תגיש את הטופס.
אבל מה עושים אם יש מספר כפתורים שכל אחד מהם צריך לגרום לפעולה אחרת?
אז פה תשמחו לשמוע שלכל כפתור אפשר לכתוב גם מאפיין name, והערך שנרשום ב name יישלח לשרת בעת הגשת הטופס דרך אותו כפתור. דוגמה? בטח. הנה טופס עם שני כפתורים:
<form method="POST" action="/news">
<button name="like" value="like-value" >Like</button>
<button name="dislike" value="dislike-value" >Dislike</button>
</form>
לחיצה על כל אחד מהכפתורים תשלח לשרת בקשת POST לנתיב /news. בקוד השרת אני יכול להסתכל על הפרמטרים שהגיעו כדי להבין איזה כפתור נלחץ: לחיצה על כפתור like תשלח פרמטר בשם like עם הערך like-value. לחיצה על כפתור dislike תשלח פרמטר בשם dislike עם הערך dislike-value.
בקוד צד השרת אני יכול לטפל בפרמטר שנכנס באופן הבא:
let likes = 0;
router.post('/news', function(req, res, next) {
console.log(req.body);
const {like, dislike} = req.body;
if (like) {
likes += 1;
} else if (dislike) {
likes -= 1;
}
res.render('index', { title: 'Express POST /news', likes});
});
נ.ב. במצב שיש מספר כפתורים שצריכים להשפיע אחרת על אותה פעולה (כמו בדוגמת הלייקים) זה הגיוני לטפל בשניהם באותו נתיב. במצבים אחרים כשהכפתורים גורמים לפעולות שונות אולי יהיה הגיוני לכתוב כל כפתור בטופס משלו. לדוגמה כפתור של "יצירת פריט חדש" וכפתור של "מחיקה" עדיף לרשום בשני טפסים שונים שיישלחו לשני נתיבים שונים בצד השרת.1 421
# הפרויקט שלא פורסם
הפרויקט שלא פורסם התחיל עם רעיון טוב והמשיך עם קוד שלא הכי עבד אבל כן הראה איזושהי יכולת. אחרי זה הפרויקט שלא פורסם נשלח לכמה חברים והם לא התלהבו (כי קשה לבנות משהו מלהיב, כי היו באגים, כי יש אינסוף שעות של שיוף לפני שאנשים יוכלו להתלהב ממשהו) ואז לא היה ברור איך להמשיך.
להמשיך לעבוד עליו אחרי שאף אחד לא התלהב? בשביל מה? לא עדיף להתרכז בדברים שיש להם ערך?
הפרויקט שלא פורסם נגנז הבעיה שהוא היה צריך לפתור הפסיקה להיות מעניינת, או לפחות עברה להיות הרבה פחות מעניינת ממה שהיתה בהתחלה. כי אולי לפני שנתיים חשבת שיהיה מדליק לייצר הכנסה פאסיבית דרך פרויקט, אבל עכשיו כבר מצאת עבודה שמשלמת הרבה יותר ממה שהפרויקט היה יכול להכניס והבעיה כבר לא רלוונטית. אולי לפני שנה חשבת שחייבים JavaScript Framework טוב יותר מריאקט, אבל היום כבר הבנת שעם כל החסרונות בשילוב עם TypeScript ריאקט דווקא נותנת פיתרון מספיק טוב. אולי לפני שנתיים חשבת שחייבים תוכנה שתעזור לנהל את ההוצאות בבית, אבל מאז המחירים ירדו ויוקר המחיה נפתר מעצמו (או שלא...).
נמאס לכם לגנוז פרויקטים? תתחילו בלחפש בעיות יותר מעניינות. כל עוד הבעיה עדיין רלוונטית, פרויקט לא מספיק מעניין הוא בסך הכל ניסיון כושל אחד לפתור אותה והזדמנות ל Pivot.
1 421
from functools import reduce
from itertools import chain
from collections import defaultdict
input_data = """08 02 22 97 38 15 00 40 00 75 04 05 07 78 52 12 50 77 91 08
49 49 99 40 17 81 18 57 60 87 17 40 98 43 69 48 04 56 62 00
81 49 31 73 55 79 14 29 93 71 40 67 53 88 30 03 49 13 36 65
52 70 95 23 04 60 11 42 69 24 68 56 01 32 56 71 37 02 36 91
22 31 16 71 51 67 63 89 41 92 36 54 22 40 40 28 66 33 13 80
24 47 32 60 99 03 45 02 44 75 33 53 78 36 84 20 35 17 12 50
32 98 81 28 64 23 67 10 26 38 40 67 59 54 70 66 18 38 64 70
67 26 20 68 02 62 12 20 95 63 94 39 63 08 40 91 66 49 94 21
24 55 58 05 66 73 99 26 97 17 78 78 96 83 14 88 34 89 63 72
21 36 23 09 75 00 76 44 20 45 35 14 00 61 33 97 34 31 33 95
78 17 53 28 22 75 31 67 15 94 03 80 04 62 16 14 09 53 56 92
16 39 05 42 96 35 31 47 55 58 88 24 00 17 54 24 36 29 85 57
86 56 00 48 35 71 89 07 05 44 44 37 44 60 21 58 51 54 17 58
19 80 81 68 05 94 47 69 28 73 92 13 86 52 17 77 04 89 55 40
04 52 08 83 97 35 99 16 07 97 57 32 16 26 26 79 33 27 98 66
88 36 68 87 57 62 20 72 03 46 33 67 46 55 12 32 63 93 53 69
04 42 16 73 38 25 39 11 24 94 72 18 08 46 29 32 40 62 76 36
20 69 36 41 72 30 23 88 34 62 99 69 82 67 59 85 74 04 36 16
20 73 35 29 78 31 90 01 74 31 49 71 48 86 81 16 23 57 05 54
01 70 54 71 83 51 54 69 16 92 33 48 61 43 52 01 89 19 67 48"""
Index = tuple[int, int]
Matrix = dict[Index, int]
data: Matrix = defaultdict(lambda: 1)
for line_number, line in enumerate(StringIO(input_data)):
for column_number, column_value in enumerate(line.split()):
data[(line_number, column_number)] = int(column_value)
def mul(arr: Matrix, indices: Iterable[Index]) -> int:
return reduce(operator.mul,
[arr[index] for index in indices],
1)
def move(start_index: Index, step: Callable[[Index], Index], count: int = 4):
index = start_index
for i in range(count):
yield index
index = step(index)
products = [
[
mul(data, move((row, col), lambda t: (t[0] + 1, t[1]))),
mul(data, move((row, col), lambda t: (t[0] - 1, t[1]))),
mul(data, move((row, col), lambda t: (t[0], t[1] + 1))),
mul(data, move((row, col), lambda t: (t[0], t[1] - 1))),
mul(data, move((row, col), lambda t: (t[0] + 1, t[1] + 1))),
mul(data, move((row, col), lambda t: (t[0] - 1, t[1] - 1))),
mul(data, move((row, col), lambda t: (t[0] + 1, t[1] - 1))),
mul(data, move((row, col), lambda t: (t[0] - 1, t[1] + 1))),
]
for row in range(20)
for col in range(20)
]
def flatten(matrix):
return list(chain.from_iterable(matrix))
print(max(flatten(products)))
יש לכם כיוונים אחרים איך לפתור את זה? רוצים לשתף פיתרון בשפה אחרת? בדיוק בשביל זה יש מקום לתגובות פה למטה.1 421
# בואו נפתור יחד את פרויקט אוילר תרגיל 11 בשפת Python
פרויקט אוילר הוא אוסף של בעיות תכנות חמודות איתן אפשר לתרגל שפות תכנות שונות, או יכולות חדשות של שפות תכנות שאולי ביום יום לא כל כך יוצא לגעת בהן. יש שם אינסוף בעיות ואפשר למצוא את כולן באתר הפרויקט:
https://projecteuler.net/archives
בתרגיל 11 יש מטריצה של מספרים:
08 02 22 97 38 15 00 40 00 75 04 05 07 78 52 12 50 77 91 08
49 49 99 40 17 81 18 57 60 87 17 40 98 43 69 48 04 56 62 00
81 49 31 73 55 79 14 29 93 71 40 67 53 88 30 03 49 13 36 65
52 70 95 23 04 60 11 42 69 24 68 56 01 32 56 71 37 02 36 91
22 31 16 71 51 67 63 89 41 92 36 54 22 40 40 28 66 33 13 80
24 47 32 60 99 03 45 02 44 75 33 53 78 36 84 20 35 17 12 50
32 98 81 28 64 23 67 10 26 38 40 67 59 54 70 66 18 38 64 70
67 26 20 68 02 62 12 20 95 63 94 39 63 08 40 91 66 49 94 21
24 55 58 05 66 73 99 26 97 17 78 78 96 83 14 88 34 89 63 72
21 36 23 09 75 00 76 44 20 45 35 14 00 61 33 97 34 31 33 95
78 17 53 28 22 75 31 67 15 94 03 80 04 62 16 14 09 53 56 92
16 39 05 42 96 35 31 47 55 58 88 24 00 17 54 24 36 29 85 57
86 56 00 48 35 71 89 07 05 44 44 37 44 60 21 58 51 54 17 58
19 80 81 68 05 94 47 69 28 73 92 13 86 52 17 77 04 89 55 40
04 52 08 83 97 35 99 16 07 97 57 32 16 26 26 79 33 27 98 66
88 36 68 87 57 62 20 72 03 46 33 67 46 55 12 32 63 93 53 69
04 42 16 73 38 25 39 11 24 94 72 18 08 46 29 32 40 62 76 36
20 69 36 41 72 30 23 88 34 62 99 69 82 67 59 85 74 04 36 16
20 73 35 29 78 31 90 01 74 31 49 71 48 86 81 16 23 57 05 54
01 70 54 71 83 51 54 69 16 92 33 48 61 43 52 01 89 19 67 48
ואנחנו צריכים למצוא את רביעיית המספרים הצמודים (באותה שורה, טור או אלכסון) שמכפלתם הגדולה ביותר.
## פיתרון הבעיה
בגלל ש-1 לא משפיע על מכפלות, הדרך הכי קלה (עבורי) לגשת לבעיה כזאת היתה להכניס את כל המספרים ל defaultdict שערך ברירת המחדל שלו הוא 1. בצורה כזאת לא צריך לבדוק אם אנחנו בגבולות של המטריצה - פשוט כל דבר שמחוץ למטריצה יהיה 1 ולא ישפיע על המכפלה.
זה הקוד שקורא את המטריצה למבנה נתונים מתאים בפייתון:
Index = tuple[int, int]
Matrix = dict[Index, int]
data: Matrix = defaultdict(lambda: 1)
for line_number, line in enumerate(StringIO(input_data)):
for column_number, column_value in enumerate(line.split()):
data[(line_number, column_number)] = int(column_value)
עכשיו שיש לנו את ה defaultdict אפשר לדבר על חישובי המכפלות. בשביל לחשב מכפלה של מספר אינדקסים במילון כתבתי את הפונקציה mul:
def mul(arr: Matrix, indices: Iterable[Index]) -> int:
return reduce(operator.mul,
[arr[index] for index in indices],
1)
ובשביל לגלות איזה אינדקסים אני צריך להעביר לפונקציה כתבתי פונקציה נוספת בשם move:
def move(start_index: Index, step: Callable[[Index], Index], count: int = 4):
index = start_index
for i in range(count):
yield index
index = step(index)
הפונקציה move פשוט לוקחת אינדקס התחלה, פונקציה לצעד הבא ומספר צעדים ומחזירה Generator שכל next שלו מחזיר את האינדקס הבא.
לסיום נשאר רק לקחת את כל המטריצה ולהפוך אותה למערך של מכפלות כך שכל תא במערך יכיל את 8 המכפלות שסביבו:
products = [
[
mul(data, move((row, col), lambda t: (t[0] + 1, t[1]))),
mul(data, move((row, col), lambda t: (t[0] - 1, t[1]))),
mul(data, move((row, col), lambda t: (t[0], t[1] + 1))),
mul(data, move((row, col), lambda t: (t[0], t[1] - 1))),
mul(data, move((row, col), lambda t: (t[0] + 1, t[1] + 1))),
mul(data, move((row, col), lambda t: (t[0] - 1, t[1] - 1))),
mul(data, move((row, col), lambda t: (t[0] + 1, t[1] - 1))),
mul(data, move((row, col), lambda t: (t[0] - 1, t[1] + 1))),
]
for row in range(20)
for col in range(20)
]
ולהדפיס את המכפלה הגדולה ביותר שמצאנו:
def flatten(matrix):
return list(chain.from_iterable(matrix))
print(max(flatten(products)))
סך הכל הפיתרון המלא הוא:
from typing import Sequence, Callable, Iterable
from io import StringIO
import operator
¡Ya disponible! Investigación de Telegram 2025 — los principales insights del año 
