פערי תרבויות בין שפות
פוסט זה כולל טיפ קצר לעבודה יעילה עם Python. אם אתם רוצים ללמוד פייתון יותר לעומק אני ממליץ על קורס Python כאן באתר.
הקורס כולל עשרות שיעורי וידאו והמון תרגול מעשי וילמד אתכם Python בצורה מקצועית מההתחלה ועד הנושאים המתקדמים.
הרבה פעמים כשנגיע לכתוב פרויקט חדש עולם התוכן של הפרויקט או החברים לצוות כבר יכתיבו את הכלים הטכנולוגיים. צריך לפתח מערכת צד-לקוח? JavaScript כמובן. כותבים משהו בתחום למידת מכונה? Python, אלא מה.
אפילו בפרויקטים שבהם ההחלטה כן דורשת התלבטות, למשל Angular מול React, השיקול של איך השפה נראית או מה החיבור שמישהו מרגיש לספריה מסוימת הוא כנראה אחד האחרונים ברשימה. יותר חשוב לבדוק איפה יהיה לי יותר קל לגייס מתכנתים או באיזה סביבה יש יותר דוגמאות קוד זמינות שדומות למה שאני בונה.
ולמרות כל הפתיח הזה אני עדיין חושב שמעניין לשים לב להבדלי גישות בין שפות תכנות. הבדלים כאלה לא יהוו שיקול בבחירת שפה, אבל כן יעזרו לנו לקבל נקודת מבט נוספת על כל אחת מהשפות.
את ruby ו python למדתי באותו הזמן, אחרי שנים של עבודה עם perl. מטבע הדברים תקופה ארוכה הרגשתי הרבה יותר בבית ב ruby, אבל לקח לי זמן להסביר לעצמי ולעולם למה. פוסט זה הוא ניסיון לשים את האצבע על פערי התרבויות בין השפות ruby ו python וההשלכות שלהן על מבנה השפה.
1. הסתרה
בימים הראשונים בעבודה עם שתי השפות פייתון היא דווקא זו שנתנה את התחושה הביתית יותר. הגישה של פייתון לחיים היא לספר למתכנת רק מה שהוא צריך לדעת בכל רגע נתון. כמובן שמי ששואל מוצא את התשובות, ולכן יש תופעה מעניינת של מתכנתי python שככל שמכירים פחות את השפה יש תחושה שיודעים בה הכל.
רובי לעומתה לקחה גישה הפוכה. היא זורקת עליכם את האמת בפרצוף מהרגע הראשון בלי הסתרות ובלי לנסות למצוא חן. זה אומר שההתחלה תהיה קשה יותר אבל ככל שלומדים יותר גם מרגישים יותר ביטחון.
כך למשל חישוב אורך של מערך. בשפת Python נשתמש בקוד הבא:
a = [10, 20, 30]
print(len(a))
הקוד ידפיס 3 ונראה שלמדנו שהפונקציה len מחשבת אורך של מערך. הבעיה שזה רק חלק קטן מהסיפור. הפונקציה len בסך הכל מקבלת אוביקט ומפעילה פונקציה בשם __len__
של האוביקט. במקרה של מערך פונקציה זו מחזירה את האורך. אגב באותה מידה היינו יכולים לכתוב את הקוד השקול הבא:
a = [10, 20, 30]
print(a.__len__())
ולקבל את אותה תוצאה. פייתון מסתירה את המורכבות האמיתית של len ושל פיתוח מונחה עצמים עד שנגיע לשאול איך אנחנו כותבים בעצמנו אוביקט ש len תעבוד עבורו.
ומה לגבי רובי? הנה הקוד:
a = [10, 20, 30]
puts a.length
חישוב אורך הוא פונקציה של המערך, ולכן ברור שהקוד מופעל כמו הפעלת מתודה על האוביקט.
דוגמא מורכבת יותר אנו רואים בלולאות for. בשתי השפות לולאת for משמשת לסריקה של אוספים, ובשתי השפות גודל האוסף שאנו סורקים לא חייב להיות ידוע מראש. כך פייתון:
for i in range(10):
print(i)
הפקודה range מחזירה אוביקט מסוג range. לאוביקט זה יש פונקציה בשם __iter__
שמחזירה אוביקט חדש מסוג range_iterator
ולו פונקציה בשם __next__
שמחזירה את האלמנט הבא מתוך האוסף. בתחילת הלולאה פייתון מפעילה באופן אוטומטי את __iter__
, מקבלת איטרטור על הטווח ורצה באמצעות __next__
על כל האלמנטים עד לסיום הלולאה.
כמובן שאת כל מה שתיארתי בפיסקה הקודמת אתם לא צריכים לדעת כדי לכתוב לולאה, ומתכנתי פייתון רבים שאני מכיר באמת לא מכירים את המנגנון מבפנים.
נשווה עם לולאה דומה בשפת ruby:
10.times do |i|
puts i
end
כאן אנחנו כבר צריכים לעצור ולחשוב מה בעצם אנחנו רואים. הלולאה times
היא הפעם מתודה של האוביקט 10 שהוא מספר, והיא מקבלת כפרמטר בלוק של פקודות. אני לא חושב שאפשר בכלל לטעות ולחשוב שאתה מבין את הקוד הזה בלי להבין מה זה קריאה למתודה, מה זה בלוק ומה זה אומר להעביר בלוק לפונקציה. רובי לא נותנת תחושה שהנה הכל בסדר וזה בסך הכל לולאת for each שאנחנו מכירים משפות אחרות, אלא התחביר עצמו דואג להבהיר לנו שצריך ללכת לקרוא ולהבין מה קורה שם.
2. כלים לעומת פתרונות
הגישה ברובי היא לנסות להרחיב את השפה כמה שיותר לכיוון פיתוח כלים בסיסיים ולתת למתכנתים לראות איך להתאים אותם ולבנות מהם פתרונות לבעיות ספציפיות.
הגישה בפייתון היא לראות עם איזה בעיות מתכנתים מתמודדים ולהוסיף לשפה דרכים קלות לפתור בעיות אלו.
הבדל זה בגישות מביא לתוצאות מעניינות. בין השאר זה אומר שאם אתם ב Python כדאי שתחפשו את הדרך המומלצת לפתור את הבעיה שלכם. אם אתם ב Ruby כדאי לחפש כמה דרכים שונות בהם אנשים פתרו בעיות דומות ולראות מה הכי יתאים לכם, או אם אפשר לקחת את הכלים ולבנות לכם פתרון רלוונטי.
דוגמא קלאסית היא העברת בלוקים לפונקציה. ברובי אחד הרעיונות הבסיסיים בשפה הוא היכולת להעביר בלוק כפרמטר לפונקציה, ואז הפונקציה יכולה לקרוא לבלוק כשהיא מוצאת לנכון. זה מה שאיפשר לנו ב Ruby לבנות לולאה בתור הפעלת פונקציה של אוביקט ולקבל קוד שנראה מאוד הגיוני:
a = [10, 20, 30]
a.each do |item|
puts "item: #{item}"
end
ומה אם רוצים להוסיף גם את האינדקס? אין בעיה, פשוט נקרא לפונקצית לולאה אחרת:
a = [10, 20, 30]
a.each_with_index do |item, index|
puts "Index: #{index}, item: #{item}"
end
אבל בפייתון יש רק לולאה אחת for וכבר דיברנו על המורכבות שלה. ברור שפייתון לא הולכת להוסיף עוד פונקצית לולאה כל פעם שמישהו ירצה יכולות קצת אחרות בסריקה. הפתרון של פייתון הוא הפונקציה enumerate שמקבלת אוביקט שאפשר לסרוק ומחזירה Generator שכולל גם את האינדקסים:
a = [10, 20, 30]
for index, item in enumerate(a):
print("index = {}, item = {}".format(index, item))
end
שני הבלוקים רק נראים דומים אבל מעידים על גישה שונה של השפה. ברובי העברת בלוק לפונקציה פתרה את כל הבעיות שקשורות ללולאות. רוצים סוג אחר לגמרי של לולאה? אין בעיה תכתבו בעצמכם. בפייתון אין אפשרות להרחיב את השפה עם סוגים חדשים של לולאות, אבל כן אפשר לכתוב פונקציות Generators שפותרות בעיות ספציפיות (להוסיף אינדקס ללולאת for).
דוגמא נוספת היא נושא ה Property. בשתי השפות הגישה היא לא לכתוב Getters ו Setters מראש בעת שכותבים מחלקה אלא לרשום ישירות ל Data Members של המחלקה. אם יעלה הצורך להוסיף קוד שירוץ בעת קריאה או כתיבה של ערך תמיד אפשר להוסיף אותו אחר כך.
ניקח לדוגמא מחלקה שמייצגת מוצר. למוצר יש שם ומחיר, ובשלב ראשון אנחנו מקבלים ערכים אלו בבנאי. בפייתון המחלקה נראית כך:
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
p = Product('foo', 10)
print(p.price)
ואילו קוד רובי מקביל נראה כך:
class Product
attr_reader :price, :name
def initialize(name, price)
@name = name
@price = price
end
end
p = Product.new('foo', 10)
puts p.price
שימו לב שברובי כן יצרנו את ה getter עבור השדות, פשוט באופן אוטומטי וללא המימוש.
עכשיו מה יקרה כשנרצה להוסיף מע"מ למחיר, בלי לשנות את הקוד שמדפיס את המחיר? כלומר נרצה לשנות את הגדרת המחלקה כך שהדפסת המחיר תדפיס מחיר גבוה ב 17% מזה שהעברנו בבנאי.
מבחינת רובי אין בעיה. הקריאה שלכם ל price ממילא הפעילה פונקציה, וכל השינוי שנדרש זה לכתוב בעצמנו את קוד הפונקציה במקום זה שיוצר אוטומטית:
class Product
attr_reader :price, :name
def initialize(name, price)
@name = name
@price = price
end
def price
@price * 1.17
end
end
p = Product.new('foo', 10)
puts p.price
פייתון לעומתה מציעה שתשתמשו ב Properties:
class Product:
def __init__(self, name, price):
self.name = name
self._price = price
@property
def price(self):
return self._price * 1.17
p = Product('foo', 10)
print(p.price)
ברובי ממילא אפשר להפעיל פונקציות גם בלי סוגריים, ולכן מבחינת הקוד הקורא אין הבדל אם הפעלנו או לא הפעלנו פונקציה. בנוסף הפונקציה attr_reader
והאפשרות לייצר פונקציות בצורה דינמית ונוחה בתוך המחלקה יצרו מצב שאנחנו תמיד עובדים עם פונקציות getters.
אבל פייתון צריכה לקפוץ דרך חישוקים צרים כדי להתמודד עם הנושא של הפעלת פונקציה ללא סוגריים. אם תכנסו לקוד של @property
תגלו שבשביל להבין מה זה עושה אתם צריכים ללמוד על Decorators ו Descriptors ועוד מושגים נחמדים שמאפשרים את הלוליינות הזו. הפתרון קיים והוא גם נראה אלגנטי, אבל התחושה שהוא מאוד ממוקד ב Use Case של שינוי קוד מחלקה בלי לשנות את הקוד הקורא.
3. מחשבות קדימה
יש עוד המון הבדלים בין השפות וברור שפערי התרבויות הם משהו שאנחנו כמתכנתים מרגישים אותו בכל שפה חדשה. אני לא חושב שיש ייתרון לגישה אחת או אחרת, ואני די בטוח שפערי תרבויות בין שפות זה לא משהו שצריך לגרום לנו לבחור בשפה אחת או אחרת. יחד עם זאת תוך כדי למידת שפה חדשה כדאי לנסות לאפיין מה הגישה של המתכנתים שכותבים בה ואיך גישה זו מסתדרת עם עולם התוכן שלנו.