פרל, פייתון או רובי: באיזו שפה הכי קל לכתוב אופרטור השוואה למחלקה שלכם
פוסט זה כולל טיפ קצר לעבודה יעילה עם Python. אם אתם רוצים ללמוד פייתון יותר לעומק אני ממליץ על קורס Python כאן באתר.
הקורס כולל עשרות שיעורי וידאו והמון תרגול מעשי וילמד אתכם Python בצורה מקצועית מההתחלה ועד הנושאים המתקדמים.
שפות מונחות עצמים רבות מציעות אפשרות לדרוס את משמעות האופרטורים הבנויים בשפה, למשל אופרטורי השוואה, אופרטורים נומריים, המרות ועוד. דריסת אופרטורים נשמעת בהתחלה כמו רעיון טוב, אך אם לא מטופלת נכון יכולה להזיק. בואו נראה כיצד פועלת דריסת אופרטור ההשואה ב-3 שפות מונחות עצמים.
1. דריסת אופרטור השוואה בפרל
תכונה שאני מאוד אוהב בפרל היא היעדר אופרטורים להשוואת רשימות איבר-איבר והשוואת מילונים איבר-איבר. ההשואה בפרל:
my @a = (10, 20, 30);
my @b = (‘a’, ‘b’, ‘c’);
my @c = (10, 20);
# True
@a == @b
# False
@b == @c
לפרל שתי קבוצות אופרטורים להשוואה, האחת עבור השוואה נומרית והשניה משמשת להשוואה מחרוזתית. זה הכל. מרגע שהבנתם את זה אתם עשויים להתעצבן כשתגיעו להשוות שני מערכים, אך לפחות לא תתבלבלו באיזה אופרטור השוואה להשתמש.
במחלקות ואובייקטים שלנו הפשטות עשויה להיעלם. התוכנית הבאה מדפיסה False:
use strict;
use warnings;
use v5.20;
package Point {
use Moose;
has 'x', is => 'ro', isa => 'Num', required => 1;
has 'y', is => 'ro', isa => 'Num', required => 1;
}
my $p = Point->new(x => 10, y => 20);
my $q = Point->new(x => 10, y => 20);
say "p == q: ", ($p == $q ? "True" : "False");
מתכנתים יצירתיים עשויים לתהות: הרי שתי הנקודות מייצגות את אותו המקום במרחב, למה אם כן שלא יהיו שווים? אולי אפשר לשנות את אופרטור ההשוואה הנומרית כך שבהפעלה על נקודות ישווה את ערכי ה-x וה-y של הנקודות? התשובה היא שאפשר אך לא מומלץ.
התוכנית הבאה כבר תדפיס True:
use strict;
use warnings;
use v5.20;
package Point {
use Moose;
use overload
'==' => \&eq;
has 'x', is => 'ro', isa => 'Num', required => 1;
has 'y', is => 'ro', isa => 'Num', required => 1;
sub eq {
my ($self, $other) = @_;
$self->x == $other->x && $self->y == $other->y
}
}
my $p = Point->new(x => 10, y => 20);
my $q = Point->new(x => 10, y => 20);
say "p == q: ", ($p == $q ? "True" : "False");
הבעייה היא שברגע שלוקחים סמנטיקה של השפה ומשנים אותה בהתאם למרחב הבעייה אנו מתחילים לפגוע בקריאות. מי שכותב פרל מגיע עם ציפיות מסוימות לאופרטורים הקיימים, בפרט הציפייה שעבור סקלארים השוואה נומרית תתן ערך אמת אם ורק אם הם מתיחסים לאותו האובייקט ממש. בפרל לא נהוג להשתמש באובייקטים בתור מפתחות ב Hash, אך אם בטעות מישהו כן ינסה להשתמש במחלקת הנקודה שכתבנו כמפתח ב Hash האובייקט יתורגם למחרוזת המייצגת את כתובתו בזכרון ואז שני האובייקטים שלנו יתפקדו כמפתחות שונים.
פרל מאפשרת דריסה של אופרטור השוואה, אך העובדה שמעט מאוד מודולים בשפה משתמשים ביכולת זו העמסה שכזו עשויה רק לבלבל. במקום דריסת אופרטור, העדיפו להשאר עם פונקציית השוואה מפורשת.
2. דריסת אופרטור השוואה ברובי
המעבר לרובי מכניס אותנו גם לתרבות אחרת. לרובי 4 סימני שיוויון שונים במקום ה-2 של פרל: אופרטור ==, אופרטור === להשוואת case, הפונקציה eql? להשוואת מפתחות ב Hash והפונקציה equal? להשוואת זהות.
אחרי שמתגברים על הבלבול הראשוני יש משהו נחמד במוסכמה זו, שכן כעת אנו יכולים לדרוס את אופרטור ההשוואה כך שיבצע השוואה ספציפית למחלקה, ומתכנתי רובי שיגיעו בהמשך ידעו שזו היתה הכוונה:
class Point
attr_reader :x, :y
def initialize(x:, y:)
@x = x
@y = y
end
def ==(other)
return super.==(other) if ! other.is_a?(Point)
@x == other.x && @y == other.y
end
def hash
@x.hash & @y.hash
end
alias_method :eql?, :==
end
p = Point.new(x: 10, y: 20)
q = Point.new(x: 10, y: 20)
puts p == q ? "p == q" : "p != q"
puts p.equal?(q) ? "p equals q" : "p not equals q"
רובי מאפשרת שימוש בכל אובייקט בתור מפתח ב Hash, ולכן כל דריסת אופרטור ההשוואה מחייבת גם דריסת הפונקציה eql? והגדרת הפונקציה hash. הפוקנציה eql? מבצעת השוואה בין שני אובייקטים כאשר מנסים להשתמש בהם כמפתחות ב Hash, והפונקציה hash מחזירה את תוצאת פונקציית הגיבוב עבור האובייקט.
גם לאחר הדריסה נשארנו עם פונקציית equal? המקורית המבצעת השוואת כתובות בזכרון. כך כל עוד המוסכמה נשמרת גם הקוד נשאר קריא. שווה לציין שאין לרובי מנגנון המונע מכם לדרוס את הפונקציה equal?, כך שאם אתם מחליטים לכתוב קוד גרוע היחידים שיהיו עצובים מזה הם החברים שיצטרכו לתחזק קוד זה.
3. דריסת אופרטור ההשואה בפייתון
פייתון היא הקשוחה ביותר מבין השפות. אתם מוזמנים לדרוס את אופרטור ההשוואה == בלבד, בעוד שהשוואת אובייקטים באמצעות is אינה ניתנת לשינוי. אין גם פונקציה ייעודית להשוואת אובייקטים עבור hash, ובפייתון אנו מסתפקים בפונקציה hash המחזירה את ערך הגיבוב של האובייקט.
למרות עומס הסימנים הקוד בפייתון מציע את האחידות הטובה ביותר בין תוכניות. כך זה נראה:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self,other):
if isinstance(other, Point):
return self.x == other.x and self.y == other.y
else:
return NotImplemented
def __hash__(self):
return hash(self.x) & hash(self.y)
p = Point(x=10, y=20)
q = Point(x=10, y=20)
print "p == q" if p == q else "f"
print "p is q" if p is q else "f"
print "t" if p == 10 else "f"
4. סיכום והערות
ההחלטה כיצד לקרוא לפונקציית ההשוואה שלכם צריכה להתאים קודם כל למוסכמות של השפה והציפיות של מתכנתים אחרים שיגיעו לקרוא את הקוד. מתכנתי פרל רגילים לשני סוגי השוואות ולכן מומלץ למממש המרה למספר או המרה למחרוזת במקומות הרלוונטים, ולהשתדל להימנע מדריסת אופרטורים.
מתכנתי רובי יודעים להשתמש באופרטור ההשוואה כדי לקבל השוואה מותאמת למחלקה, ולכן מומלץ לדרוס אופרטור זה יחד עם הפונקציה eql? והפונקציה hash. נשים לב שרובי היא היחידה שגוזרת מתוך מימוש == גם את המימוש לפונקציה !=. בשתי השפות האחרות תצטרכו לדרוס את האופרטור ״שונה מ״ בנפרד, או למממש אופרטור השוואה כללי יותר (למשל cmp בפייתון או <=> בפרל).
מתכנתי פייתון קיבלו את העבודה הקלה ביותר הפעם. מאוד ברור כיצד הפונקציה eq צריכה להתנהג ומה עליה להחזיר בכל מצב, ואין דרך לדרוס את הפונקציה is כך שגם מתכנתים מבולבלים במיוחד לא יוכלו לכתוב קוד מטעה.
תוכנית הפרל היא היחידה שהשתמשה בספריה חיצונית (Moose) לצורך כתיבת קוד המחלקה. קהילת הפרל הצביעה ברגליים לכיוון השימוש במודול זה ודומיו, וכיום כמעט כל קוד פרל חדש שנכתב בסגנון מונחה עצמים ישתמש בתחביר של Moose. יתרון של Moose על פני שתי השפות האחרות הוא ביכולת להגדיר את טיפוס הקלט לו אנו מצפים. תוכנית הפרל היא היחידה מבין התוכניות שבודקת גם שהפרמטרים שקיבלה הם אכן מספרים.