יסודות התכנות בשפת Python · פרק 5 — פעולות על מחרוזות⌂📚🏠
פרק 5

פעולות על מחרוזות

20 שעות לימוד

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

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

5.1 הגדרה ושימוש בסיסי במחרוזות, הגדרת המחרוזת הריקה

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

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

device = "voltage sensor"
port = 'A0'
note = "the 'red' wire"
print(device)
print(note)
print(type(port))
print(len(device))
print(len(port))
print("line 1\nline 2")
print(len("A\nB"))
פלט
voltage sensor
the 'red' wire
<class 'str'>
14
2
line 1
line 2
3

הפעולה len ספרה גם את הרווח שבין המילים. הסימן \n פירושו מעבר לשורה חדשה: בקוד כותבים אותו בשני סימנים, אבל במחרוזת הוא תו אחד, ולכן האורך של "A\nB" הוא 3.

מחרוזת ריקה: מחרוזת שאין בה תווים: שתי מירכאות צמודות "", ואורכה אפס. היא שונה מ-" ", שהיא מחרוזת עם תו אחד, הרווח.
empty = ""
space = " "
print(len(empty), len(space))
print(empty == space)
print(bool(empty), bool(space))
פלט
0 1
False
False True
דוגמה 5.1
קוראים שם של חיישן. אם המשתמש הקיש Enter בלי לכתוב דבר, input מחזירה מחרוזת ריקה. נכתוב מסננת קלט כמו שלמדנו בפרק 4, ולא נקבל שם ריק.
פתרון:
name = input("Sensor name: ")
while name == "":
    print("Name is empty")
    name = input("Sensor name: ")
print("Saved:", name)
פלט
Sensor name: 
Name is empty
Sensor name: TMP36
Saved: TMP36
טעות נפוצה: השוואה בין מחרוזות בודקת תו אחר תו משמאל, ואינה מבינה את משמעות הספרות. לכן "9" גדולה מ-"12" כמחרוזות, כי התו 9 בא אחרי התו 1. אות גדולה שונה מאות קטנה, ומחרוזת לעולם אינה שווה למספר:
print("on" == "ON")
print("12" == 12)
print("9" > "12")
print(9 > 12)
פלט
False
False
True
False
שאלה 5.1: מה מדפיסה התוכנית הבאה?
a = ""
b = "A0 1"
print(len(a) + len(b))
print(a == " ")
print(b == "a0 1")
print("5" > "10")

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

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

L0-8E1-7D2-63-514-45-3O6-2N7-1
האינדקסים של המחרוזת "LED 1 ON": החיוביים מעל התווים מתחילים ב-0, והשליליים מתחתם סופרים מן הסוף ומתחילים ב-−1
cmd = "LED 1 ON"
print(cmd[0], cmd[4], cmd[7])
print(cmd[-1], cmd[-2])
print(cmd[len(cmd) - 1])
print(type(cmd[0]))
פלט
L 1 N
N O
N
<class 'str'>

השורה האחרונה מראה שגם תו בודד בפייתון הוא מטיפוס str: מחרוזת שאורכה אחד. אין טיפוס מיוחד לתו.

דוגמה 5.2
על לוח המעגל לכל רכיב יש כתובת: התו הראשון מציין את סוגו, והספרות שאחריו את מספרו, למשל "R12" ו-"C7". נקרא כתובת ונדפיס את סוג הרכיב.
פתרון:
addr = input("Address: ")
if addr[0] == "R":
    print("Resistor")
elif addr[0] == "C":
    print("Capacitor")
else:
    print("Other part")
פלט
Address: C7
Capacitor
טעות נפוצה: האינדקס הגדול ביותר במחרוזת באורך שלושה הוא 2. האינדקס 3 מחוץ למחרוזת, והוא עוצר את התוכנית בשגיאה IndexError. למחרוזת ריקה אין בכלל תו ראשון, ולכן כשלא יודעים מה הגיע כותבים addr != "" and addr[0] == "R", ונשענים על קיצור הדרך בחישוב מפרק 3.
טעות נפוצה: מחרוזת אינה ניתנת לשינוי: אפשר לקרוא כל תו בה, אבל אי אפשר להציב במקומו תו אחר. כדי לקבל מחרוזת שונה בונים מחרוזת חדשה, ונראה איך בסעיף הבא:
addr = "R12"
addr[0] = "C"
פלט
TypeError: 'str' object does not support item assignment

