תחביר פייתון נפוץ במדעי הנתונים (בסיסי)

בימים האחרונים אני קורא את הספר Data Science from Scratch (כתובת PDF), שהוא ספר מבוא מצוין וקל להבנה למדעי הנתונים. אחד הפרקים מציג את התחביר הבסיסי של פייתון ואת התחביר המתקדם הנפוץ במדעי הנתונים. מצאתי שההסבר מצוין, תמציתי וברור, ולכן אני מתרגם אותו לכאן כהערה לעצמי. תחביר פייתון נפוץ במדעי הנתונים (בסיסי) תחביר פייתון נפוץ במדעי הנתונים (מתקדם)

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

פורמט רווחים (Indentation)

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

for i in [1, 2, 3, 4, 5]:
    print i          # השורה הראשונה בלולאת "for i"
    for j in [1, 2, 3, 4, 5]:
        print j      # השורה הראשונה בלולאת "for j"
        print i + j  # השורה האחרונה בלולאת "for j"
    print i          # השורה האחרונה בלולאת "for i"
print "done looping"

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

long_winded_computation = (1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20)

וזה גם הופך את הקוד לקריא יותר:

list_of_lists = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
easier_to_read_list_of_lists = [ [1, 2, 3],
                                 [4 ,5 ,6 ],
                                 [7 ,8 ,9 ] ]

ביטויים מרובי שורות

ניתן להשתמש בלוכסן אחורי (backslash) כדי לחבר שתי שורות שנשברו (פרקטיקה זו נדירה):

two_plus_three = 2 + \
                 3

מודולים (Modules)

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

  1. פשוט לייבא את המודול כולו ישירות:
import re
my_regex = re.compile("[0-9]+", re.I)

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

  1. אם שם המודול שמיובא כבר נמצא בשימוש בקוד, ניתן למפות את המודול לשם אחר בעת הייבוא:
import re as regex
my_regex = regex.compile("[0-9]+", regex.I)
  1. אם אתם “רעים”, אתם יכולים לייבא את המודול כולו למרחב השמות הנוכחי, מה שעלול בטעות לדרוס משתנים שכבר הגדרתם:
match = 10
from re import *  # למודול re יש פונקציה בשם match
print match       # מדפיס את הפונקציה match

מכיוון שאתם אנשים טובים, אני בטוח שלא תעשו זאת.

פעולות חשבון (Arithmetic)

פייתון 2.7 משתמשת כברירת מחדל בחלוקת שלמים, ולכן $5 / 2 = 2$. אך לעיתים קרובות איננו רוצים חלוקת שלמים, ולכן ניתן לייבא את המודול הזה:

from __future__ import division

לאחר הייבוא, נקבל $5 / 2 = 2.5$. חלוקת שלמים: $5 // 2 = 2$.

פונקציות (Functions)

הגדרת פונקציות

פונקציה היא כלל שיכול לקבל 0 או יותר קלטים, ולהחזיר פלט מסוים. בפייתון, אנו משתמשים ב-def שם_הפונקציה(פרמטרים) כדי להגדיר פונקציה:

def double(x):
    """כאן ניתן לכתוב הסבר על תפקיד הפונקציה
    לדוגמה, פונקציה זו מכפילה את הקלט ב-2"""
    # כאן ניתן לכתוב את גוף הפונקציה, זכרו להזיח
    return x * 2

שימוש בפונקציות

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

def apply_to_one(f):
    """קורא לפונקציה f ומעביר 1 כארגומנט לפונקציה"""
    return f(1)
my_double = double          # double מפנה לפונקציה שהוגדרה בסעיף הקודם
x = apply_to_one(my_double) # x שווה ל-2

פונקציות אנונימיות (Lambda)

ניתן גם ליצור פונקציות אנונימיות באמצעות lambda:

y = apply_to_one(lambda x: x + 4)     # שווה ל-5

ניתן להקצות lambda למשתנים אחרים, אך רוב האנשים ימליצו לכם להשתמש ב-def במידת האפשר:

another_double = lambda x: 2 * x      # לא מומלץ
def another_double(x): return 2 * x   # מומלץ

הערות נוספות:

העברת פרמטרים לפונקציות

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

def my_print(message="my default message"):
    print message
my_print("hello")     # פלט: "hello"
my_print()            # פלט: "my default message"

לפעמים שימושי גם לציין פרמטרים ישירות לפי שמם:

def subtract(a=0, b=0):
    return a - b
subtract(10, 5)   # מחזיר 5
subtract(0, 5)    # מחזיר -5
subtract(b=5)     # זהה לקודם, מחזיר -5

מחרוזות (Strings)

ניתן ליצור מחרוזות באמצעות גרש בודד או כפול (הגרשיים חייבים להיות תואמים):

single_quoted_string = 'data science'
double_quoted_string = "data science"

