על סגנון, 79 תווים בשורה, טכניקות לפיצול שורה וכיו"ב

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

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

בפוסט הזה אני אדבר על:

  1. למה יש הגבלה כזו בכלל? מה היתרונות שלה?
  2. האם היא קיימת בתעשייה?
  3. איך עושים את זה נכון?
  4. האם אין משהו שפותר את זה אוטומטית?
  5. האם אני אתחיל לאכוף את זה בקומיטים שנשלחים?

נפתח בכמה מילים על…

למה ההגבלה הזו קיימת?

היסטוריה

כמו כל דבר, הסיפור שלנו לגבי ההגבלה הזו מתחיל בקצת היסטוריה.
בעבר, דרך מסוימת לשמור נתונים ממוחשבים בצורה פיזית הייתה בעזרת כרטיסים מנוקבים.
בין הכרטיסים הפופולריים ביותר היו אלו של IBM, שהכילו 12 שורות שבכל אחת מהן 80 “תאים”, כל תא מייצג ביט.
אם “תא” בטבלה הזו (למעשה מקום פיזי על הכרטיס) נוקב, ערכו היה 1, אחרת ערכו היה 0.
על אותם כרטיסים היו שומרים פעמים רבות גם תוכניות מחשב שרצו להריץ.

לכרטיסים המנוקבים השפעות תרבותיות רבות.
רוחב ברירת המחדל של הטרמינל שלכם (cmd) הוא 80 תווים, ו־PEP8, לדוגמה, ממליץ על עד 79 תווים בשורה אלא אם יש הסכמה רחבה בקרב כותבי הפרויקט על להעלות את המגבלה לעד 100 תווים.

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

פרקטיקה

הנה כמה סיבות שנהוג לציין בעד ההגבלה של X תווים בשורה:

  1. קוד של 79 תווים בשורה נכנס בכל מסך ובכל חלון – בטרמינלים, בטבלטים ואפילו במסך של טלפון בתצוגה אופקית.
  2. בסביבת עבודה, קוד של 79 תווים מאפשר לראות diff (שתי עמודות של קוד) בקלות, בלי שהקוד “גולש” ו"נשבר" לשורות הבאות. במסכים גדולים זה האורך היחיד שיאפשר עבודה נוחה במצבי ‏3 way merge.
  3. העמסת פעולות רבות בשורה מעמיסה קוגנטיבית על הקורא. פיצול לשורות קצרות מכריחה את המתכנת לא להכניס יותר מדי לוגיקה לשורה אחת.
  4. זה נכון גם לגבי הזחות – קוד מוזח מדי “מבזבז” תווים בשורה, כך שהגבלת תווים מעודדת מתכנתים לא להזיח את הקוד יותר מדי, ומכריחה אותם לפצל לוגיקה מורכבת לפונקציות נפרדות.
  5. נרצה לאפשר לקוראי הקוד לסרוק אותו במהירות. שורות ארוכות מדי יגרמו לעיניים לזוז הרבה ימינה ושמאלה.

הסיבות פה מנוסחות עבור 79 תווים בשורה – אבל תופסות פחות או יותר גם עבור הגבלות דומות (100, נניח).

האם השטות הזו קיימת בתעשייה?

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

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

איך עושים את זה נכון?

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

דרך 1: השמה למשתנים.

נדמיין את השורה הבאה:

wav_lowered_filenames = [file.lower() for file in pathlib.Path(SOUND_DIRECTORY).glob('**/*.wav')]

בקלות אפשר לפצל למשהו כזה:

wav_files = pathlib.Path(SOUND_DIRECTORY).glob('**/*.wav')
wav_lowered_filenames = [file.lower() for file in wav_files]

הרבה יותר קריא, ונכנס אחלה ב־80 תווים בשורה.
זכרו שמותר לעשות את אותו דבר גם עם תנאים:

return select(Users.id, User.name).from(Users).where(Users.role == UserRoles.Administrator)

יכול להיכתב כ:

is_admin = Users.role == UserRoles.Administrator
return select(Users.id, User.name).from(Users).where(is_admin)

דרך 2: Unpacking.

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

במקום:

is_admin = Users.role == UserRoles.Administrator
return select(User.id, User.name, User.address, User.email).from(Users).where(is_admin)

נעדיף לכתוב:

user_shipping_details = (User.id, User.name, User.address, User.email)
is_admin = Users.role == UserRoles.Administrator
return select(*user_shipping_details).from(Users).where(is_admin)

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

אפשר להשתמש ב־Unpacking גם כדי לחסוך קריאה לא נעימה לפונקציה מרובת פרמטרים, אם יש לנו את הנתונים במילון.
תוכלו לראות דוגמה פשוטה לזה ב־lms או דוגמה מורכבת יותר בפתרון שלי ל־Advent of Code יום 2.

דרך 3: פיצול שורה בכוח.

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

אחת הצורות המוכרות והפחות מומלצות היא לעשות את זה באמצעות התו \. PEP8 פחות מתחבר לשיטה הזו.

הצורה הפופולרית והמומלצת היא לפצל את השורה באמצעות סוגריים.
לדוגמה:

return get_all_directory_names(MAIN_DIRECTORY).get_files().to_lowercase().read()

יהפוך ל־

return (
    get_all_directory_names(MAIN_DIRECTORY)
   .get_files()
   .to_lowercase()
   .read()
)

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

def get_files_in_directory(dir_name: str, is_recursive: bool) -> Iterable[str]:

במקרה הזה, אפשר לפצל בצורה הבאה:

def get_files_in_directory(
    dir_name: str, is_recursive: bool,
) -> Iterable[str]:

אפשר להשתמש באותו טריק גם עם import־ים שצריכים פיצול, או במבני נתונים (מילון, רשימה, tuple) עם הרבה נתונים שצריכים פיצול.

טיפ: איך מפצלים עם סוגריים ככה שהקוד לא יראה צ’יקמוק

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

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

def get_files_in_directory(
    dir_name: str, is_recursive: bool) -> Iterable[str]:

וכן כך:

def get_files_in_directory(
    dir_name: str, is_recursive: bool,
) -> Iterable[str]:

או כך:

def get_files_in_directory(
    dir_name: str,
    is_recursive: bool,
) -> Iterable[str]:

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

האם אין משהו שפותר את זה אוטומטית?

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

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

חשוב לי להדגיש שוב: אל תעירו לאנשים אחרים על דברים סגנוניים אם זה לא ב־guideline של הפרויקט.
שתי סיבות:

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

האם אני אתחיל לאכוף את זה בקומיטים שנשלחים?

לא.

16 לייקים