מעבר על התווים:

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

tag = "R12"
i = 0
while i < len(tag):
    print(i, tag[i])
    i += 1
מעקב אחר המעבר על תווי המחרוזת
פקודהtagii < len(tag)פלט
tag = "R12"'R12'
i = 00
while i < len(tag):True
print(i, tag[i])0 R
i += 11
while i < len(tag):True
print(i, tag[i])1 1
i += 12
while i < len(tag):True
print(i, tag[i])2 2
i += 13
while i < len(tag):False

התנאי הוא i < len(tag) ולא i <= len(tag): כש-i מגיע ל-3 המחרוזת כבר נגמרה, ואין להיכנס לסבב נוסף.

תרגול מהיר — אינדקס, עם cmd = "LED 1 ON"
1cmd[1]
2cmd[-1]
3cmd[5] == " "
4len(cmd)
5cmd[len(cmd) - 3]
6cmd[-8]
תשובות: 1 E 2 N 3 True 4 8 5 6 L
שאלה 5.2: מה מדפיסה התוכנית הבאה?
text = "A0=512"
i = len(text) - 1
while i >= 3:
    print(text[i], end="")
    i -= 1
print()
print(text[-6])

5.3 חיתוך מחרוזת

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

s[start : stop : step]
01234567cmd[0:3]LED1ON"LED"cmd[4:5]LED1ON"1"cmd[6:]LED1ON"ON"cmd[::2]LED1ON"LD1O"
ארבעה קטעים מאותה מחרוזת: התווים הצבועים נכנסים לקטע, והריבוע המקווקו הוא האינדקס stop, שאינו נכנס
cmd = "LED 1 ON"
print(cmd[0:3])
print(cmd[4:5])
print(cmd[:3])
print(cmd[6:])
print(cmd[-2:])
print(cmd[:-3])
פלט
LED
1
LED
ON
ON
LED 1

מספר התווים בקטע הוא stop - start, וזה מקל על הבדיקה: ב-cmd[0:3] יש שלושה תווים. גם אינדקסים שליליים עובדים כאן, והשורה האחרונה פירושה: כל המחרוזת חוץ משלושת התווים האחרונים. צעד שלילי פירושו התקדמות לאחור, וזה מועיל כשהמסגרת מגיעה כשהסיבית הפחות משמעותית בתחילתה:

bits = "10110010"
print(bits[:4])
print(bits[::2])
print(bits[::-1])
פלט
1011
1101
01001101
טעות נפוצה: שוכחים ש-stop אינו נכנס, וכותבים cmd[0:2] כשרוצים את שלושת התווים הראשונים. בניגוד לאינדקס, חיתוך אינו עוצר את התוכנית כשהגבולות מחוץ למחרוזת: הוא לוקח רק את מה שקיים, ואם אין דבר הוא נותן מחרוזת ריקה. למשל "R12"[1:100] נותן 12, ושום הודעה אינה חושפת את הטעות.

החיתוך משלים את מה שהתחלנו בסעיף 5.2: בכתובת addr = "R12" הביטוי addr[0] הוא סוג הרכיב, ו-addr[1:] הוא מספרו, בין שיש בו ספרה אחת ובין שיש יותר. וכדי לשנות כתובת, כשהמחרוזת אינה ניתנת לשינוי, בונים מחרוזת חדשה ומציבים אותה באותו משתנה: addr = "C" + addr[1:].

דוגמה 5.3
על נגדים קטנים שמולחמים על פני הלוח (SMD) כתוב קוד של ספרות. הספרה האחרונה היא החזקה של עשר, ומה שלפניה הן הספרות המשמעותיות. כך הקוד 472 פירושו:
R = 47 · 102 Ω
נפענח את הקוד בחיתוך, בלי שיהיה חשוב כמה ספרות יש בו:
פתרון:
mark = input("SMD code: ")
digits = mark[:-1]
power = mark[-1]
ohms = int(digits) * 10 ** int(power)
print("Resistance:", ohms, "ohm")
פלט
SMD code: 472
Resistance: 4700 ohm

ובאותה תוכנית בלי שום שינוי, הקוד בן ארבע הספרות 1002 נותן: Resistance: 10000 ohm