השתמשו בלוכסן אחורי כדי לציין תווי מילוט, לדוגמה:

tab_string = "\t"      # מייצג תו טאב
len(tab_string)        # שווה ל-1

כאשר אתם רוצים להשתמש בלוכסן אחורי עצמו (עבור תיקיות Windows או ביטויים רגולריים), תוכלו להגדיר אותו באמצעות מחרוזות גולמיות r"":

not_tab_string = r"\t" # מייצג את התווים '\' ו-'t'
len(not_tab_string)    # שווה ל-2

השתמשו בשלושה גרשיים כפולים כדי ליצור מחרוזות מרובות שורות:

multi_line_string = """זו השורה הראשונה
זו השורה השנייה
זו השורה השלישית"""

טיפול בחריגות (Exception Handling)

כאשר מתרחשת שגיאה בתוכנית, פייתון מעלה חריגה (exception). אם לא נטפל בה, התוכנית תופסק. ניתן ללכוד חריגות באמצעות פקודות try ו-except:

try:
    print 0 / 0
except ZeroDivisionError:
    print "לא ניתן לחלק באפס"

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

רשימות (Lists)

יצירת רשימות

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

integer_list = [1, 2, 3]
heterogeneous_list = ["string", 0.1, True]
list_of_lists = [ integer_list, heterogeneous_list, [] ]
list_length = len(integer_list)   # שווה ל-3
list_sum = sum(integer_list)      # שווה ל-6

גישה לערכים ברשימה

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

x = range(10)       # הרשימה x = [0, 1, ..., 9]
zero = x[0]         # שווה ל-0, אינדקסים מתחילים מ-0
one = x[1]          # שווה ל-1
nine = x[-1]        # שווה ל-9, האלמנט האחרון ברשימה
eight = x[-2]       # שווה ל-8, האלמנט הלפני אחרון ברשימה
x[0] = -1           # הרשימה הנוכחית x = [-1, 1, 2, 3, ..., 9]

חיתוך רשימות (Slicing)

ניתן לחתוך רשימות באמצעות סוגריים מרובעים:

first_three = x[:3]                  # [-1, 1, 2]
three_to_end = x[3:]                 # [3, 4, ..., 9]
one_to_four = x[1:5]                 # [1, 2, 3, 4]
last_three = x[-3:]                  # [7, 8, 9]
without_first_and_last = x[1:-1]     # [1, 2, ..., 8]
copy_of_x = x[:]                     # [-1, 1, 2, ..., 9]

ניתן להשתמש ב-in כדי לבדוק אם אלמנט מסוים נמצא ברשימה:

1 in [1, 2, 3]        # True
0 in [1, 2, 3]        # False

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

שרשור רשימות

בפייתון קל מאוד לשרשר שתי רשימות:

x = [1, 2, 3]
x.extend([4, 5, 6])   # x הנוכחי = [1,2,3,4,5,6]

אם אינכם רוצים לשנות את הרשימה המקורית x, תוכלו להשתמש באופרטור “חיבור” כדי ליצור רשימה חדשה:

x = [1, 2, 3]
y = x + [4, 5, 6]     # y הנוכחי = [1, 2, 3, 4, 5, 6]; x לא השתנה

לעתים קרובות מוסיפים אלמנט אחד לרשימה בדרך זו:

x = [1, 2, 3]
x.append(0)           # x הנוכחי = [1, 2, 3, 0]
y = x[-1]             # שווה ל-0
z = len(x)            # שווה ל-4

פירוק רשימות (Unpacking)

אם אתם יודעים כמה אלמנטים יש ברשימה, קל מאוד לפרק אותה:

x, y = [1, 2]         # x הנוכחי = 1, y = 2

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

_, y = [1, 2]         # y == 2, ללא קשר לאלמנט הראשון

טופלים (Tuples)

רשימות וטופלים דומים מאוד. ההבדל היחיד מרשימות הוא שאת האלמנטים בטופל לא ניתן לשנות.

יצירת טופלים

ניתן ליצור טופלים באמצעות סוגריים עגולים או ללא סוגריים כלל:

my_tuple = (1, 2)
other_tuple = 3, 4
my_list[1] = 3        # my_list הנוכחי הוא [1, 3]
try:
    my_tuple[1] = 3
except TypeError:
    print "לא ניתן לשנות טופל"

באמצעות טופלים ניתן לקבל מספר ערכי החזרה מפונקציה בצורה נוחה מאוד:

def sum_and_product(x, y):
    return (x + y),(x * y)
sp = sum_and_product(2, 3)    # שווה ל-(5, 6)
s, p = sum_and_product(5, 10) # s = 15, p = 50

טופלים (ורשימות) תומכים בהקצאת ערכים מרובים בו זמנית:

x, y = 1, 2       # x הנוכחי = 1, y = 2
x, y = y, x       # מחליף את ערכי שני המשתנים בפייתון; x הנוכחי = 2, y = 1

מילונים (Dictionaries)

יצירת מילונים

מבנה נתונים בסיסי נוסף בפייתון הוא המילון, המאפשר לכם לקבל במהירות את הערך המתאים באמצעות מפתח (key):

empty_dict = {}                       # הגדרת מילון ריק "פייתוןית" מאוד
empty_dict2 = dict()                  # הגדרת מילון ריק פחות "פייתוןית"
grades = { "Joel" : 80, "Tim" : 95 }  # אחסון מילון

חיפוש אלמנטים במילון

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

joels_grade = grades["Joel"]          # שווה ל-80

אם המפתח שאתם מחפשים אינו נמצא במילון, תוחזר שגיאת KeyError:

try:
    kates_grade = grades["Kate"]
except KeyError:
    print "אין ציון עבור Kate!"

ניתן להשתמש ב-in כדי לבדוק אם מפתח נמצא במילון:

joel_has_grade = "Joel" in grades     # True
kate_has_grade = "Kate" in grades     # False

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

joels_grade = grades.get("Joel", 0)   # שווה ל-80
kates_grade = grades.get("Kate", 0)   # שווה ל-0
no_ones_grade = grades.get("No One")  # מחזיר את ערך ברירת המחדל None

שינוי מילונים

ניתן להשתמש בסוגריים מרובעים כדי ליצור או לשנות זוגות מפתח-ערך במילון:

grades["Tim"] = 99                    # מחליף את הערך הישן
grades["Kate"] = 100                  # מוסיף זוג מפתח-ערך
num_students = len(grades)            # שווה ל-3

לעתים קרובות נשתמש במילונים בדרך זו כדי לבטא את מבנה הנתונים:

tweet = {
    "user" : "joelgrus",
    "text" : "Data Science is Awesome",
    "retweet_count" : 100,
    "hashtags" : ["#data", "#science", "#datascience", "#awesome", "#yolo"]
}

בנוסף לחיפוש מפתח ספציפי, אנו יכולים גם לבצע פעולות על כל המפתחות כך:

tweet_keys = tweet.keys()             # מקבל רשימת מפתחות
tweet_values = tweet.values()         # מקבל רשימת ערכים
tweet_items = tweet.items()           # מקבל טופלים של (מפתח, ערך)
"user" in tweet_keys                  # מחזיר True, משתמש בחיפוש in ברשימה (פחות יעיל)
"user" in tweet                       # שימוש יותר "פייתוןי", משתמש בחיפוש in יעיל במילון
"joelgrus" in tweet_values            # True

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

מילונים עם ערך ברירת מחדל (Default Dictionaries)

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

word_counts = {}
for word in document:
    if word in word_counts:
        word_counts[word] += 1
    else:
        word_counts[word] = 1

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

word_counts = {}
for word in document:
    try:
        word_counts[word] += 1
    except KeyError:
        word_counts[word] = 1

השיטה השלישית היא להשתמש ב-get, שיטה זו מצטיינת בטיפול במפתחות חסרים:

word_counts = {}
for word in document:
    previous_count = word_counts.get(word, 0)
    word_counts[word] = previous_count + 1

מילון ברירת המחדל (defaultdict) זהה למילון רגיל, ההבדל היחיד הוא שכאשר אתם מנסים לחפש מפתח שאינו קיים במילון, הוא ייצור אוטומטית זוג מפתח-ערך באמצעות המפתח שסיפקתם. כדי להשתמש במילון ברירת המחדל, עליכם לייבא את ספריית collections:

from collections import defaultdict
word_counts = defaultdict(int)        # int() מייצר 0
for word in document:
    word_counts[word] += 1

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

dd_list = defaultdict(list)           # list() מייצר רשימה ריקה
dd_list[2].append(1)                  # dd_list הנוכחי הוא {2: [1]}
dd_dict = defaultdict(dict)           # dict() מייצר מילון ריק
dd_dict["Joel"]["City"] = "Seattle"   # dd_dict הנוכחי הוא { "Joel" : { "City" : "Seattle"}}
dd_pair = defaultdict(lambda: [0, 0]) # יצר מילון שבו הערך עבור מפתח הוא רשימה
dd_pair[2][1] = 1                     # dd_pair הנוכחי הוא {2: [0,1]}

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

מונה (Counter)

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

from collections import Counter
c = Counter([0, 1, 2, 0]) # c (בערך) הוא { 0 : 2, 1 : 1, 2 : 1 }

כך יש לנו שיטה נוחה מאוד לספירת תדירות מילים:

word_counts = Counter(document)

למונה יש גם שיטה נפוצה מאוד, most_common, שיכולה לקבל ישירות את המילים הנפוצות ביותר ואת התדירות המתאימה להן:

