البرمجة بلغة بايثون · الفصل 5 — العمليّات على الجمل النصيةפעולות על מחרוזות · יסודות התכנות בשפת Python⌂📚🏠
الفصل 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

المعامل 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