תרגול מהיר — חיתוך, עם tag = "TMP=31.5"
1tag[:3]
2tag[4:]
3tag[4:6]
4tag[-3:]
5tag[::3]
6tag[::-1]
תשובות: 1 TMP 2 31.5 3 31 4 1.5 5 T=. 6 5.13=PMT
שאלה 5.3: מה מדפיסה התוכנית הבאה?
sn = "AB-1234-77"
print(sn[3:7])
print(sn[-2:])
print(sn[::-1][:2])
print(int(sn[3:5]) + 1)

5.4 המרה למחרוזת

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

print(str(12.50))
print(str(True))
print(type(str(1023)))
print(len(str(1023)))
print(str(1023)[0])
פלט
12.5
True
<class 'str'>
4
1

הסימן + אינו משרשר מחרוזת למספר ואינו ממיר מעצמו, כפי שראינו בסעיף 2.12. לכן ממירים בעצמנו.

דוגמה 5.4
התחנה שאנחנו בונים תשלח את הקריאות שלה בשורה אחת: שם, סימן שוויון, ערך, ונקודה-פסיק בין כל שתי קריאות. נבנה את השורה הזאת משני ערכים מספריים.
פתרון:
battery = 12.42
temp = 31.5
line = "BAT=" + str(battery) + ";TMP=" + str(temp)
print(line)
print(len(line))
פלט
BAT=12.42;TMP=31.5
18

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

הפעולות bin ו-hex מפרק 2 נותנות מחרוזת, ולכן אפשר לחתוך מהן את התחילית. הביטוי bin(13)[2:] נותן 1101. ההמרה ההפוכה, ממחרוזת למספר, היא int ו-float שאנחנו מכירים.

טעות נפוצה: השם str נראה טבעי למשתנה, אבל הוא מסתיר את הפעולה המוכנה שנקראת כך, ואחריו אי אפשר להמיר דבר. לכן לא קוראים כך למשתנה.
שאלה 5.4: מה מדפיסה התוכנית הבאה?
n = 407
s = str(n)
print(s + s)
print(n + n)
print(s[1] == 0)
print(s[1] == "0")

5.5 אופרטורים על מחרוזות

פקודהמה היא עושה
s + tשרשור: מחרוזת חדשה ובה התווים של s ואחריהם התווים של t, בלי רווח ביניהם
s * nשכפול: מחרוזת חדשה ובה s חוזרת n פעמים, ו-n מספר שלם

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