# מדפיס את 10 המילים הנפוצות ביותר ואת ספירתם
for word, count in word_counts.most_common(10):
    print word, count

קבוצות (Sets)

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

s = set()
s.add(1)          # s הוא { 1 }
s.add(2)          # s הוא { 1, 2 }
s.add(2)          # s הוא { 1, 2 }
x = len(s)        # שווה ל-2
y = 2 in s        # שווה ל-True
z = 3 in s        # שווה ל-False

שתי סיבות עיקריות לשימוש בקבוצות:

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

stopwords_list = ["a","an","at"] + hundreds_of_other_words + ["yet", "you"]
"zip" in stopwords_list               # נכשל, צריך לבדוק כל אלמנט
stopwords_set = set(stopwords_list)
"zip" in stopwords_set                # החיפוש מצליח, ומהיר מאוד

שנית, שימוש בקבוצות נוח מאוד לקבלת אלמנטים ייחודיים מקבוצת נתונים:

item_list = [1, 2, 3, 1, 2, 3]
num_items = len(item_list)            # 6
item_set = set(item_list)             # {1, 2, 3}
num_distinct_items = len(item_set)    # 3
distinct_item_list = list(item_set)   # [1, 2, 3]

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

הצהרות תנאי (Conditional Statements)

ברוב מוחלט של שפות התכנות, תוכלו להשתמש ב-if כדי לבטא ענפים מותנים כך:

if 1 > 2:
    message = "הלוואי ש-1 היה גדול מ-2…"
elif 1 > 3:
    message = "elif מייצג 'else if'"
else:
    message = "כאשר כל השאר נכשל, השתמשו ב-else (אם אתם רוצים)"

ניתן גם לכתוב הצהרות תנאי בשורה אחת כך, אך זה פחות נפוץ:

parity = "even" if x % 2 == 0 else "odd"

הצהרות לולאה (Loop Statements)

לולאת while

לולאת while בפייתון:

x = 0
while x < 10:
    print x, "קטן מ-10"
    x += 1

לולאת for

נפוץ יותר להשתמש בלולאת for-in:

for x in range(10):
    print x, "קטן מ-10"

עבור ביטויים לוגיים מורכבים יותר, ניתן להשתמש בפקודות continue ו-break:

for x in range(10):
    if x == 3:
        continue          # עובר ישירות לסיבוב הבא של הלולאה
    if x == 5:
        break             # יוצא לגמרי מהלולאה
    print x

התוצאה תהיה הדפסת 0, 1, 2 ו-4.

אמת (Truthiness)

משתנים בוליאניים בפייתון פועלים בדומה לשפות אחרות, ההבדל היחיד הוא שהאות הראשונה חייבת להיות גדולה:

one_is_less_than_two = 1 < 2      # True
true_equals_false = True == False # False

פייתון משתמשת ב-None כדי לייצג העדר ערך, בדומה ל-null בשפות אחרות:

x = None
print x == None        # מדפיס True, פחות אלגנטי
print x is None        # מדפיס True, יותר אלגנטי

פייתון מאפשרת לכם להשתמש בערכים אחרים במקום ערכים בוליאניים, והבאים כולם שווים ל-False:

באופן דומה, ישנם גם ערכים רבים השקולים ל-True, מה שמאפשר לכם לבדוק בקלות רשימות לא ריקות, מחרוזות לא ריקות, מילונים לא ריקים וכן הלאה.

כמובן, אם אינכם יכולים לצפות את התוצאה, אתם עלולים להיתקל בשגיאות במהלך השימוש:

s = some_function_that_returns_a_string()
if s:
    first_char = s[0]
else:
    first_char = ""

גישה פשוטה יותר, אשר משיגה את אותה תוצאה כמו הגישה לעיל:

first_char = s and s[0]

אם הערך הראשון הוא אמת, יוחזר הערך השני; אחרת, יוחזר הערך הראשון.

באופן דומה, אם x יכול להיות מספר או ריק, כך תוכלו לקבל x שבוודאות יהיה מספר:

safe_x = x or 0

בפייתון קיימת גם הפונקציה all, המחזירה True אם כל אלמנט הוא True. הפונקציה any מחזירה True אם לפחות אלמנט אחד הוא True. לדוגמה, עבור רשימה שבה כל אלמנט הוא “אמיתי”, הפונקציה all תחזיר True, אחרת היא תחזיר False:

all([True, 1, { 3 }])       # True
all([True, 1, {}])          # False, {} שווה ל-"False"
any([True, 1, {}])          # True
all([])                     # True, אין אלמנט ששווה ל-"False"
any([])                     # False, אין אלמנט ששווה ל-"True"

קריאה נוספת: תחביר פייתון נפוץ במדעי הנתונים (מתקדם)