ToCode
رفتن به کانال در Telegram
1 420
مشترکین
+124 ساعت
+17 روز
-430 روز
آرشیو پست ها
1 420
היום למדתי: בדיקת מאפיין ב Beautiful Soup בפייתון
הקוד הבא נכשל כי ללינק השני אין מאפיין href:
from bs4 import BeautifulSoup
text = """<div>
<a href="#a">a</a>
<a>not a link</a>
</div>"""
if __name__ == "__main__":
soup = BeautifulSoup(text, features="html.parser")
for link in soup.find_all("a"):
print(link["href"])
יותר מעניין לשים לב ש bs4.Tag לא מימשו בדיקת שייכות הקוד הזה רץ אבל לא מדפיס אף לינק:
for link in soup.find_all("a"):
if "href" in link:
print(link["href"])
מה עושים? דרך אחת היא להסתכל על מאפיין attrs של התג, שהוא כן מילון:
for link in soup.find_all("a"):
if "href" in link.attrs:
print(link["href"])
דרך שניה היא להשתמש ב get במקום בסוגריים מרובעים, כי get מחזיר None כשהמאפיין לא קיים:
for link in soup.find_all("a"):
if link.get("href") is not None:
print(link["href"])1 420
שגיאות בתור ערכים בפייתון?
אחת הסיבות שעברתי מפייתון לסקאלה בפרויקט חדש שאני בונה היא מערכת הטיפוסים של סקאלה, וכן בין השאר גם עבודה עם שגיאות בתור ערכים. מתכנתי פייתון רבים שמגיעים לסקאלה או גו עוברים חוויה דומה ותוהים איך לחזור לעולם הפייתונאי בו כל שורה שאנחנו מפעילים יכולה לזרוק Exception ולשנות את כל מהלך התוכנית, ואין לנו דרך טובה בהגדרה של פונקציה להצהיר שאותה פונקציה זורקת Exception.
ניסיון אחד להתמודד עם האתגר הופיע בפוסט של מהנדסי חברת Inngest. הם הציעו לארגן את הקוד שלהם כך שלעולם לא יזרוק Exception ובמקום זה יחזיר תמיד ערך או קוד שגיאה, לדוגמה:
* Define a function that returns a union of a User and an error *
def get_user(user_id: str) -> User | Exception:
rows = users.find(user_id=user_id)
if len(rows) == 0:
return Exception("user not found")
return rows[0]
def rename_user(user_id: str, name: str) -> User | Exception:
# Consume the function
user = get_user(user_id)
if isinstance(user, Exception):
return user
user.name = name
return user
במבט ראשון זה נראה מעניין ואולי מזכיר את ראסט או גו. הפונקציה get_user יכולה להצליח או להיכשל והעובדה הזאת מופיעה בחתימה שלה, וכל מי שמפעיל אותה ומבצע בדיקת טיפוסים יהיה חייב לוודא שהפונקציה הצליחה לפני שאפשר יהיה לעבוד עם הערך.
אבל קשה לראות איך ממשק כזה יוביל לשיפור בקוד. התוצאה היותר סבירה היא קוד בסגנון Java שמכריח אותנו לבדוק אם פונקציה נכשלה בכל קריאה לפונקציה. זה קצת יותר טוב מהפזרנות של פייתון אבל יוצר עומס לא סביר על הקוד שקורא לפונקציות.
נשווה את זה לסקאלה (או שפות אחרות שמאפשרות שגיאה-בתור-ערך):
case class User(id: Long, name: String)
val users: List[User] = List(
User(1, "glassgrieving"),
User(2, "wrensponge"),
User(3, "horizonhow"))
def getUserIndex(userId: Long): Option[Int] =
users.indexWhere(p => p.id == userId) match
case i if i >= 0 => Some(i)
case _ => None
def renameUser(userId: Long, newName: String): Option[List[User]] =
for {
index <- getUserIndex(userId)
} yield users.updated(index, User(userId, newName))
בגירסת הסקאלה החתימה של הפונקציה ברורה לגבי הסיכון לכישלון - אנחנו מחזירים Option ולא את האוביקט הנדרש, ו Option בסקאלה עשוי להחזיק ערך אבל גם יכול להיות ריק.
הקסם מגיע בפונקציה השניה renameUser. נכון בסקאלה אנחנו עובדים עם מידע שהוא Immutable ולכן היה צריך לשנות קצת את הדוגמה ולהחזיר רשימת משתמשים חדשה, אבל הדבר החשוב כאן הוא אופן העבודה עם הערך שיכול להיות ריק. הפונקציה renameUser היא בסך הכל חישוב וערך ריק ממשיך להיות ריק כשהחישוב ממשיך. אפשר לחשוב על זה כמו סימן שאלה בטייפסקריפט כשאנחנו כותבים:
a?.b?.c?.d
אם אחד מהערכים יהיה ריק החישוב כולו יחזיר תוצאה ריקה והכל בסדר, אנחנו לא צריכים את ה if שבודק מה ערך ההחזר בצורה מפורשת - שגיאה בתור ערך אומרת שאפשר להמשיך "לעבוד" עם השגיאה כאילו היתה ערך רגיל, ורק כשנצטרך את הערך להחליט מה לעשות עם התוצאה או השגיאה.
לכן לדעתי הניסיון של Inngest לעקוף את מנגנון זריקת השגיאות של פייתון לא עבד לטובתם, והפך מנגנון שהיה די נוח לעבוד איתו למנגנון שרק מוסיף סירבול ולא באמת יפתור את הבעיה, כי exceptions בפייתון נזרקים מכל ספריה ומכל מודול מובנה.1 420
פיתרון Advent Of Code 2023 יום 10 חלק ראשון בסקאלה
אין כמו חידת גרפים טובה בשביל להתחיל את השבוע, וכך הגענו ליום העשירי כבר של Advent Of Code האחרון בסקאלה. אני כמובן מקווה עד סוף השנה לסיים את כל 25 החידות כדי להגיע מוכן ל 2024, אבל התמדה זה תמיד דבר מסובך. בכל מקרה בואו נראה מה הכין לנו אריק ווסטל לחלק הראשון של החידה העשירית.
חיפוש מעגל בגרף
הקלט הפעם הוא תיאור של מסלולים אפשריים במרחב בפורמט קצת מוזר, לדוגמה:
.....
.F-7.
.|.|.
.L-J.
.....
נקודה מסמנת משבצת שנמצאת מחוץ לגרף, והסימנים האחרים מסמנים לאיזה משבצות המשבצת עם הסימן מחוברת. למשל המשבצת עם הסימן - בשורה השניה מחוברת למשבצת עם הסימן F וזו עם הסימן 7. לכל סימן יש משמעות לפי הפירוט הבא:
1. קו אנכי מחבר את המשבצות שמעליו ומתחתיו
2. קו אופקי מחבר את המשבצות משני צדדיו
3. האות L מחברת את המשבצת שמעליה עם זאת שמימינה
4. האות J מחברת את המשבצת שמעליה עם זו שמשמאלה
5. האות F מחברת את המשבצת שמתחתיה עם זאת שמימינה
6. האות L מחברת את המשבצת שמעליה לזו שמימינה
האתגר הבא הוא שנקודת ההתחלה מסומנת באות S ואנחנו לא יודעים איזה סוגי חיבורים יש לה, כלומר קלט אמיתי לדוגמה יהיה בעצם:
.....
.S-7.
.|.|.
.L-J.
.....
המשימה שלנו היא למצוא את המעגל ולהדפיס את המרחק של הנקודה הרחוקה ביותר שבתוכו מנקודת ההתחלה. בקלט הדוגמה התשובה היא 4 והנקודה הכי רחוקה היא איפה שמופיעה האות J (תספרו ותראו).
פיענוח הקלט בסקאלה
דרך קלה לייצג את הקלט היא Hash Map בו המפתח הוא קואורדינטות של משבצת והערך הוא רשימה של כל המשבצות אליה אפשר להגיע מאותה משבצת. בשביל לבנות את הקלט התחלתי עם פונקציה שמקבלת קואורדינטות של משבצת וסימן ומחזירה את רשימת המשבצות אליהן אפשר להגיע:
def connections(ch: Char, row: Int, column: Int): List[(Int, Int)] =
ch match
case '.' => List()
case '|' => List((row - 1, column), ((row + 1), column))
case '-' => List((row, column - 1), (row, column + 1))
case 'L' => List((row - 1, column), (row, column + 1))
case 'J' => List((row - 1, column), (row, column - 1))
case '7' => List((row + 1, column), (row, column - 1))
case 'F' => List((row + 1, column), (row, column + 1))
case 'S' => List((-1, -1)) // marker
בגלל שאני לא יודע איזה סוג צינור יהיה במשבצת ההתחלה השארתי אותה עם רשימה פיקטיבית. בהמשך הקוד אחליף את הרשימה הזאת כל פעם בצינור אחר כדי למצוא את המעגלים השונים האפשריים ואבחר מהם את הארוך ביותר.
לאחר מכן אפשר להשתמש בפונקציה יחד עם קצת קסם של סקאלה כדי לבנות את המטריצה:
def parseInput(input: Source): Map[(Int, Int), List[(Int, Int)]] =
input
.getLines()
.zipWithIndex
.collect {
case (line: String, index: Int) => line.toList.zipWithIndex.map((ch, column) => (index, column, ch))
}
.flatten
.flatMap { case (row, column, ch) => Map((row, column) -> connections(ch, row, column))}
.toMap
המפתח הוא הפונקציה zipWithIndex שמוסיפה את האינדקסים וכך נוצרת לולאה כפולה של שורות ועמודות.
החלק הבא והמרכזי בפיתרון הוא הפונקציה הרקורסיבית שמחפשת את המעגל. החיפוש הוא בשיטת DFS בעזרת מחסנית ואנחנו יודעים שמצאנו מעגל כשהגענו לצומת שכבר ראינו בעבר:
@tailrec
def findCycleSizeDFS(map: Map[(Int, Int), List[(Int, Int)]],
workQueue: List[(Int, Int)],
seen: Set[(Int, Int)] = Set()): Int =
workQueue match
case start :: tail if seen.contains(start) =>
// Loop
seen.size
case start :: tail =>
val neighbors = map.getOrElse(start, List()).filterNot { p => seen.contains(p) }
findCycleSizeDFS(map, neighbors ++ workQueue, seen + start)
case Nil =>
// Dead End
0
האנוטציה tailrec מבטיחה לי שהפונקציה לא תשתמש בטעות במחסנית הרקורסיה ותמיד תאפשר אופטימיזציה על ידי מחיקת זנב הרקורסיה (רקורסיית זנב).
אחרי שבנינו את התשתית אפשר להמשיך ל main - תפקידו לרוץ על כל האפשרויות למשבצת ההתחלה ולמצוא את זו שתתן את המעגל הגדול ביותר, וגם להדפיס את גודל המעגל חלקי 2 בשביל למצוא את המרחק של הנקודה הרחוקה ביותר:
@main
def day10part1(): Unit =1 420
מה זה בעצם Premature Optimization?
השבוע נתקלתי במקרה בפוסט ישן של מקס צ'רנייק. בפוסט הוא בסך הכל מפרט הרבה דוגמאות לדברים שאנשים עשויים לחשוב שהם Premature Optimization אבל בעצם הם לא - לדוגמה לחשוב על אלגוריתם שמישהו אחר לא חשב או להשתמש במערכת קיימת במקום לפתח משהו מאפס כשיש מערכת שפותרת את הבעיה.
מכל הנקודות בפוסט (תקראו אותו. הוא באמת מעניין ולא חופר), עולה רעיון משותף שהנדסה טובה וחשיבה בריאה הם דבר שכדאי להפעיל מההתחלה, ואינם נחשבים לאיזה אופטימיזציה משוגעת שנעשית מוקדם מדי. אבל מהי אם כן אופטימיזציה לא נחוצה שעדיף לחכות איתה? על זה מקס לא מדבר ואני אנסה להציע כמה רעיונות-
1. זה אולי עוד לא הזמן לבנות מערכת שגדלה אוטומטית להתמודד עם עומסים, לפני שיש לך לקוחות או צפי לעומסים.
2. זה אולי עוד לא הזמן לשפר את זמן הטעינה של האתר אם עדיין יש בו באגים והוא לא נפתח בחלק מהדפדפנים.
3. זה אולי עוד לא הזמן להוסיף 2FA (אופטימיזציה למנגנון אימות משתמשים) אם עדיין אין לך מספיק משתמשים או שאין מידע רגיש השמור במערכת.
4. זה אולי עוד לא הזמן ליצור אבסטרקציה חדשה בקוד, לפני שאנחנו מבינים מה יהיה המבנה הסופי שלו ועם איזה שינויים בדרישות עוד נצטרך להתמודד.
הסיפור של Premature Optimization הוא קודם כל סיפור של פוקוס וסדרי עדיפויות. אנחנו נגיד שיש לנו Premature Optimization כשאנחנו מתקנים את הדברים הלא נכונים במערכת במקום להתרכז בבעיות הכואבות שלה, כלומר זאת פחות שאלה של "איך" ויותר שאלה של "מה", וחשוב לקחת בחשבון את העלויות.
יצירת אבסטרקציה בקוד שפותרת בצורה אלגנטית בעיה היום עלולה להיות Premature Optimization בגלל שאולי איתה יהיה לנו קשה להתמודד עם דרישות חדשות בעתיד.
תוספת של שרת Memcache כדי לשפר ביצועים עלולה להיות Premature Optimization כי תוספת מכונה גוררת עלות תפעול, השרת יכול ליפול, צריך להתקין גירסה חדשה ולהיזהר מפירצות וקונפיגורציה לא נכונה ונפילה של השרת יכולה ליצור תגובת שרשרת שתפיל את כל המערכת. אם אין לי עדיין בעיות ביצועים שדורשות Cache אני רק משלם את המחיר בלי לקבל תמורה משמעותית.
יצירת מנגנון אימות מאובטח יותר עלולה להיות Premature Optimization כי הלקוחות שלי אולי לא צריכים את המנגנון המתוחכם, והוא יכול לפגוע בהם אם הסמס לא מגיע או שהחליפו טלפון ושכחו להעביר את אפליקציית ה Authenticator למכשיר החדש.
בשורה התחתונה המפתח להבנה מהו Premature Optimization יהיה בחינת העלות של הפיצ'ר או המנגנון, כולל עלות הפיתוח או התחזוקה של אותו פיצ'ר, והאם התמורה שאקבל מאותו פיתוח שווה את המחיר.
1 420
ללמוד מהסיבות הלא נכונות?
כשלימדתי קורסים של הסבה לתכנות לאנשים בלי רקע קודם בפיתוח אחד הדברים שהפתיעו אותי היה הסיבות השונות בגללן אנשים באים ללמוד. ברור היו את אלה שמכורים למחשב ותמיד חלמו לדעת לתכנת, אבל היו גם המון סיבות אחרות - אנשים שבאים ללמוד תכנות כדי לכתוב לבד פרויקט שתמיד חלמו עליו, אנשים שרוצים לפתוח סטארטאפ טכנולוגי ויש להם רעיון פצצה אבל לא יודעים איך ליישם אותו, אנשים שחולמים להיות נוודים דיגיטליים או כאלה שרוצים להרוויח יותר בעבודה (כי בכל זאת משכורת הייטק וכו).
ברוב המקרים זה לא נגמר טוב.
אנשים שמחפשים עבודה שמכניסה יותר כסף מגלים אחרי חצי שנה של לימוד שבעצם תכנות זה לא כזה פשוט וגם אחרי שהם עשו המון מאמץ הם לא מצליחים למצוא עבודה. אנשים שמחפשים לבנות מוצר טכנולוגי מאפס מגלים כמה השקעה הם יצטרכו בבניית המוצר (אחרי הקורס) ומתייאשים לפני שיש משהו באוויר. אנשים שחולמים להיות נוודים דיגיטליים עד סוף הקורס (שיכול להימשך גם שנה) מתחתנים ומתמסדים. וכך מתוך כיתה של עשרים ומשהו משתתפים רק מעטים מסיימים את הקורס ועדיין מתאמצים להשתלב במקצוע.
לכן לימים כשישבתי לבנות אתר קורסים אחת ההחלטות הראשונות שלי היתה לבנות את האתר רק לאנשים שכבר יודעים תכנות, בדיוק בשביל לחסוך לכולם את כאב הלב של לימוד ארוך ובסופו התפכחות כואבת. נתתי לפסימיות לנצח וויתרתי מראש על אותם אנשים שהסיכויים שלהם להתמיד נראו לי נמוכים.
היום אני חושב שיש דרך טובה יותר.
הבעיה עם קורסי ההסבה היא שהם מאוד מחייבים. מי שלא מכיר תכנות בכלל עשוי להוציא כמעט עשרים אלף ש"ח על קורס של שנה, בלי לדעת אם בסוף הקורס הוא באמת יעבוד בתחום. אם אתם בתחילת הדרך ומתלבטים אם תכנות זה בשבילכם, השקעה כזאת נראית מפחידה ובצדק. דרך טובה יותר היא במקום להתמכר לחלומות להתקדם איתם צעד צעד, כלומר:
1. ללמוד תכנות לבד מהאינטרנט פעם בשבוע, בקטנה.
2. לבנות לעצמך משהו שמעניין אותך, גם אם זה ייקח המון זמן.
3. להתיחס ללימוד כמו לחוג או לתחביב, משהו שאפשר לעשות וגם להפסיק ואז לחזור לזה אחרי חודשיים והכל בסדר.
4. לקחת את הלימוד בפרופורציה, לפחות עד שנהיה בטוחים.
אם הייתי מתחיל היום אתר קורסים מאפס הייתי שמח לבנות משהו לצריכה קלה בתור תחביב, משהו שהילדים שלי היו יכולים לנסות - שיעורים קצרים על מחשב בשילוב שיעורי אודיו (בסגנון פודקסט שאפשר לשמוע מהדרך), המון דוגמאות לתרגול עצמי וכמעט בלי חיבור ל"טרנדים" של היום. לא במטרה ללמד תכנות אלא במטרה להראות מה זה תכנות ולמה זה כיף, לפני שצוללים לדברים הקשים.
1 420
בלתי אפשרי
"בלתי אפשרי" או "אני לא יכול לדמיין את זה"?
רעיונות בלתי אפשריים שווה לזרוק ולהפסיק לחשוב עליהם.
לגבי רעיונות שאי אפשר לדמיין אותם שווה לאמן את הדמיון. רק בגלל שהיום אני לא מצליח לדמיין משהו לא אומר שהוא לא אפשרי עבורי, וחבל לזנוח חלומות רק בגלל בעיית דמיון.
טיפ לחיים - בואו נעלה את רף ההוכחה לפני שמכריזים על רעיון "בלתי אפשרי". רוב הזמן "אני עדיין לא מצליח לדמיין את זה" יעבוד טוב יותר.
1 420
תרגיל פייתון: בול פגיעה עם חזרות
במשחק בול פגיעה משתמש אחד בוחר מספר והשני צריך לנחש מהו, כאשר בכל סיבוב הרמז הוא כמה ספרות מופיעות במספר הסודי במקום אחר מהניחוש, וכמה ספרות מופיעות במספר הסודי בדיוק באותו מקום (בשני המקרים בלי לגלות איזה ספרות מופיעות איפה). בגירסה של המשחק עם חזרות סיפרה יכולה להופיע כמה פעמים במספר הסודי, וצריך לקחת את זה בחשבון כשמחזירים תשובה למשתמש שמנחש. בואו נראה את האתגרים ואז את הפיתרון בפייתון.
למה חזרות זה כל כך מסובך
אלגוריתם נאיבי לחישוב "כמה ספרות במקום הנכון" ו"כמה ספרות במקום הלא נכון" יכול להיכתב בתור לולאה כפולה:
blacks = 0
whites = 0
for idx1, i in enumerate(str(value)):
for idx2, j in enumerate(str(other)):
if i == j:
if idx1 == idx2:
blacks += 1
else:
whites += 1
כאשר whites מייצג את מספר הספרות במקום הלא נכון, ו blacks את מספר הספרות שנמצאות בדיוק במקום הנכון בין שני מספרים - האחד שמור במשתנה value והשני במשתנה other.
אבל לולאה כזאת כבר לא עובדת אם יש חזרות, בגלל שאנחנו מקבלים יותר מדי "לבנים". חישבו על שני המספרים 112 ו 221 - אם הניחוש הסודי הוא 221 ומישהו מנסה לנחש את המספר 112 עלינו לענות לו שיש לו שני מספרים במקום הלא נכון וזהו.
פיתרון בפייתון שמתמודד גם עם כפילויות
בשביל לחשב את מספר הלבנים והשחורים עם כפילויות עלינו לקחת בחשבון שכל סיפרה יכולה "להשפיע" רק פעם אחת. דרך אחת לגשת לזה תהיה לשמור את האינדקסים של כל סיפרה במילון כאשר המפתח הוא הסיפרה והערך הוא קבוצת כל האינדקסים שלה.
בייצוג כזה אפשר לחשב את השחורים בתור קבוצת החיתוך של שתי קבוצות האינדקסים, ובשביל הלבנים נוריד משתי הקבוצות את קבוצת החיתוך (כי השתמשנו בה כבר בשביל השחורים) וניקח את גודל הקבוצה הקטן משתיהן. קוד? ברור:
class Guess:
def __init__(self, value: int):
digitgroups = itertools.groupby(
sorted(list(enumerate(str(value))), key=lambda m: m[1]),
lambda m: m[1])
self.value = {
k: set([i[0] for i in v])
for k, v in digitgroups
}
def compare(self, other: "Guess") -> Result:
white = 0
black = 0
for digit in self.value.keys():
if digit in other.value:
intersection = self.value[digit].intersection(other.value[digit])
black += len(intersection)
white += min(len(self.value[digit] - intersection), len(other.value[digit] - intersection))
return Result(white=white, black=black)
ועכשיו ההשוואה נותנת את התוצאה הנכונה:
g1 = Guess(112)
g2 = Guess(221)
* prints: Result(white=2, black=0) *
print(g1.compare(g2))
יש לכם רעיונות נוספים? אולי טובים יותר? אתם כבר יודעים מה לעשות - הדביקו כאן בתגובות או בטלגרם.1 420
למה לכתוב בדיקות?
1. בדיקות עוזרות לבוס שלי להרגיש טוב.
2. בדיקות עוזרות לי להבין את הבעיה טוב יותר לפני שמתחיל לכתוב קוד.
3. בדיקות שומרות עליי מחברי הצוות ההזויים שלי שרק מכניסים באגים למערכת.
4. בדיקות שומרות עליי מטעויות טפשיות שאעשה בעתיד שישברו מנגנונים שבניתי.
5. בדיקות מאפשרות לי לפתח פיצ'רים חדשים מהר יותר כי הרצת הבדיקות מהירה יותר מהרצת המערכת בסביבה האמיתית.
וכן אפשר לבחור יותר מתשובה אחת, אבל זה לא חשוב. יותר מעניין לדמיין איך נראות הבדיקות עצמן, איך נראות בדיקות שאנחנו כותבים כדי לשמח את הבוס, לעומת בדיקות שעוזרות לנו לכתוב קוד מהר יותר, ועל איזה סוג מערכת היינו מעדיפים לעבוד.
1 420
כריך של באשר
בדרך החוצה מהחדר כושר יש מעדניית גבינות (פרמז'רי, בשביל האווירה החו"לית) בשם באשר עם שלט מזמין "כריך של באשר כבר טעמת?"
ואני כל פעם עברתי שם ורציתי לטעום, אבל פעם אחת הכריך נראה יקר מדי, ופעם אחרת הוא לא נראה מספיק טרי, ועוד פעם בדיוק לא הייתי במצב רוח לכריך גבינה וככה עוברים הימים וידעתי שכל פעם שאני עובר ליד הפרומז'רי אני מסרב להזדמנות אבל משאיר את הדלת פתוחה. ה"לא" היה בסך הכל "לא היום", אבל אל תדאג באשר מחר ננסה שוב. וכשהפסקתי לאכול מוצרי חלב רציתי להסתכל אחורה ולהגיד "רגע, מה עם הכריך?" ו"יש לי פה הזדמנות שמחכה" אבל זה כבר היה מאוחר מדי. את הכריך של באשר אני כבר לא אוכל.
נזכרתי בבאשר כשהסתכלתי שוב על קורס מקצועי שרציתי לעשות, ומכל מיני סיבות לא מצאתי את ההזדמנות. הפעם זה לא אני שהשתניתי אלא הקורס שירד מהאוויר.
דרק סיברס אמר Hell Yeah Or No והרבה זמן שמעתי אותו אבל בראש שלי זה היה Hell Yeah or Not Now. אבל האמת שה No של דרק הרבה יותר עוזר לפוקוס. לא רוצה? תמשיך הלאה, מכל הלב. ההמתנה להזדמנויות טובות יותר רק מבלבלת.
اکنون در دسترس! پژوهش تلگرام ۲۰۲۵ — مهمترین بینشهای سال 