name = "BAT"
value = "12.42"
print(name + "=" + value)
print("-" * 20)
print(3 * "01")
print("0" + "1" * 3)
print(len("AB" * 0))
פלט
BAT=12.42
--------------------
010101
0111
0
טעות נפוצה: שכפול מקבל מספר שלם בלבד, ולכן "01" * 2.5 עוצר את התוכנית. ואין חיסור בין מחרוזות: כדי למחוק חלק ממחרוזת משתמשים בחיתוך או ב-replace מסעיף 5.7.
דוגמה 5.5
על מסך קטן נציג את טעינת הסוללה כפס של עשר משבצות, ולא כמספר. כל 10 אחוזים שלמים ממלאים משבצת אחת בסימן #, והשאר נשאר נקודה.
פתרון:
percent = int(input("Battery percent: "))
full = percent // 10
bar = "#" * full + "." * (10 - full)
print("[" + bar + "]", percent)
פלט
Battery percent: 67
[######....] 67

כשהטעינה 100 אחוזים מספר הנקודות הוא אפס, ו-"." * 0 נותן מחרוזת ריקה, ולכן לא צריך תנאי. נבדוק: [##########] 100

בניית מחרוזת בלולאה:

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

bits = "10110001"
result = ""
i = 0
while i < len(bits):
    if bits[i] == "1":
        result = result + "0"
    else:
        result = result + "1"
    i += 1
print(bits)
print(result)
פלט
10110001
01001110

הצד שממנו משרשרים חשוב: בתוכנית כתבנו את התו החדש אחרי result, כמו ב-result + "0", ולכן הסדר נשמר. אילו כתבנו אותו לפניו, כמו ב-"0" + result, הסדר היה מתהפך. כך השורה result = bits[i] + result נותנת בלולאה את מה שנותן bits[::-1].

תרגול מהיר — שרשור ושכפול
1"ab" + "c"
2"ab" * 2
3"1" + "2"
4"=" * 3 + ">"
5len("ab" * 4)
6str(1 + 2) + "3"
תשובות: 1 abc 2 abab 3 12 4 ===> 5 8 6 33
שאלה 5.5: מה מדפיסה התוכנית הבאה?
s = ""
i = 1
while i <= 3:
    s = str(i) + "-" * i + s
    i += 1
print(s)
print(len(s))

5.6 אופרטור היחס in

האופרטור in שואל: האם המחרוזת הזאת נמצאת בתוך ההיא, כקטע רצוף? התשובה היא True או False, והאופרטור not in נותן את ההפך. החיפוש מבחין בין אות גדולה לקטנה, ומחרוזת ריקה נמצאת בכל מחרוזת:

line = "BAT=12.42;TMP=31.5"
print("=" in line)
print("BAT" in line)
print("bat" in line)
print("HUM" not in line)
print("" in line)
פלט
True
True
False
True
True
טעות נפוצה: in מחפש בכל המחרוזת, ולא רק בתחילתה. הכתובת LDR1 של חיישן אור מכילה את התו R, אבל הרכיב אינו נגד מסוג R. את סוג הרכיב בודקים לפי התו הראשון. וגם: שני הצדדים של in הם מחרוזות, ולכן 1 in "101" עוצר את התוכנית, כי הצד השמאלי הוא מספר.
addr = "LDR1"
print("R" in addr)
print(addr[0] == "R")
פלט
True
False

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

דוגמה 5.6
נבדוק שהמסגרת שהגיעה מורכבת רק מהסיביות 0 ו-1. נעבור על התווים, נעצור בתו השגוי הראשון, ונגיד איפה הוא.
פתרון:
frame = input("Frame: ")
i = 0
good = True
while i < len(frame) and good:
    if frame[i] not in "01":
        good = False
    else:
        i += 1
if good:
    print("Valid frame")
else:
    print("Bad char", frame[i], "at", i)
פלט
Frame: 10120010
Bad char 2 at 3

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

שאלה 5.6: מה מדפיסה התוכנית הבאה?
a = "SW1=ON"
print("ON" in a)
print("on" in a)
print(a[4:] in "ON OFF")
print("1" in a[:3] and "=" in a)

5.7 פעולות על מחרוזות

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

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

חיפוש:

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

T0-8M1-7P2-6=3-534-415-3.6-257-1
הסימן = באינדקס 3: מה שלפניו הוא השם, ומה שאחריו הוא הערך
item = "TMP=31.5"
print(item.find("="))
print(item.find("#"))
pos = item.find("=")
print(item[:pos])
print(item[pos + 1:])
פלט
3
-1
TMP
31.5
טעות נפוצה: כשמה שמחפשים אינו קיים, find מחזירה -1, ואינה עוצרת ואינה נותנת שגיאה. אבל -1 הוא אינדקס תקין בחיתוך: פירושו התו האחרון. לכן התוכנית הבאה רצה בלי תלונה, ונותנת שם קטוע וערך שהוא המחרוזת כולה:
item = "TMP31.5"
pos = item.find("=")
print(pos)
print(item[:pos])
print(item[pos + 1:])
פלט
-1
TMP31.
TMP31.5

לכן בודקים את התוצאה לפני שמשתמשים בה, בהשוואה pos != -1, ולא בכתיבת if s.find("="): בלבד: הערך -1 נחשב אמת, והערך 0, שהוא תוצאה נכונה שפירושה שהמחרוזת התחילה במה שחיפשנו, נחשב שקר.

אותיות גדולות וקטנות:

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

cmd = "Led 1 on"
print(cmd.upper())
cmd.lower()
print(cmd)
cmd = cmd.lower()
print(cmd)
פלט
LED 1 ON
Led 1 on
led 1 on

פקודות שהמשתמש מקליד עשויות להגיע באותיות גדולות או קטנות, ולכן מאחידים אותן בקריאה: cmd = input("Command: ").upper(). אפשר לכתוב את הפעולה מיד אחרי input, כי מה שחוזר ממנה הוא מחרוזת.

ספירה:

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

bits = "10110010"
print(bits.count("1"))
print(bits.count("10"))
print(bits.count("2"))
print("0000".count("00"))
print(bits.count("1") % 2 == 0)
פלט
4
3
0
2
True

התחלה וסוף:

הפעולות startswith ו-endswith עונות True או False. הן ברורות יותר מחיתוך והשוואה, כי לא צריך לספור תווים:

addr = "R12"
print(addr.startswith("R"))
print(addr.startswith("r"))
print(addr.endswith("2"))
print("log_05.csv".endswith(".csv"))
פלט
True
False
True
True

בדיקת סוג התווים:

הפעולה isalpha עונה האם כל תווי המחרוזת הם אותיות, ו-isnumeric האם כולם ספרות. מחרוזת ריקה נותנת False בשתיהן.

print("LED".isalpha(), "LED1".isalpha())
print("LED 1".isalpha(), "".isalpha())
print("1023".isnumeric(), "12.42".isnumeric())
print("-5".isnumeric(), "".isnumeric())
פלט
True False
False False
True False
False False
טעות נפוצה: isnumeric שואלת על התווים, ולא על המשמעות. במספר 12.42 יש נקודה, ובמספר -5 יש מינוס, והם אינם ספרות, ולכן התשובה היא False אף ש-float מקבלת את שתי המחרוזות. את עניין הנקודה נפתור עוד מעט, בעזרת replace.
דוגמה 5.7
נבדוק מספר סידורי מן הצורה EL-2026-0417 לפני שנשמור אותו: שתי אותיות של החברה, ארבע ספרות של השנה, וארבע של מספר היחידה. נשתמש ב-elif כדי לעצור בשגיאה הראשונה.
פתרון:
sn = input("Serial number: ")
if len(sn) != 12:
    print("Wrong length")
elif not sn.startswith("EL-") or sn[7] != "-":
    print("Wrong format")
elif not (sn[3:7] + sn[8:]).isnumeric():
    print("Bad number")
else:
    print("Valid")
פלט
Serial number: EL-2026-0417
Valid
קלטפלט
EL-2026-417Wrong length
XY-2026-0417Wrong format
EL-20A6-0417Bad number

בודקים קודם את האורך, כדי שהתו sn[7] יהיה קיים בוודאות כשמגיעים אליו.

החלפה:

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

raw = "12,42 V"
print(raw.replace(" V", ""))
print(raw.replace("x", "y"))
print(raw)
clean = raw.replace(",", ".").replace(" V", "")
print(float(clean) * 2)
פלט
12,42
12,42 V
12,42 V
24.84

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

text = input("Value: ")
digits = text.replace(".", "")
if text.count(".") <= 1 and digits.isnumeric():
    print("Number:", float(text))
else:
    print("Not a number")
פלט
Value: 12.42
Number: 12.42
קלטפלט
1.2.3Not a number
hotNot a number
-5Not a number
הערה: השורה האחרונה בטבלה מראה שהבדיקה הזאת אינה מקבלת מספרים שליליים. נוסיף אותם באחד מתרגילי הפרק.

פיצול:

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

cmd = "LED 1 ON"
parts = cmd.split()
print(parts)
print(len(parts))
print(parts[0], parts[2])
print(parts[1] == 1)
print(int(parts[1]) + 1)
פלט
['LED', '1', 'ON']
3
LED ON
False
2

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

טעות נפוצה: בלי פרמטר, רווחים רצופים נחשבים למפריד אחד. עם פרמטר, כל מפריד מפצל, גם אם אין דבר בין שני מפרידים, ולכן מופיעות מחרוזות ריקות בין האיברים. כך מפריד מיותר בסוף השורה נותן איבר ריק נוסף:
line = "BAT=12.42;TMP=31.5"
print(line.split(";"))
print(line.split("#"))
print("A  B".split())
print("A  B".split(" "))
print("A;B;".split(";"))
פלט
['BAT=12.42', 'TMP=31.5']
['BAT=12.42;TMP=31.5']
['A', 'B']
['A', '', 'B']
['A', 'B', '']
דוגמה 5.8
דרך היציאה הגיעה פקודה מן הצורה LED 1 ON. נפצל אותה למילים, ונבדוק כל חלק לפני שנבצע את הפקודה. אינדקס מחוץ לרשימה עוצר את התוכנית, ולכן בודקים קודם את len(parts), לפני שקוראים איבר כלשהו.
פתרון:
cmd = input("Command: ").upper()
parts = cmd.split()
if len(parts) != 3:
    print("ERR: need 3 words")
elif parts[0] != "LED":
    print("ERR: unknown device")
elif not parts[1].isnumeric():
    print("ERR: bad number")
elif parts[2] != "ON" and parts[2] != "OFF":
    print("ERR: bad state")
else:
    print("LED", int(parts[1]), "goes", parts[2])
פלט
Command: led 1 on
LED 1 goes ON
קלטפלט
LED 1ERR: need 3 words
LED one ONERR: bad number
LED 1 DIMERR: bad state

אורך:

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

msg = "ALARM: TEMP HIGH"
print(len(msg))
print(len(msg.split()))
print(msg.len())
פלט
16
3
AttributeError: 'str' object has no attribute 'len'
דוגמה 5.9
בשורה של מסך LCD נפוץ יש מקום ל-16 תווים. הודעה ארוכה נחתוך בגבול הזה, והודעה קצרה נשלים בנקודות כדי לראות איפה השורה נגמרת.
פתרון:
msg = input("Message: ")
width = 16
if len(msg) > width:
    print(msg[:width])
else:
    print(msg + "." * (width - len(msg)))
פלט
Message: ALARM: TEMP TOO HIGH
ALARM: TEMP TOO 

ובהודעה הקצרה BAT OK נקבל: BAT OK..........

שאלה 5.7: מה מדפיסה התוכנית הבאה?
s = "SW1=OFF;SW2=ON"
print(s.find("="))
print(s.find("X"))
print(s.count("SW"))
print(s.split(";")[1])
print(s.lower().startswith("sw"))
print(s.replace("OFF", "ON").count("ON"))
▶ניסוי חי — מעבדת המחרוזותכותבים במעבדה מחרוזת, רואים לפנינו את האינדקסים ואת החיתוך, ומריצים תוכניות צעד אחר צעד. עובדות בה find, split, replace, isnumeric ושאר הפעולות של הסעיף הזה. אין בה קלט מן המשתמש, ולכן מציבים את הערך ישירות במשתנה במקום input.
⚠
המעבדה אינה זמינה כרגע. החומר בפרק שלם גם בלעדיה: ממשיכים לקרוא, וחוזרים לניסוי מאוחר יותר או פותחים אותו ישירות מן הקישור שמתחת למסגרת.
https://fahed-lab.com/electronics/python_basics#strings
פותחים את המעבדה בדף נפרד ↗ fahed-lab.com/electronics/python_basics#strings
פרויקט הספר: תחנת המדידה · שלב 5 — התחנה מפענחת שורה

בפרק 4 התחנה קראה מספרים. מכשירים אמיתיים שולחים טקסט, והוא עשוי להגיע קטוע או עם רעש. נכתוב את התוכנית שמקבלת שורה כמו BAT=12.42;TMP=31.5, מפרידה את הקריאות, בודקת כל קריאה, ומקבלת את השורה רק אם היא תקינה כולה. זו בדיוק השורה שהארדואינו ישלח בפרק 10.

"BAT=12.42;TMP=31.5""BAT=12.42"parts[0]"BAT"item[:pos]"12.42"item[pos + 1:]float(text)12.42"TMP=31.5"parts[1]"TMP"item[:pos]"31.5"item[pos + 1:]float(text)31.5line.split(";")
פירוק השורה: split מפרידה את הקריאות, find קובעת איפה לחתוך בכל קריאה, ו-float ממירה את הערך
line = input("Line: ")
parts = line.split(";")
battery = -1.0
temp = -1.0
errors = 0
i = 0
while i < len(parts):
    item = parts[i]
    pos = item.find("=")
    if pos == -1:
        print("No '=' in:", item)
        errors += 1
    else:
        name = item[:pos]
        text = item[pos + 1:]
        digits = text.replace(".", "")
        if text.count(".") > 1 or not digits.isnumeric():
            print("Bad value in:", item)
            errors += 1
        elif name == "BAT":
            battery = float(text)
        elif name == "TMP":
            temp = float(text)
    i += 1
if errors == 0 and battery >= 0 and temp >= 0:
    print("Battery:", battery, "V")
    print("Temp:", temp, "C")
else:
    print("Line rejected")
פלט
Line: BAT=12.42;TMP=31.5
Battery: 12.42 V
Temp: 31.5 C

בודקים כל קריאה בשני צעדים: האם יש בה =? והאם הערך הוא מספר? הערך -1.0 בהתחלה פירושו שעוד לא קראנו; אם הוא נשאר כך אחרי הלולאה, חסרה קריאה. אלה תוצאות התוכנית על שורות פגומות:

שורהפלט
BAT=12.42;TMP=hotBad value in: TMP=hot / Line rejected
BAT12.42;TMP=31.5No '=' in: BAT12.42 / Line rejected
BAT=12.42Line rejected
BAT=12.42;TMP=31.5;No '=' in: / Line rejected

נסו בעצמכם: התוכנית מתעלמת מקריאה ששמה אינו מוכר, כמו HUM=40. מה צריך להוסיף כדי שתדחה את השורה במקרה כזה?

הרעיונות המרכזיים של הפרק:

סיכום הפקודות בפרק:

פקודהמה היא עושה
"" len(s)המחרוזת הריקה, ומספר התווים: פעולה מוכנה ולא פעולה של העצם
s[i] s[-1]תו לפי אינדקס, והתו האחרון
s[a:b] s[a:] s[:b]קטע מן המחרוזת, בלי התו שבאינדקס b
s[a:b:c] s[::-1]קטע עם צעד, והמחרוזת הפוכה
str(x)המרה למחרוזת
s + t s * nשרשור ושכפול
x in s x not in sהאם x הוא קטע מ-s
s.find(x)האינדקס של ההופעה הראשונה, או −1 אם לא נמצא
s.upper() s.lower()מחרוזת חדשה באותיות גדולות, או באותיות קטנות
s.count(x)מספר ההופעות
s.startswith(x) s.endswith(x)בדיקת ההתחלה והסוף
s.isalpha() s.isnumeric()בדיקת סוג התווים
s.replace(a, b)החלפה ומחיקה
s.split() s.split(x)רשימת הקטעים שבין הרווחים, או בין המפרידים x

מילון מונחים:

עבריתEnglishערבית
מחרוזתStringالجملة النصية
מחרוזת ריקהEmpty stringالجملة النصية الفارغة
תוCharacterالحرف
אינדקסIndexالمؤشّر
חיתוךSlicingالقصّ
צעדStepالخطوة
שרשורConcatenationالوصل
שכפולRepetitionالتكرار
בלתי ניתנת לשינויImmutableغير قابلة للتغيير
אופרטור יחסRelational operatorمعامل العلاقة
מפרידSeparatorالفاصل
רשימהListقائمة
איברElementعنصر
התשובות הסופיות לשאלות הפרק
5.1 4 , False , False , True
5.2 215 , A
5.3 1234 , 77 , 77 , 13
5.4 407407 , 814 , False , True
5.5 3---2--1- , 9
5.6 True , False , True , True
5.7 3 , -1 , 2 , SW2=ON , True , 2

שאלות לתרגול:

רמה ראשונה — בסיסי
1.
מה מדפיסה התוכנית?
tag = "SW-07"
print(tag[0])
print(tag[-1])
print(tag[3:])
print(len(tag))
print(tag.find("-"))
print(tag[::-1])
תשובות: S , 7 , 07 , 5 , 2 , 70-WS
2.
מה ערכו של כל ביטוי?
(א) "A5".isalpha()
(ב) "105".isnumeric()
(ג) "1.5".isnumeric()
(ד) "LED".lower()
(ה) "ON OFF".find("F")
(ו) "ON OFF".count("O")
תשובות: False , True , False , led , 4 , 2
3.
כתבו תוכנית שקוראת שם של הדק, כמו A5 או D13, ומדפיסה בנפרד את האות ואת המספר.
תשובות:
port = input("Port: ")
print("Letter:", port[0])
print("Number:", int(port[1:]))
עבור הקלט D13 היא מדפיסה: Letter: D / Number: 13
4.
בתוכנית הבאה שתי טעויות. רוצים להדפיס את הפקודה באותיות גדולות, ושהתו הראשון שלה יהיה X. מצאו את הטעויות ותקנו אותן.
cmd = "led on"
cmd.upper()
cmd[0] = "X"
print(cmd)
תשובות: השורה השנייה אינה שומרת את התוצאה, והשורה השלישית מנסה לשנות מחרוזת שאינה משתנה:
cmd = "led on"
cmd = cmd.upper()
cmd = "X" + cmd[1:]
print(cmd)
היא מדפיסה: XED ON
רמה שנייה — בינוני
5.
קוד הקבל 104 פירושו 10 כפול 104 ביחידות פיקופאראד. כתבו תוכנית שקוראת קוד בן שלוש ספרות, ומדפיסה את הקיבול בפיקופאראד ובננופאראד.
תשובות:
mark = input("Capacitor code: ")
pf = int(mark[:2]) * 10 ** int(mark[2])
print(pf, "pF")
print(pf / 1000, "nF")
עבור הקלט 104 היא מדפיסה: 100000 pF / 100.0 nF
6.
הגיעה הודעה עם רווחים רצופים בין המילים, ואין רווח בתחילתה או בסופה. כתבו תוכנית שמשאירה רווח אחד, בעזרת in ו-replace, ואחר כך מדפיסה את מספר המילים.
תשובות:
msg = input("Message: ")
while "  " in msg:
    msg = msg.replace("  ", " ")
print(msg)
print(len(msg.split(" ")))
עבור הקלט TEMP OK NOW היא מדפיסה: TEMP OK NOW / 3. הלולאה הכרחית: החלפה אחת אינה מספיקה כשיש שלושה רווחים רצופים או יותר.
7.
כתבו תוכנית שקוראת מסגרת. אם היא אינה מורכבת רק מהסיביות 0 ו-1, היא מדפיסה הודעת שגיאה. אחרת היא מדפיסה את המסגרת ואחריה סיבית שעושה את מספר הסיביות 1 לזוגי. עשו זאת בלי לולאה.
תשובות:
frame = input("Frame: ")
ones = frame.count("1")
zeros = frame.count("0")
if ones + zeros != len(frame) or frame == "":
    print("Bad frame")
else:
    print(frame + str(ones % 2))
עבור הקלט 1011 היא מדפיסה: 10111 , ועבור הקלט 10a1 היא מדפיסה: Bad frame
8.
רשימת רכיבים כתובה בשורה, כמו R1,R12,C3,R7,L2. כתבו תוכנית שמדפיסה את מספר כל הרכיבים, ואת מספר הנגדים ביניהם.
תשובות:
board = input("Parts: ")
parts = board.split(",")
count = 0
i = 0
while i < len(parts):
    if parts[i].startswith("R"):
        count += 1
    i += 1
print(len(parts), count)
עבור הקלט הזה היא מדפיסה: 5 3
רמה שלישית — אתגר
9.
כתבו תוכנית שממירה מחרוזת הקסדצימלית למספר בלי int(text, 16): שומרים את הספרות לפי הסדר במחרוזת, ו-find נותנת את הערך של כל ספרה. אחר כך בדקו את התוצאה בעזרת int(text, 16).
תשובות:
text = input("Hex: ").upper()
table = "0123456789ABCDEF"
value = 0
i = 0
while i < len(text):
    value = value * 16 + table.find(text[i])
    i += 1
print(value)
print(int(text, 16))
עבור הקלט 1f4 היא מדפיסה: 500 / 500
10.
כדי לגלות הודעה חסרה, השולח מוסיף בסופה את הסימן * ואחריו את אורך ההודעה, כמו HELLO*5. כתבו תוכנית שבודקת זאת.
תשובות:
frame = input("Frame: ")
pos = frame.find("*")
if pos == -1:
    print("No length field")
else:
    body = frame[:pos]
    size = frame[pos + 1:]
    if size.isnumeric() and int(size) == len(body):
        print("Good:", body)
    else:
        print("Length mismatch")
עבור הקלטים HELLO*5, HELL*5 ו-HELLO היא מדפיסה: Good: HELLO / Length mismatch / No length field
11.
שורה ובה מספר לא ידוע של קריאות, כמו A0=2.5;A1=4.75;A2=1.2, וכולן תקינות. כתבו תוכנית שמדפיסה את שם הקריאה הגבוהה ביותר ואת ערכה.
תשובות:
line = input("Line: ")
parts = line.split(";")
top = 0.0
top_name = ""
i = 0
while i < len(parts):
    pos = parts[i].find("=")
    value = float(parts[i][pos + 1:])
    if i == 0 or value > top:
        top = value
        top_name = parts[i][:pos]
    i += 1
print(top_name, top)
עבור הקלט הזה היא מדפיסה: A1 4.75
12.
טמפרטורה עשויה להיות שלילית, ובדיקת הנקודה מסעיף 5.7 אינה מקבלת זאת. הרחיבו אותה כך שתקבל סימן מינוס אחד, בהתחלה בלבד.
תשובות:
text = input("Value: ")
body = text
if text.startswith("-"):
    body = text[1:]
digits = body.replace(".", "")
if body.count(".") <= 1 and digits.isnumeric():
    print("Number:", float(text))
else:
    print("Not a number")
עבור הקלטים -3.5, 12 ו-3-5 היא מדפיסה: Number: -3.5 / Number: 12.0 / Not a number