البرمجة بلغة بايثون · الفصل 11 — مواضيع اختياريةנושאי רשות · יסודות התכנות בשפת Python⌂📚🏠
الفصل 11

مواضيع اختيارية

נושאי רשות

هذا الفصل اختياري: مواضيعه موصى بها في المنهاج، لكنها ليست إلزامية، ولا تُحسب ساعاتها في مجموع ساعات المنهاج، ولا تأتي منها أسئلة في امتحان البجروت. ومع ذلك فهي الخطوة الطبيعية بعد الفصول السابقة: كل برنامج حقيقي يحتاج إلى أن يبحث بسرعة، وأن يحفظ ما عرفه في ملف، وأن يبقى واقفا عندما يصله نص فاسد. نأخذ هنا أربعة مواضيع، ونجمعها في النهاية في مرحلة جديدة من محطة القياس.

في هذا الفصل:
  • نستعمل المجموعة set عندما يهمنا هل العنصر موجود، لا مكانه.
  • نستعمل القاموس dict لنصل إلى قيمة بواسطة مفتاح.
  • نبني فئة على فئة بالوراثة، ونكتب super و__add__ والفئات المجرّدة.
  • نقرأ من ملفات نصية ونكتب إليها، ونتحرك داخلها بواسطة seek وtell.
  • نميّز بين الخطأ النحوي والاستثناء، ونعالج الاستثناء بواسطة try وexcept وfinally.
  • نكتب برنامجا يقرأ سجل المحطة، يتجاوز السطور الفاسدة، ويحفظ ملخّصا في ملف.
الموضوعالبنودالساعات المقترحة في المنهاج
بنى معطيات إضافية (מבני נתונים נוספים)11.1 و11.210
وراثة الفئات (ירושה של מחלקות)11.315
العمل مع الملفات (עבודה עם קבצים)11.410
معالجة الاستثناءات (טיפול בחריגים)11.510
ملاحظة: لا يعمل مختبر الموقع مع الملفات، ولا مع try وimport. لذلك نشغّل برامج هذا الفصل في بايثون على الحاسوب.

11.1 بنية المعطيات: المجموعة – מבנה נתונים מטיפוס Set (set)

في القائمة والصفّ الثابت من الفصل 6 ترتيب ومؤشّرات. لكننا أحيانا لا نحتاج إلى الترتيب ولا إلى التكرار. على لوحة أردوينو مثلا نريد أن نعرف: هل الطرف 7 مشغول؟ لا يهمنا متى حجزناه، ولا معنى لأن يُحجز مرتين. هذا هو عمل المجموعة.

التهيئة والمجموعة الفارغة والطباعة – אתחול, מבנה ריק, set, הדפסה:

المجموعة (Set): بنية معطيات تحفظ عناصر من دون تكرار ومن دون ترتيب. نكتبها بين قوسين معقوفين { }، وهي كائن من الفئة set.
pins = {13, 2, 7, 2, 13}
print(pins)
print(len(pins))
empty = set()
print(empty, type(empty))
other = {}
print(type(other))
المُخرَج
{2, 13, 7}
3
set() <class 'set'>
<class 'dict'>

كتبنا خمسة عناصر، فبقي منها ثلاثة: المكرر يُحفظ مرة واحدة. ولم تأت الطباعة بالترتيب الذي كتبناه، ولا بترتيب تصاعدي: بايثون تختار مكان كل عنصر بطريقتها لتجده بسرعة، ولا تعدنا بأي ترتيب.

خطأ شائع: المجموعة الفارغة تُكتب set(). أما القوسان الفارغان {} فيعطيان قاموسا فارغا، كما يظهر في السطر الأخير من المُخرَج.

الدالة set تحوّل إلى مجموعة كل ما نستطيع المرور عليه: قائمة، صفّا ثابتا، range، وحتى جملة نصية. وهذه أقصر طريقة لنعرف كم قيمة مختلفة في قائمة:

samples = [5, 3, 5, 5, 8, 3]
print(set(samples))
print(len(samples), len(set(samples)))
print(set(range(1, 4)))
print(sorted(set("10110010")))
المُخرَج
{8, 3, 5}
6 3
{1, 2, 3}
['0', '1']

في السطر الأخير استعملنا الدالة الجاهزة sorted، وهي تعطي قائمة جديدة مرتبة. نفعل ذلك مع الجمل النصية لسبب إضافي: ترتيبها داخل المجموعة يتغيّر من تشغيل إلى تشغيل للبرنامج نفسه، ولذلك لا نطبع في هذا الكتاب مجموعة جمل نصية إلا بعد sorted.

خطأ شائع: لا ترتيب، إذن لا مؤشّر: pins[0] يوقف البرنامج. السؤال الوحيد عن عنصر هو: هل هو موجود؟ وعناصر المجموعة يجب ألا تكون قابلة للتغيير: عدد وجملة نصية وصفّ ثابت مسموح، أما القائمة فلا.

إضافة عنصر – הוספת איברים על ידי add:

العمليّة add تضيف عنصرا واحدا وتغيّر المجموعة نفسها. وإذا كان العنصر موجودا فلا يحدث شيء، ولا تظهر رسالة خطأ.

مثال 11.1
وصلتنا عبر الاتصال التسلسلي إطارات لها أسماء، وبعضها أُعيد إرساله. نطبع كل تكرار، ثم عدد الإطارات المختلفة.
الحل:
frames = ["A7", "B2", "A7", "C4", "B2", "A7"]
seen = set()
for frame in frames:
    if frame in seen:
        print("Repeated:", frame)
    else:
        seen.add(frame)
print(len(seen), "different frames")
المُخرَج
Repeated: A7
Repeated: B2
Repeated: A7
3 different frames

مجموعة باسم seen هي نمط نستعمله كثيرا: «ما الذي رأيناه حتى الآن؟». والسؤال in سريع فيها حتى لو كان فيها مليون عنصر، لأن بايثون تذهب مباشرة إلى مكان العنصر، أما في القائمة فتمر على العناصر واحدا واحدا.

الحلقة for على عناصر المجموعة – לולאת for העוברת על האיברים:

تمر الحلقة for على كل عنصر مرة واحدة، بالترتيب الداخلي للمجموعة. وإذا كان الترتيب يهمنا مررنا على sorted(pins):

pins = {13, 2, 7}
for pin in pins:
    print("Pin", pin, "is an output")
for pin in sorted(pins):
    print(pin, end=" ")
print()
المُخرَج
Pin 2 is an output
Pin 13 is an output
Pin 7 is an output
2 7 13 

عمليّات الفئة set – פעולות המחלקה:

الأمرعمله
x in sهل العنصر x موجود في المجموعة: True أو False
s.add(x)يضيف عنصرا واحدا
s.update(t)يضيف كل عناصر t إلى s نفسها، ولا يعيد شيئا
s.union(t)يعيد مجموعة جديدة فيها عناصر الاثنتين، وs لا تتغيّر
s.discard(x)يحذف x إذا كان موجودا، ولا خطأ إذا لم يكن
s.pop()يحذف عنصرا ما ويعيده، ولا نعرف مسبقا أي عنصر
s.clear()يفرغ المجموعة

العمليّة union تعطي مجموعة جديدة فيها كل ما في المجموعتين، والعنصر المشترك فيها مرة واحدة، والمجموعتان الأصليتان تبقيان كما هما:

board_aboard_b2345{2, 3, 4, 5}union
الطرف 4 مستعمل في اللوحتين، لكنه يظهر في الاتحاد مرة واحدة
board_a = {2, 3, 4}
board_b = {4, 5}
both = board_a.union(board_b)
print(both)
print(board_a)
print(board_b)
print(4 in both, 9 in both)
المُخرَج
{2, 3, 4, 5}
{2, 3, 4}
{4, 5}
True False
خطأ شائع: أما update فتغيّر المجموعة نفسها ولا تعيد شيئا. إذا وضعنا نتيجتها في متغيّر حصلنا على None، وضاعت المجموعة:
busy = {2, 3}
busy.update([4, 5, 5])
print(busy)
busy = busy.update({9})
print(busy)
busy.add(5)
المُخرَج
{2, 3, 4, 5}
None
AttributeError: 'NoneType' object has no attribute 'add'

العمليّة discard تحذف عنصرا ولا تشكو إذا لم تجده، بخلاف remove، التي تعمل مثل remove في القوائم وتتوقف بخطأ. والعمليّة pop تسحب عنصرا لا نحدده: بقي هنا 2 و9، فأخذت 9. وهي تصلح عندما يكون أي عنصر مناسبا، مثل أي طرف حر. وعلى مجموعة فارغة تتوقف بخطأ:

busy = {2, 3, 9}
busy.discard(3)
busy.discard(100)
print(busy)
print("Took", busy.pop())
print(busy)
busy.clear()
print(busy, len(busy))
busy.pop()
المُخرَج
{9, 2}
Took 9
{2}
set() 0
KeyError: 'pop from an empty set'
تدريب سريع — المجموعة
1len({1, 1, 2})
23 in {1, 2}
3len(set("ABBA"))
4sorted({3, 1, 2})
5len(set())
6type({}).__name__
الإجابات: 1 2 2 False 3 2 4 [1, 2, 3] 5 0 6 dict
سؤال 11.1: ماذا يطبع البرنامج التالي؟
a = {1, 2}
b = a.union({2, 3})
a.add(5)
a.discard(1)
a.discard(7)
print(len(a), len(b))
print(5 in b, 3 in b)
print(sorted(a), sorted(b))

11.2 بنية المعطيات: القاموس – מבנה נתונים מטיפוס Dictionary (dict)

في القائمة نصل إلى العنصر بعدد: values[2]. لكن ماذا يعني المكان 2 في قائمة قراءات؟ علينا أن نتذكر أن الحرارة هي الثالثة. أما القاموس فنصل فيه إلى القيمة باسمها: station["TMP"].

التهيئة والقاموس الفارغ والطباعة – אתחול, מבנה ריק, הדפסה:

القاموس (מילון): بنية معطيات تحفظ أزواجا، كل زوج فيه مفتاح (מפתח) وقيمة. نصل إلى القيمة بواسطة مفتاحها. وهو كائن من الفئة dict.
keyvalue"BAT"12.42"TMP"31.5"SOL"18.0
القاموس station: كل مفتاح يؤدي إلى قيمة واحدة

الكتابة d[key] = value تضيف زوجا جديدا إذا لم يكن المفتاح موجودا، وتغيّر قيمته إذا كان موجودا. الشكل واحد، والفعل حسب ما هو موجود:

station = {"BAT": 12.42, "TMP": 31.5}
print(station["TMP"])
station["SOL"] = 18.0
station["TMP"] = 33.0
print(station)
print(len(station))
empty = {}
print(empty, type(empty))
المُخرَج
31.5
{'BAT': 12.42, 'TMP': 33.0, 'SOL': 18.0}
3
{} <class 'dict'>
خطأ شائع: المفتاح غير الموجود يوقف البرنامج. لذلك نسأل أولا بواسطة in، وهو يبحث في المفاتيح فقط لا في القيم، كما يظهر في السطر الثاني:
station = {"BAT": 12.42, "TMP": 31.5}
print("TMP" in station)
print(12.42 in station)
print(station["HUM"])
المُخرَج
True
False
KeyError: 'HUM'

إضافة أزواج – הוספת איברים:

مثال 11.2
ألوان المقاومة: نقرأ ثلاثة ألوان، الأول والثاني هما الرقمان، والثالث هو قوة العشرة. الجدول الذي نحفظه عادة يصبح قاموسا.
الحل:
brown1black0red× 10²
المقاومة 10 · 102 = 1000 Ω. الشريط الرابع هو السماح، ولا يدخل في الحساب
CODES = {"black": 0, "brown": 1, "red": 2,
         "orange": 3, "yellow": 4, "green": 5,
         "blue": 6, "violet": 7, "grey": 8,
         "white": 9}
b1 = input("Band 1: ")
b2 = input("Band 2: ")
b3 = input("Band 3: ")
digits = CODES[b1] * 10 + CODES[b2]
ohms = digits * 10 ** CODES[b3]
print(ohms, "ohm")
المُخرَج
Band 1: brown
Band 2: black
Band 3: red
1000 ohm

وللمُدخَل yellow violet orange يطبع: 47000 ohm. لو كتبناه بواسطة elif لاحتجنا إلى عشرة فروع لكل شريط. القاموس حوّل كل ذلك إلى بيانات.

الحلقة for على القاموس – לולאת for העוברת על האיברים:

الحلقة for تمر على المفاتيح. وإذا أردنا المفتاح والقيمة معا مررنا على d.items()، فنفكّ كل زوج إلى متغيّرين كما فككنا الصفّ الثابت في الفصل 6. والعمليّتان keys وvalues تعطيان المفاتيح وحدها أو القيم وحدها:

station = {"BAT": 12.42, "TMP": 31.5, "SOL": 18.0}
for name in station:
    print(name, station[name])
for name, value in station.items():
    print(name, "=", value)
print(list(station.keys()))
print(list(station.values()))
المُخرَج
BAT 12.42
TMP 31.5
SOL 18.0
BAT = 12.42
TMP = 31.5
SOL = 18.0
['BAT', 'TMP', 'SOL']
[12.42, 31.5, 18.0]

قاموس عدّادات. عندما نعدّ أشياء لها أسماء، ولا نعرف مسبقا ما هي، يكون الاسم مفتاحا والعدد قيمة. أول مرة نرى اسما نضعه بالقيمة 1، وبعدها نزيده. نتابع ذلك على لوحة فيها ثلاث قطع:

counts = {}
for part in "R1,C1,R2".split(","):
    kind = part[0]
    if kind in counts:
        counts[kind] += 1
    else:
        counts[kind] = 1
print(counts)
متابعة قاموس العدّادات
الأمرpartkindcountskind in countsالمُخرَج
counts = {}{}
for part in "R1,C1,R2".split(","):'R1'
kind = part[0]'R'
if kind in counts:False
counts[kind] = 1{'R': 1}
for part in "R1,C1,R2".split(","):'C1'
kind = part[0]'C'
if kind in counts:False
counts[kind] = 1{'R': 1, 'C': 1}
for part in "R1,C1,R2".split(","):'R2'
kind = part[0]'R'
if kind in counts:True
counts[kind] += 1{'R': 2, 'C': 1}
for part in "R1,C1,R2".split(","):
print(counts){'R': 2, 'C': 1}

ولهذا الفرع المزدوج اختصار: counts.get(kind, 0) تعطي القيمة إذا وُجد المفتاح، وإلا أعطت القيمة التي نحددها، بلا خطأ. فنكتب: counts[kind] = counts.get(kind, 0) + 1.

عمليّات الفئة dict – פעולות המחלקה: pop, popitem, del, clear, copy:

الأمرعمله
d.pop(k)يحذف المفتاح k ويعيد قيمته، وKeyError إذا لم يوجد
d.pop(k, x)كما سبق، لكنه يعيد x بدل الخطأ إذا لم يوجد
d.popitem()يحذف آخر زوج أُضيف، ويعيده كصفّ ثابت، وKeyError إذا كان القاموس فارغا
del d[k]أمر لا عمليّة: يحذف الزوج، ولا يعيد شيئا
d.clear()يفرغ القاموس
d.copy()يعيد قاموسا جديدا فيه الأزواج نفسها
cfg = {"rate": 9600, "port": "COM3",
       "bits": 8, "echo": True}
print(cfg.pop("echo"))
print(cfg)
print(cfg.pop("mode", "none"))
print(cfg.popitem())
print(cfg)
del cfg["port"]
print(cfg)
cfg.clear()
print(cfg, len(cfg))
المُخرَج
True
{'rate': 9600, 'port': 'COM3', 'bits': 8}
none
('bits', 8)
{'rate': 9600, 'port': 'COM3'}
{'rate': 9600}
{} 0

رأينا في البند 6.4 أن الإسناد لا ينسخ القائمة، وكذلك القاموس: same = base يعطي الكائن نفسه اسما ثانيا. أما copy فتبني قاموسا جديدا، والتغيير في الأصل لا يصل إليه. وهي تنسخ الأزواج فقط، فإذا كانت القيمة قائمة أشار القاموسان إلى القائمة نفسها.

base = {"rate": 9600, "bits": 8}
same = base
twin = base.copy()
base["rate"] = 115200
print(same["rate"])
print(twin["rate"])
print(same is base, twin is base)
المُخرَج
115200
9600
True False
خطأ شائع: لا نحذف من قاموس في أثناء المرور عليه. والحل أن نمر على قائمة مفاتيح منسوخة: for name in list(loads):.
loads = {"LED": 0, "FAN": 5, "PUMP": 0}
for name in loads:
    if loads[name] == 0:
        del loads[name]
المُخرَج
RuntimeError: dictionary changed size during iteration
القائمةالصفّ الثابتالمجموعةالقاموس
الكتابة[1, 2](1, 2){1, 2}{"a": 1}
الفارغة[]()set(){}
الوصولبالمؤشّربالمؤشّرin فقطبالمفتاح
التكرارمسموحمسموحلاالمفتاح لا
التغييرنعملانعمنعم
تدريب سريع — القاموس، مع d = {"a": 1, "b": 2}
1d["b"]
2len(d)
3"a" in d
41 in d
5list(d)
6list(d.values())
الإجابات: 1 2 2 2 3 True 4 False 5 ['a', 'b'] 6 [1, 2]
سؤال 11.2: ماذا يطبع البرنامج التالي؟
d = {"R": 3, "C": 1}
e = d
f = d.copy()
d["L"] = 2
e["R"] = 9
print(len(d), len(f))
print(d["R"], f["R"])
print(d.popitem())
print("L" in e)

11.3 وراثة الفئات – ירושה של מחלקות (inheritance)

الوراثة علاقة بين فئتين – המהות: ירושה כיחס בין שתי מחלקות:

نريد فئة للمقاومات وفئة للأسلاك. لكل قطعة اسم على اللوحة، وللمقاومة قيمة أيضا. لو كتبناهما كما في الفصل 8 لكررنا الأسطر نفسها. والتكرار هنا له معنى: كلتاهما قطعة إلكترونية. فلنكتب مرة واحدة ما هو مشترك، في فئة باسم Component، ولنبنِ عليها.

الوراثة (ירושה): علاقة بين فئتين: الفئة الجديدة تأخذ كل ما في فئة موجودة، وتضيف إليها أو تغيّر فيها. وكل كائن من الفئة الجديدة هو أيضا كائن من الفئة الموجودة.

هرمية واضحة – היררכיה ברורה וחד-משמעית:

الفئات التي ترث بعضها من بعض تكوّن هرمية (היררכיה) نرسمها كشجرة، والسهم يخرج من الفئة الوارثة إلى الفئة التي ترث منها:

objectComponentnameshow()WireResistorohmsshow()
كل فئة تكتب ما تضيفه فقط، وما عدا ذلك تأخذه ممن فوقها

الهرمية الجيدة تجيب بوضوح عن سؤال واحد: هل كل كائن من هذه الفئة هو كائن من تلك؟ فنقرأ الجملة بصوت عال، فإذا بدت غريبة فالوراثة خاطئة.

الجملةالجوابما نكتبه
المقاومة قطعة إلكترونيةصوابوراثة: class Resistor(Component)
المحطة حسّاسخطأ: المحطة فيها حسّاساتصفة: قائمة حسّاسات داخل Station، كما في الفصل 9
المقاومة سلكخطأفئتان شقيقتان، ولا وراثة بينهما

المصطلحان super وsubclass – המונחים super ו-subclass:

الفئة الأساس (מחלקת בסיס): الفئة التي نرث منها، وتسمى أيضا superclass.
الفئة المشتقّة (מחלקה נגזרת): الفئة التي ترث، وتسمى أيضا subclass. نكتب اسم الأساس بين قوسين بعد اسمها.

عمليّة بالاسم نفسه في الفئة المشتقّة تحل محل عمليّة الأساس، وهذا يسمى إعادة التعريف (דריסה). وللوصول إلى عمليّة الأساس نفسها توجد الدالة super(). والأمر pass يعني جسما فارغا عن قصد:

class Component:
    def __init__(self, name):
        self.name = name

    def show(self):
        print("Part:", self.name)

class Wire(Component):
    pass

class Resistor(Component):
    def __init__(self, name, ohms):
        super().__init__(name)
        self.ohms = ohms

    def show(self):
        super().show()
        print("Value:", self.ohms, "ohm")

w = Wire("W1")
r = Resistor("R1", 470)
w.show()
r.show()
print(r.name, r.ohms)
المُخرَج
Part: W1
Part: R1
Value: 470 ohm
R1 470

عند w.show() تبحث بايثون عن show في Wire فلا تجدها، فتصعد إلى Component. أما عند r.show() فتجدها في Resistor، وأول سطر فيها يستدعي عمليّة الأساس ثم يكمل. البحث يبدأ دائما من فئة الكائن، ويصعد في الهرمية حتى يجد.

خطأ شائع: إذا كتبنا __init__ في الفئة المشتقّة ونسينا super().__init__، فلن تكون للكائن صفات الأساس. والخطأ AttributeError يظهر بعيدا عن مكانه، عند أول استعمال للصفة.

الفئة الأم object – מחלקה קדומה בשם object:

كل فئة ترث في النهاية من فئة جاهزة اسمها object، حتى لو لم نكتب ذلك، فالكتابة class Component: تعني class Component(object):. ومن object تأتي مثلا صيغة __str__ الجاهزة التي رأيناها في الفصل 8. والأعداد والجمل النصية هي أيضا كائنات من فئات ترث منها.

الدالة issubclass – הפונקציה issubclass:

الدالة issubclass(a, b) تسأل عن فئتين: هل الأولى مشتقّة من الثانية؟ والدالة isinstance(x, c) تسأل عن كائن وفئة. والصفة __bases__ تعطي الفئات التي ترث منها الفئة مباشرة:

class Component:
    pass

class Resistor(Component):
    pass

print(Component.__bases__)
r = Resistor()
print(r)
print(issubclass(Resistor, Component))
print(issubclass(Component, Resistor))
print(issubclass(Resistor, object))
print(isinstance(r, Component))
print(type(r) == Component)
print(issubclass(r, Component))
المُخرَج
(<class 'object'>,)
<__main__.Resistor object at 0x000001F2A4C8>
True
False
True
True
False
TypeError: issubclass() arg 1 must be a class

السطران الأولان يبيّنان object والطباعة التي ورثناها منها، والعنوان ثابت في هذا الكتاب فقط. والعلاقة باتجاه واحد: كل مقاومة قطعة، وليست كل قطعة مقاومة. والسطر قبل الخطأ مهم: type(r) == Component تسأل عن الفئة بالضبط، أما isinstance فتقبل الفئات المشتقّة أيضا، ولذلك نفضّلها. والخطأ سببه أننا أعطينا issubclass كائنا بدل فئة.

الوراثة المتعددة – ירושה מרובה:

قد ترث الفئة من أكثر من فئة أساس، فنكتبها كلها بين القوسين. بطارية ذكية هي مصدر جهد، وهي أيضا جهاز يكتب سجلا. وكلتا الفئتين فيها info، فأيهما يعمل؟ لكل فئة ترتيب بحث ثابت نقرؤه من الصفة __mro__، وفئات الأساس تُفحص فيه من اليسار إلى اليمين:

class PowerSource:
    def info(self):
        return "Source"

class Logger:
    def info(self):
        return "Logger"

class SmartBattery(PowerSource, Logger):
    pass

class Other(Logger, PowerSource):
    pass

print(SmartBattery().info())
print(Other().info())
for c in SmartBattery.__mro__:
    print(c.__name__)
المُخرَج
Source
Logger
SmartBattery
PowerSource
Logger
object
خطأ شائع: إذا كان لفئتي الأساس عمليّة بالاسم نفسه ومعنيين مختلفين، حصلنا على واحدة منهما بلا أي تحذير، كما يبيّن Other. لذلك نستعمل الوراثة المتعددة عندما تكون الفئات الأساس صغيرة ومستقلة، ولكل منها عملها.

تعدّد الأشكال – רב-צורתיות, פולימורפיזם:

تعدّد الأشكال (פולימורפיזם): الأمر نفسه ينفّذ عملا مختلفا حسب نوع الكائن الذي يستقبله.

الممانعة (עכבה) تختلف من قطعة إلى أخرى. في المقاومة هي R ولا تتعلق بالتردد، أما في المكثّف والملف الكهربائي فمقدارها هو المفاعلة (היגב):

XC = 12·π·f·C
XL = 2·π·f·L

نكتب لكل فئة صيغتها من impedance، ونضع القطع في قائمة واحدة. والحلقة لا تسأل عن النوع أبدا، فكل كائن ينفّذ صيغته:

from math import pi

class Component:
    def __init__(self, name, value):
        self.name = name
        self.value = value

class Resistor(Component):
    def impedance(self, f):
        return self.value

class Capacitor(Component):
    def impedance(self, f):
        return 1 / (2 * pi * f * self.value)

class Inductor(Component):
    def impedance(self, f):
        return 2 * pi * f * self.value

parts = [Resistor("R1", 1000),
         Capacitor("C1", 0.000001),
         Inductor("L1", 0.1)]
for f in [50, 1000]:
    print("f =", f, "Hz")
    for part in parts:
        z = part.impedance(f)
        print(" ", part.name, round(z, 1), "ohm")
المُخرَج
f = 50 Hz
  R1 1000 ohm
  C1 3183.1 ohm
  L1 31.4 ohm
f = 1000 Hz
  R1 1000 ohm
  C1 159.2 ohm
  L1 628.3 ohm

عندما يكبر التردد تصغر مفاعلة المكثّف وتكبر مفاعلة الملف الكهربائي، وتبقى المقاومة كما هي. ولو أضفنا غدا فئة لقطعة أخرى، لما تغيّرت الحلقة بحرف واحد. وفي بايثون لا تلزم الوراثة حتى لهذا: يكفي عند الاستدعاء أن يكون للكائن عمليّة بهذا الاسم، وإلا توقف البرنامج في تلك اللحظة.

ملاحظة: لا نجمع ممانعات القطع المختلفة جمعا عاديا، لأن بينها فرق طور. هذا موضوع دروس التيار المتناوب، ولذلك نطبع كل قطعة وحدها.

العمليّة الهادمة – פונקציה הורסת, destructor:

العمليّة الهادمة (פעולה הורסת): العمليّة __del__: تستدعيها بايثون بنفسها عندما توشك أن تزيل الكائن من الذاكرة، وهي عكس __init__.

الأمر del لا يهدم الكائن، بل يحذف الاسم فقط. أما الكائن فيزول عندما لا يبقى اسم يشير إليه. لذلك لا يحدث شيء عند حذف الاسم الأول، بل عند حذف الثاني:

class Port:
    def __init__(self, name):
        self.name = name
        print("Open", self.name)

    def __del__(self):
        print("Close", self.name)

a = Port("COM3")
b = a
del a
print("Still open")
del b
print("After second del")
المُخرَج
Open COM3
Still open
Close COM3
After second del
خطأ شائع: هذا المُخرَج ثابت في بايثون التي نستعملها على الحاسوب، لكن لغة بايثون نفسها لا تعد بموعد استدعاء __del__، ولا حتى بأنها ستُستدعى عند انتهاء البرنامج. ولهذا حذفنا الاسمين بأنفسنا. فلا نعتمد عليها في إغلاق ملف أو منفذ: لهذا العمل نستعمل with من البند 11.4 أو finally من البند 11.5.

عمليّات النسخة والفئة والساكنة – INSTANCE, CLASS, STATIC METHODS:

النوعالوسيط الأولتصل إلىالاستعمال
عمليّة النسخة (פעולת מופע)selfصفات الكائن وفئتهما يخص كائنا واحدا
عمليّة الفئة (פעולת מחלקה) @classmethodclsمتغيّرات الفئةطريقة أخرى لبناء كائن
عمليّة ساكنة (פעולה סטטית) @staticmethodلا شيءوسائطها فقطحساب لا يحتاج إلى كائن
class Resistor:
    def __init__(self, ohms):
        self.ohms = ohms

    def power(self, volts):
        return volts ** 2 / self.ohms

    @classmethod
    def from_code(cls, code):
        return cls(int(code[:-1]) * 10 ** int(code[-1]))

    @staticmethod
    def parallel(a, b):
        return a * b / (a + b)

r = Resistor(50)
s = Resistor.from_code("472")
print(r.power(5))
print(s.ohms)
print(Resistor.parallel(100, 300))
print(r.parallel(100, 100))
المُخرَج
0.5
4700
75.0
50.0

التحميل الزائد للعمليّات – העמסת מתודות:

في لغات مثل C#‎ يمكن أن نكتب في الفئة نفسها عمليّتين بالاسم نفسه ووسائط مختلفة. أما في بايثون فهذا لا يوجد. الاسم يشير إلى عمليّة واحدة، والتعريف الثاني يحل محل الأول بلا تحذير:

class Load:
    def power(self, volts):
        return volts ** 2 / 100

    def power(self, volts, amps):
        return volts * amps

d = Load()
print(d.power(12, 2))
print(d.power(12))
المُخرَج
24
TypeError: Load.power() missing 1 required positional argument: 'amps'

الطريقة في بايثون هي عمليّة واحدة فيها وسيط بقيمة افتراضية، كما تعلمنا في الفصل 7. والقيمة الخاصة None تعني «لم يُعطَ شيء»، ونسأل عنها بواسطة is:

class Load:
    def __init__(self, ohms):
        self.ohms = ohms

    def power(self, volts, amps=None):
        if amps is None:
            return volts ** 2 / self.ohms
        return volts * amps

d = Load(100)
print(d.power(12))
print(d.power(12, 2))
print(d.power(amps=2, volts=5))
المُخرَج
1.44
24
10

ويجب ألا نخلط بين هذا وبين إعادة التعريف، التي تكون في فئة مشتقّة، وهي موجودة في بايثون وهي أساس تعدّد الأشكال.

التحميل الزائد للمعاملات – העמסת אופרטורים:

رأينا في الفصل 8 أن كتابة __str__ تجعل print يعمل على كائننا. وبالطريقة نفسها نجعل المعاملات تعمل عليه: عندما نكتب a + b، تترجمها بايثون إلى a.__add__(b). وتوجد أيضا __sub__ لـ -، و__mul__ لـ *، و__eq__ لـ ==، و__lt__ لـ <. وماذا تعني الإشارة + بين مقاومتين؟ التوصيل على التوالي:

class Res:
    def __init__(self, ohms):
        self.ohms = ohms

    def __add__(self, other):
        return Res(self.ohms + other.ohms)

    def __eq__(self, other):
        return self.ohms == other.ohms

    def __lt__(self, other):
        return self.ohms < other.ohms

    def __str__(self):
        return str(self.ohms) + " ohm"

a = Res(470)
b = Res(1000)
c = Res(470)
print(a + b + c)
print(a == c, a != b, a is c)
print(a < b)
المُخرَج
1940 ohm
True True False
True
خطأ شائع: الوسيط other يجب أن يكون من نوعنا: Res(100) + 220 يوقف البرنامج لأن العدد ليس فيه ohms. ومن يكتب __eq__ يفقد إمكانية وضع الكائن في مجموعة أو استعماله مفتاحا في قاموس، إلا إذا كتب أيضا __hash__.

الفئات المجرّدة – מחלקות מופשטות, אבסטרקטיות:

ما هي ممانعة «قطعة» لا نعرف نوعها؟ لا معنى للسؤال. فالفئة Component موجودة لتُورث، لا لكي نبني منها كائنات.

الفئة المجرّدة (מחלקה מופשטת): فئة لا يمكن بناء كائن منها مباشرة، وتحدد عمليّات يجب على كل فئة مشتقّة أن تكتبها.

نبني ذلك بواسطة الوحدة الجاهزة abc: نرث من ABC، ونضع @abstractmethod قبل كل عمليّة يجب أن تُكتب في الفئات المشتقّة:

from abc import ABC, abstractmethod

class Component(ABC):
    def __init__(self, name):
        self.name = name

    @abstractmethod
    def impedance(self, f):
        pass

class Resistor(Component):
    def __init__(self, name, ohms):
        super().__init__(name)
        self.ohms = ohms

    def impedance(self, f):
        return self.ohms

r = Resistor("R1", 470)
print(r.name, r.impedance(50))
c = Component("X1")
المُخرَج
R1 470
TypeError: Can't instantiate abstract class Component without an implementation for abstract method 'impedance'

والربح الحقيقي يظهر في الفئة المشتقّة التي نسيت أن تكتب العمليّة: بناء كائن منها يعطي الخطأ نفسه، مع اسم العمليّة الناقصة، ولا ينتظر الخطأ حتى أول استدعاء في وسط التشغيل.

سؤال 11.3: ماذا يطبع البرنامج التالي؟ انتبهوا إلى أي g يعمل في كل حالة.
class A:
    def __init__(self):
        self.x = 1

    def f(self):
        return self.x + self.g()

    def g(self):
        return 10

class B(A):
    def g(self):
        return 20

class C(B):
    def __init__(self):
        super().__init__()
        self.x = 5

print(A().f(), B().f(), C().f())
print(issubclass(C, A), isinstance(A(), B))

11.4 العمل مع الملفات – עבודה עם קבצים (files)

كل ما عرفناه حتى الآن يختفي عندما ينتهي البرنامج. أما الملف (קובץ) فهو نص محفوظ على القرص، يقرؤه برنامجنا اليوم، ويكمل الكتابة فيه غدا. ونعمل هنا مع الملفات النصية التي نستطيع أن نفتحها أيضا في المفكرة. كل برنامج في الكتاب يعمل في مجلد فارغ خاص به، ومعظم برامج هذا البند تعمل على ملف سجل اسمه log.txt.

فتح الملف والقراءة منه – פתיחת קובץ טקסט, קריאת טקסט מקובץ:

نفتح، نقرأ أو نكتب، ثم نغلق. الدالة open تعطي كائن ملف، والعمليّة read من دون وسيط تعطي كل ما بقي في الملف، كجملة نصية واحدة:

f = open("log.txt")
text = f.read()
f.close()
print(text)
print(type(text))
print(len(text))
المُخرَج
BAT=12.42
TMP=31.5
SOL=18.0

<class 'str'>
28

الملف جملة نصية عادية، و\n حرف واحد منها في آخر كل سطر. ولأن آخر الملف \n، ولأن print تضيف سطرا من عندها، ظهر سطر فارغ في المُخرَج. والمسار الكامل في ويندوز نكتبه كجملة نصية خام: r"C:\data\log.txt".

ملاحظة: إذا كان في الملف حروف عربية أو عبرية أو رموز مثل Ω، نحدد الترميز (קידוד): open("a.txt", encoding="utf-8"). من دون ذلك قد يقرأ ويندوز الملف بترميز آخر، فتظهر حروف غريبة أو خطأ.

قراءة عدد محدد: read(n) – קריאת מספר מסוים של תווים:

يسمي المنهاج هذا «تحميلا زائدا للعمليّة read». وهو كما رأينا في البند 11.3 ليس كذلك، بل عمليّة واحدة بوسيط اختياري: f.read(n) تقرأ حتى n من الحروف، وتكمل كل قراءة من حيث توقفت السابقة. وعندما لا يبقى شيء تعطي جملة فارغة. والدالة repr في السطر الثالث تُظهر الجملة النصية مع رموزها، وسنعود إليها عند العمليّة readline:

f = open("log.txt")
print(f.read(3))
print(f.read(6))
print(repr(f.read(1)))
print(f.read(3))
f.close()
المُخرَج
BAT
=12.42
'\n'
TMP
دقّة: يقول نص المنهاج «عدد من البايتات»، وهذا صحيح فقط في الوضع الثنائي "rb". في الوضع النصي يعدّ الوسيط حروفا. والفرق يظهر مع الحرف Ω الذي يشغل بايتين. في الملف 470Ω، ونقرأ أربعة في كل من الوضعين:
f = open("ohm.txt", encoding="utf-8")
print(f.read(4))
f.close()
f = open("ohm.txt", "rb")
print(f.read(4))
f.close()
المُخرَج
470Ω
b'470\xce'

أوضاع الفتح – תצורות פתיחת קובץ:

الوسيط الثاني للدالة open هو وضع الفتح (מצב פתיחה)، وإذا حذفناه كان "r":

الوضعالمعنىإذا لم يوجد الملفمحتوى الملف الموجوديبدأ من
"r"قراءة فقطخطأيبقىالبداية
"w"كتابة فقطيُنشأيُمحى فوراالبداية
"a"إضافة إلى الآخريُنشأيبقىالنهاية
"r+"قراءة وكتابةخطأيبقى، والكتابة تكتب فوقهالبداية

العمليّة write تكتب جملة نصية، وتعيد عدد الحروف التي كتبتها. فتحنا الملف هنا مرتين بالوضع "w"، وفي الثانية مُحي كل ما كان فيه:

f = open("out.txt", "w")
print(f.write("BAT=12.42\n"))
f.close()
f = open("out.txt", "w")
f.write("NEW\n")
f.close()
f = open("out.txt")
print(f.read(), end="")
f.close()
المُخرَج
10
NEW
خطأ شائع: الوضع "w" يمحو الملف قبل أن نكتب حرفا، ولا يسألنا. والعمليّة write لا تضيف \n من عندها، ولا تقبل غير جملة نصية، فنحوّل العدد أولا بواسطة str.

الوضع "a" هو وضع السجلات: كل تشغيل للبرنامج يضيف سطوره إلى آخر الملف:

f = open("log.txt", "a")
f.write("HUM=40.0\n")
f.close()
f = open("log.txt")
print(f.read(), end="")
f.close()
المُخرَج
BAT=12.42
TMP=31.5
SOL=18.0
HUM=40.0

الوضع "r+" يقرأ ويكتب في الملف نفسه. الكتابة تحل محل الحروف الموجودة في موضع مؤشّر الملف ولا تدفعها إلى الأمام: لا يوجد «إدراج» في ملف. هنا نستبدل AUTO بكلمة لها الطول نفسه:

f = open("cfg.txt", "r+")
print(f.readline(), end="")
f.seek(5)
f.write("HAND")
f.seek(0)
print(f.read(), end="")
f.close()
المُخرَج
MODE=AUTO
MODE=HAND
RATE=9600

إغلاق الملف – סגירת קובץ טקסט:

لماذا نغلق؟ لأن write لا تكتب على القرص فورا: هي تجمع ما كتبناه في ذاكرة مؤقتة، وclose يكتب ما تجمّع ويحرر الملف. فنقرأ الملف قبل الإغلاق فنجده فارغا، وبعده فنجد النص. والصفة closed تقول هل أُغلق، والكتابة بعد الإغلاق تتوقف بخطأ:

out = open("a.txt", "w")
out.write("saved?")
check = open("a.txt")
print(repr(check.read()))
out.close()
print(repr(check.read()))
check.close()
print(out.closed)
out.write("more")
المُخرَج
''
'saved?'
True
ValueError: I/O operation on closed file.

مؤشّر الملف والعمليّة seek – מצביע הקובץ, seek:

مؤشّر الملف (מצביע הקובץ): عدد يحفظه كائن الملف: مكان القراءة أو الكتابة التالية. يبدأ عادة من صفر، ويتقدم مع كل قراءة وكتابة.
051015202528BAT=12.42\nTMP=31.5\nSOL=18.0\nopenf.read(3)f.read(6)f.readline()f.readline()f.seek(4)f.seek(0)
موضع مؤشّر الملف بعد كل أمر: الجزء الملوّن هو ما قُرئ، والسهم هو المكان الذي تبدأ منه القراءة التالية

العمليّة f.seek(n) تضع مؤشّر الملف في مكان نحدده. وهذا يجيب عن سؤال يتكرر: لماذا تعطي القراءة الثانية بعد f.read() جملة فارغة؟ لأن مؤشّر الملف في آخره.

f = open("log.txt")
print(f.tell())
print(f.read(3))
print(f.tell())
f.seek(0)
print(f.read(3))
f.seek(4)
print(f.read(5))
print(f.tell())
f.close()
المُخرَج
0
BAT
3
BAT
12.42
9

هنا أيضا يقول المنهاج «تحميل زائد»، وهو وسيط ثانٍ اختياري: في seek(offset, whence) تحدد whence نقطة الانطلاق: 0 البداية وهي القيمة الافتراضية، و1 الموضع الحالي، و2 النهاية. وهذا يعمل بحرية في الوضع الثنائي، وهناك يكون الناتج بايتات تُطبع مع البادئة b، والعمليّة decode تحوّلها إلى جملة نصية:

f = open("log.txt", "rb")
f.seek(-9, 2)
print(f.read())
f.seek(4)
f.seek(3, 1)
print(f.tell())
print(f.read(2).decode())
f.close()
المُخرَج
b'SOL=18.0\n'
7
42

أما في الوضع النصي فلا يُسمح من الموضع الحالي ومن النهاية إلا بالقيمة صفر للوسيط offset: f.seek(0, 2) ينقلنا إلى آخر الملف، وf.seek(-9, 2) يوقف البرنامج.

العمليّة tell – הפונקציה tell:

العمليّة tell تعطي موضع مؤشّر الملف، وأنفع استعمال لها أن نحفظ موضعا لنعود إليه. هنا نحفظ أين وصلنا، ثم يضيف جهاز ما سطرا إلى السجل، فنقرأ الجديد فقط:

f = open("log.txt")
f.read()
mark = f.tell()
f.close()
print(mark)
w = open("log.txt", "a")
w.write("BAT=11.90\n")
w.close()
f = open("log.txt")
f.seek(mark)
print(f.read(), end="")
f.close()
المُخرَج
28
BAT=11.90
دقّة: في الوضع النصي لا نحسب بأنفسنا ما تعطيه tell، بل نعيده إلى seek كما هو. برنامجنا يكتب \n حرفا واحدا، لكن ويندوز يحفظه على القرص حرفين هما \r\n، كما يظهر في البرنامج التالي الذي شغّلناه على ويندوز. وفي Linux وmacOS يكون الناتج 2، والبايتات b'A\nB\n'. أما ملفات السجل في أمثلتنا فكُتبت بالرمز \n وحده، ولذلك فإن الأعداد التي كتبناها مباشرة في seek صحيحة: كل حرف قبلها يشغل بايتا واحدا.
f = open("t.txt", "w")
f.write("A\nB\n")
f.close()
f = open("t.txt")
print(f.readline(), end="")
print(f.tell())
f.close()
f = open("t.txt", "rb")
print(f.read())
f.close()
المُخرَج
A
3
b'A\r\nB\r\n'

العمليّة readline – הפונקציה readline:

العمليّة readline تقرأ سطرا واحدا، مع \n في آخره، وعند نهاية الملف تعطي جملة فارغة. نطبع بواسطة repr لنرى الجملة مع رموزها، والعمليّة strip تحذف المسافات و\n من طرفيها:

f = open("log.txt")
first = f.readline()
print(repr(first))
print(first.strip())
print(repr(f.readline()))
print(repr(f.readline()))
print(repr(f.readline()))
f.close()
المُخرَج
'BAT=12.42\n'
BAT=12.42
'TMP=31.5\n'
'SOL=18.0\n'
''

السطر الفارغ في الملف هو "\n" وليس جملة فارغة، فالجملة الفارغة تعني شيئا واحدا فقط: انتهى الملف. وعلى هذا نبني الحلقة:

f = open("log.txt")
line = f.readline()
while line != "":
    print(line.strip())
    line = f.readline()
f.close()
المُخرَج
BAT=12.42
TMP=31.5
SOL=18.0

العمليّة readlines – הפונקציה readlines:

العمليّة readlines تقرأ الملف كله مرة واحدة، وتعطي قائمة من السطور، وكل سطر مع \n في آخره:

f = open("log.txt")
lines = f.readlines()
f.close()
print(lines)
print(len(lines))
المُخرَج
['BAT=12.42\n', 'TMP=31.5\n', 'SOL=18.0\n']
3
مثال 11.3
في الملف volts.txt قراءة جهد في كل سطر. نحسب المتوسط وأعلى قيمة. أبسط طريقة هي الحلقة for على كائن الملف نفسه: تعطي سطرا في كل دورة، وfloat لا تعترض على \n.
الحل:
f = open("volts.txt")
values = []
for line in f:
    values.append(float(line))
f.close()
print(values)
print(round(sum(values) / len(values), 2))
print(max(values))
المُخرَج
[12.42, 12.1, 11.95]
12.16
12.42

العمليّة splitlines – הפונקציה splitlines:

إذا لم نكن نريد \n نقرأ الملف كله ونقسمه بواسطة splitlines. وهي أفضل من split("\n")، التي تعطي عنصرا فارغا زائدا لأن آخر الملف \n:

f = open("log.txt")
lines = f.read().splitlines()
f.close()
print(lines)
print("A\nB\n".split("\n"))
print("A\nB\n".splitlines())
print("A\r\nB".splitlines())
المُخرَج
['BAT=12.42', 'TMP=31.5', 'SOL=18.0']
['A', 'B', '']
['A', 'B']
['A', 'B']
خطأ شائع: المثال في نص المنهاج مكتوب file.read().splitlines، بلا قوسين. من دون قوسين لا نستدعي العمليّة، بل نحصل عليها هي نفسها، ولا تظهر رسالة خطأ.

الطباعة إلى ملف – עבודה עם print לתוך קובץ:

الدالة print تكتب إلى الملف إذا أعطيناها الوسيط file=f. وكل ما نعرفه عنها يعمل كما هو: تقبل أي نوع، وتفصل بمسافة، وتضيف \n، وتقبل sep. لذلك نختارها للتقارير والجداول، ونختار write لجملة نصية جاهزة. نكتب هنا جدول قدرة بصيغة CSV، وهي ملف نصي يفتحه برنامج الجداول:

loads = [(12.0, 0.5), (5.0, 0.2), (24.0, 1.5)]
f = open("power.csv", "w")
print("V,I,P", file=f)
for v, i in loads:
    print(v, i, v * i, sep=",", file=f)
f.close()
f = open("power.csv")
print(f.read(), end="")
f.close()
المُخرَج
V,I,P
12.0,0.5,6.0
5.0,0.2,1.0
24.0,1.5,36.0

الأمر with – שימוש ב-with:

الأمر with open("log.txt") as f: يفتح الملف، وعندما ينتهي الجسم المُزاح يُغلق الملف من تلقاء نفسه، حتى لو توقف الجسم بخطأ. يقول المنهاج إنه يلغي الحاجة إلى الفتح والإغلاق، والأدق أنه يلغي الحاجة إلى close فقط. وهذه هي الطريقة التي نستعملها من الآن فصاعدا:

with open("log.txt") as f:
    print(f.closed)
    first = f.readline()
print(f.closed)
print(first.strip())
المُخرَج
False
True
BAT=12.42

وفي السطر نفسه يمكن أن نفتح ملفين: with open("a.txt") as src, open("b.txt", "w") as dst:.

سؤال 11.4: ماذا يطبع البرنامج التالي، إذا كان log.txt هو ملف هذا البند؟
with open("log.txt", "a") as f:
    print("END", file=f)
with open("log.txt") as f:
    print(f.read(4))
    print(f.readline().strip())
    print(len(f.readlines()))
    f.seek(0)
    print(f.read().splitlines()[-1])

11.5 معالجة الاستثناءات – טיפול בחריגים (exceptions)

الفرق بين الاستثناء وخطأ الترجمة – ההבדל בין חריג לשגיאת קומפילציה:

ميّزنا في البند 2.14 بين الخطأ النحوي وخطأ التشغيل. يسمي المنهاج الأول «خطأ ترجمة» (שגיאת קומפילציה)، لأن بايثون قبل التنفيذ تترجم البرنامج كله إلى شيفرة وسيطة bytecode. ولا يُكشف في هذه الترجمة إلا خطأ في شكل الكتابة، أي خطأ نحوي، فلا يعمل شيء من البرنامج، حتى السطر الصحيح الذي قبله:

print("Start")
print("Reading"
المُخرَج
SyntaxError: '(' was never closed
الاستثناء (חריג): حالة خطأ تظهر في أثناء التشغيل، فتوقف البرنامج إذا لم نعالجها. وهي كائن من فئة، مثل ZeroDivisionError.
print("Start")
print(10 / 0)
print("End")
المُخرَج
Start
ZeroDivisionError: division by zero

والخطأ في الأنواع هو في بايثون استثناء لا خطأ ترجمة، فلا يُكشف إلا عندما نصل إلى السطر. البرنامج التالي يعمل بسلام، وفيه سطر خاطئ ينتظر اليوم الذي يصبح فيه الجهد أكبر من 100:

volts = 12
if volts > 100:
    print("High" + 1)
print("OK")
المُخرَج
OK
التعبيرالاستثناءالمعنى
int("hot")ValueErrorجملة نصية لا تصلح عددا
1 / 0ZeroDivisionErrorقسمة على صفر
[1, 2][5]IndexErrorمؤشّر خارج القائمة
{"a": 1}["b"]KeyErrorمفتاح غير موجود
"V=" + 5TypeErrorنوعان لا يعملان معا
voltsNameErrorاسم لم يُعرَّف
"AB".sizeAttributeErrorصفة أو عمليّة غير موجودة
open("no.txt")FileNotFoundErrorملف غير موجود

المقطع try – בלוק try:

نضع في مقطع try الأسطر التي قد تفشل. فإذا ظهر استثناء تتوقف بقية المقطع، وينتقل التنفيذ إلى مقطع except المناسب. وإذا لم يظهر استثناء يُتجاوز مقطع except كله.

try:line 1line 2except ValueError:finally:next lineValueErrorno error
طريقان: الأخضر من دون خطأ، والأحمر عندما يفشل السطر الأول. وسنعرف finally في آخر هذا البند

المقطع except – בלוק except:

text = input("Volts: ")
try:
    volts = float(text)
    print("Double:", volts * 2)
except ValueError:
    print("Not a number")
print("Done")
المُخرَج
Volts: abc
Not a number
Done

المُدخَل هنا ليس عددا. لم يتوقف البرنامج، ولم يُنفَّذ السطر الذي بعد الفشل. وللمُدخَل 12.5 يطبع: Double: 25.0 / Done.

مثال 11.4
في البند 5.7 كتبنا فحصا طويلا لكي نعرف هل الجملة النصية عدد، ولم يقبل حتى الأعداد السالبة. فلنترك float تقرر، ونعيد السؤال حتى يصل عدد سليم. هذه مصفاة المُدخَلات العنيدة من الفصل 4، مع try:
الحل:
while True:
    try:
        volts = float(input("Volts: "))
        break
    except ValueError:
        print("Try again")
print("Got", volts)
المُخرَج
Volts: abc
Try again
Volts: 12
Got 12.0

الأمر break لا يُنفَّذ إلا إذا نجح السطر الذي قبله. وأسلوب «نجرّب ونعالج الفشل» هو الأسلوب المعتاد في بايثون.

قد تفشل الأسطر بأكثر من طريقة، ونكتب لكل نوع مقطعا، فيُنفَّذ أول مقطع يطابق. ومقطع واحد يقبل أيضا عدة أنواع في صفّ ثابت: except (ValueError, KeyError):. أما الاستثناء الذي لا يطابق أي مقطع فيوقف البرنامج كما كان.

try:
    volts = float(input("Volts: "))
    ohms = float(input("Ohms: "))
    print("Current:", volts / ohms, "A")
except ValueError:
    print("Need a number")
except ZeroDivisionError:
    print("Ohms cannot be zero")
المُخرَج
Volts: 12
Ohms: 0
Ohms cannot be zero
المُدخَلالمُخرَج
12 , 4Current: 3.0 A
12 , xNeed a number
xNeed a number

الاستثناءات فئات، وبينها وراثة كما في البند 11.3. والمقطع يطابق الفئة وكل ما اشتُقّ منها، فمقطع except Exception يمسك معظم الاستثناءات، لأن ZeroDivisionError مثلا مشتقّة من Exception.

خطأ شائع: لذلك يهمّ ترتيب المقاطع: نكتب الخاص قبل العام. هنا لن يصل التنفيذ إلى مقطع ZeroDivisionError أبدا:
try:
    print(10 / int(input("Divisor: ")))
except Exception:
    print("Something failed")
except ZeroDivisionError:
    print("Zero")
المُخرَج
Divisor: 0
Something failed
خطأ شائع: المقطع except: بلا اسم يمسك كل شيء، حتى أخطاءنا في الكتابة. هنا كُتب اسم المتغيّر خطأ، فظهر NameError، لكن البرنامج يبلّغ عن مشكلة في المؤشّر. لذلك نحدد دائما النوع الذي نتوقعه:
values = [1, 2]
try:
    print(valeus[0])
except:
    print("Bad index")
المُخرَج
Bad index

تداخل الاستثناءات – קינון של exceptions:

يجوز أن يكون مقطع try داخل مقطع try آخر. هذا طبيعي عندما تكون لدينا مشكلتان على مستويين: الملف كله غير موجود، أو سطر واحد فاسد فيه:

total = 0
bad = 0
try:
    with open("volts.txt") as f:
        for line in f:
            try:
                total += float(line)
            except ValueError:
                bad += 1
    print("Total:", round(total, 2))
    print("Bad lines:", bad)
except FileNotFoundError:
    print("No file")
المُخرَج
Total: 24.3
Bad lines: 1

وعندما لا يوجد الملف يطبع البرنامج نفسه: No file. وإذا لم يطابق الاستثناء المقطع الداخلي، يصعد إلى المقطع الخارجي. وكذلك عبر العمليّات: الاستثناء الذي لم يُعالج في عمليّة يصعد إلى من استدعاها. ففي parse لا يوجد try، والقرار يبقى للمستدعي:

def parse(item):
    name, text = item.split("=")
    return name, float(text)

for item in ["BAT=12.4", "TMP31", "SOL=x"]:
    try:
        print(parse(item))
    except ValueError:
        print("Bad item:", item)
المُخرَج
('BAT', 12.4)
Bad item: TMP31
Bad item: SOL=x

الاسم البديل as – עבודה עם alias:

الكتابة except ValueError as e: تضع كائن الاستثناء في المتغيّر e. طباعته تعطي نص الرسالة، وtype(e).__name__ يعطي اسم فئته. هكذا نميّز بين الاستثناءات، حتى عندما يمسكها مقطع واحد:

for text in ["25", "abc", "0"]:
    try:
        print(100 / int(text))
    except (ValueError, ZeroDivisionError) as e:
        print(type(e).__name__, "-", e)
المُخرَج
4.0
ValueError - invalid literal for int() with base 10: 'abc'
ZeroDivisionError - division by zero

المقطع finally – עבודה עם finally:

المقطع finally يُنفَّذ دائما: إذا نجح try، وإذا عولج استثناء، وإذا لم يُعالج، وحتى إذا خرجنا بالأمر return. وهو المكان الصحيح لإعادة شيء إلى حالته: إغلاق ملف، أو إطفاء محرك. في البرنامج التالي لا يوجد except، فالاستثناء يوقف البرنامج، ولكن المحرك يُطفأ قبل ذلك:

try:
    print("Motor on")
    print(1 / 0)
    print("Work done")
finally:
    print("Motor off")
print("End")
المُخرَج
Motor on
Motor off
ZeroDivisionError: division by zero

والأمر with يعمل في الحقيقة مثل finally يغلق الملف: لو كتبناه بأنفسنا لفتحنا الملف قبل try، وكتبنا f.close() في finally.

سؤال 11.5: ماذا يطبع البرنامج التالي؟
def f(x):
    try:
        print(10 / x)
    except ZeroDivisionError:
        print("Z")
    finally:
        print("F")

try:
    f(5)
    f(0)
    f("a")
    print("never")
except TypeError as e:
    print(type(e).__name__)
print("end")
مشروع الكتاب: محطة القياس · المرحلة 11 — المحطة تتذكّر في ملف

كانت المحطة تنسى كل شيء عند إطفائها. نكتب الآن برنامجا يقرأ سجلا من القرص، فيه سطور من الشكل الذي رأيناه في الفصل 5، وبعضها فاسد لأن الخط أصابه تشويش. لا يتوقف البرنامج عند سطر فاسد: يعدّه ويتجاوزه، ثم يكتب ملخّصا إلى ملف. هذا هو السجل:

with open("log.txt") as f:
    print(f.read(), end="")
المُخرَج
BAT=12.42;TMP=31.5
BAT=11.65;TMP=33.0
BAT=12.10;TMP=hot
BAT12.05;TMP=38.5
BAT=11.95;TMP=41.5
BAT=12.20
BAT=12.31;TMP=42.0

نستعمل هنا مواضيع الفصل كلها. settings قاموس بدل أعداد مبعثرة في البرنامج. faulty مجموعة، لأن الحسّاس الذي فشل عشر مرات يبقى حسّاسا واحدا. try يعالج السطور الفاسدة، والملخّص يُكتب إلى summary.txt بواسطة print:

settings = {"cutoff": 11.8, "fan": 40.0}
faulty = set()
good = bad = low = hot = 0

with open("log.txt") as f:
    lines = f.read().splitlines()

for line in lines:
    try:
        values = {}
        for item in line.split(";"):
            sensor = item[:3]
            name, text = item.split("=")
            values[name] = float(text)
        bat, tmp = values["BAT"], values["TMP"]
        if bat < settings["cutoff"]:
            low += 1
        if tmp > settings["fan"]:
            hot += 1
        good += 1
    except ValueError as e:
        print("Skipped:", line, "-", e)
        faulty.add(sensor)
        bad += 1
    except KeyError as e:
        print("Skipped:", line, "- no", e)
        faulty.add(e.args[0])
        bad += 1

with open("summary.txt", "w") as out:
    print("Good lines:", good, file=out)
    print("Bad lines:", bad, file=out)
    print("Low battery:", low, file=out)
    print("Fan on:", hot, file=out)
    print("Faulty:", sorted(faulty), file=out)

with open("summary.txt") as f:
    print(f.read(), end="")
المُخرَج
Skipped: BAT=12.10;TMP=hot - could not convert string to float: 'hot'
Skipped: BAT12.05;TMP=38.5 - not enough values to unpack (expected 2, got 1)
Skipped: BAT=12.20 - no 'TMP'
Good lines: 4
Bad lines: 3
Low battery: 1
Fan on: 2
Faulty: ['BAT', 'TMP']

خطآن مختلفان وصلا إلى المقطع نفسه: float عندما تكون الجملة النصية غير عددية، وعندما لا تكون في القراءة علامة =، لأن فكّ القائمة إلى متغيّرين يفشل. والرسالة في e تبيّن الفرق. أما السطر الناقص فليس فيه قراءة حرارة، وvalues["TMP"] تعطي KeyError، واسم الحسّاس نجده في e.args[0]. ولا يزيد good إلا في آخر المقطع، بعد أن نجح كل ما قبله.

والآن الوراثة. الفئة Sensor من الفصلين 8 و9 تعمل جيدا، ولا نريد أن نلمسها. حسّاس الحرارة يحتاج إلى شيء إضافي واحد، وهو الإنذار، فنشتقّ له فئة. والقائمة تحمل كائنات من نوعين، وprint يختار __str__ المناسبة:

class Sensor:
    step = 5 / 1023

    def __init__(self, name, unit, scale):
        self.name = name
        self.unit = unit
        self.scale = scale
        self.raw = 0

    def read(self, raw):
        self.raw = raw

    def value(self):
        return round(self.raw * Sensor.step * self.scale, 2)

    def __str__(self):
        text = self.name + ": " + str(self.value())
        return text + " " + self.unit

class TempSensor(Sensor):
    def __init__(self, name, limit):
        super().__init__(name, "C", 100)
        self.limit = limit

    def alarm(self):
        return self.value() > self.limit

    def __str__(self):
        text = super().__str__()
        if self.alarm():
            text += " ALARM"
        return text

sensors = [Sensor("Battery", "V", 4),
           TempSensor("Board", 40.0),
           TempSensor("Motor", 80.0)]
raws = [620, 90, 90]
for i in range(len(sensors)):
    sensors[i].read(raws[i])
    print(sensors[i])
المُخرَج
Battery: 12.12 V
Board: 43.99 C ALARM
Motor: 43.99 C

نجرّب بأنفسنا: نضيف إلى البرنامج الأول قاموس عدّادات، يعدّ كم مرة ظهر كل نوع استثناء، ونكتبه في الملخّص. ثم نقرأ القاموس settings من ملف بدل أن يكون مكتوبا في البرنامج.

أهمّ أفكار الفصل:

ملخّص أوامر الفصل:

الأمرعمله
{1, 2} set() set(x)تهيئة مجموعة، وفارغة، وتحويل
s.add(x) s.update(t) s.union(t)إضافة عنصر، وإضافة عناصر، واتحاد جديد
s.discard(x) s.pop() s.clear()حذف بلا خطأ، وسحب عنصر، وتفريغ
{"a": 1} {} dict()تهيئة قاموس، وقاموس فارغ
d[k] d[k] = v k in d d.get(k, x)قراءة، وإضافة أو تغيير، وفحص، وقراءة بلا خطأ
d.items() d.keys() d.values()الأزواج، والمفاتيح، والقيم
d.pop(k) d.popitem() del d[k]حذف بالمفتاح، وحذف آخر زوج، وحذف بأمر
class B(A): super().__init__()فئة مشتقّة، واستدعاء عمليّة البناء في الأساس
issubclass(B, A) isinstance(x, A)العلاقة بين فئتين، وبين كائن وفئة
__del__ @classmethod @staticmethodهادمة، وعمليّة فئة، وعمليّة ساكنة
__add__ __eq__ __lt__تحميل المعاملات + و== و<
ABC @abstractmethodفئة مجرّدة، وعمليّة مجرّدة
f = open(name, mode) f.close()فتح بأحد الأوضاع r وw وa وr+، وإغلاق
f.read() f.read(n) f.readline()الكل، أو حتى n حرفا، أو سطر
f.readlines() f.read().splitlines()قائمة السطور، مع \n أو من دونه
f.seek(n) f.seek(0, 2) f.tell()نقل مؤشّر الملف، وإلى النهاية، ومعرفة موضعه
f.write(s) print(x, file=f)كتابة جملة نصية، وطباعة إلى الملف
with open(name) as f:فتح مع إغلاق تلقائي
try: except T as e: finally:محاولة، ومعالجة مع الكائن، ومقطع يعمل دائما

قاموس المصطلحات – מילון מונחים:

بالعربيةبالعبرية (كما تظهر في الامتحان)English
المجموعةSetSet
القاموسמילוןDictionary
المفتاحמפתחKey
الوراثةירושהInheritance
هرميةהיררכיהHierarchy
الفئة الأساسמחלקת בסיסBase class, Superclass
الفئة المشتقّةמחלקה נגזרתDerived class, Subclass
إعادة التعريفדריסהOverriding
تعدّد الأشكالפולימורפיזםPolymorphism
العمليّة الهادمةפעולה הורסתDestructor
عمليّة النسخةפעולת מופעInstance method
عمليّة الفئةפעולת מחלקהClass method
العمليّة الساكنةפעולה סטטיתStatic method
التحميل الزائدהעמסהOverloading
الفئة المجرّدةמחלקה מופשטתAbstract class
الملفקובץFile
وضع الفتحמצב פתיחהFile mode
مؤشّر الملفמצביע הקובץFile pointer
الترميزקידודEncoding
الاستثناءחריגException
خطأ الترجمةשגיאת קומפילציהCompilation error
الإجابات النهائية لأسئلة الفصل
11.1 2 3 , False True , [2, 5] [1, 2, 3]
11.2 3 2 , 9 3 , ('L', 2) , False
11.3 11 21 25 , True False
11.4 BAT= , 12.42 , 3 , END
11.5 2.0 , F , Z , F , F , TypeError , end

أسئلة للتمرين:

المستوى الأول — أساسي
1.
(المجموعة) ماذا يطبع البرنامج؟
a = {1, 2, 3}
b = {3, 4}
c = a.union(b)
a.add(3)
a.discard(1)
b.update([9, 9])
print(len(a), len(b), len(c))
print(2 in a, 1 in a)
print(sorted(c))
الإجابات: 2 3 4 , True False , [1, 2, 3, 4]
2.
(القاموس) ماذا يطبع البرنامج؟
d = {"A": 1, "B": 2}
e = d
f = d.copy()
d["C"] = 3
e["A"] = 10
print(len(d), len(e), len(f))
print(f["A"], d["A"])
print(d.pop("B"), d.popitem())
print(d)
الإجابات: 3 3 2 , 1 10 , 2 ('C', 3) , {'A': 10}
3.
(الوراثة) ماذا يطبع البرنامج؟ لماذا يطبع B().show() السطر B؟
class A:
    def who(self):
        return "A"

    def show(self):
        print(self.who())

class B(A):
    def who(self):
        return "B"

class C(B):
    def show(self):
        print("C", end=" ")
        super().show()

A().show()
B().show()
C().show()
print(issubclass(C, A), isinstance(B(), C))
الإجابات: A , B , C B , True False. العمليّة show موجودة في A، لكن self هناك هو كائن من B، فيعمل who الخاص به.
4.
(الملفات) في الملف a.txt ثلاثة أسطر: one وtwo وthree. ماذا يطبع البرنامج؟
with open("a.txt") as f:
    print(f.read(2))
    print(f.readline().strip())
    print(f.readlines())
    f.seek(0)
    print(f.read().splitlines()[-1])
الإجابات: on , e , ['two\n', 'three\n'] , three
5.
(الاستثناءات) ماذا يطبع البرنامج؟ لماذا لا يصل إلى "2"؟
try:
    for t in ["4", "0", "x", "2"]:
        try:
            print(8 / int(t))
        except ZeroDivisionError:
            print("zero")
        finally:
            print("end", t)
except ValueError:
    print("outer")
print("done")
الإجابات: 2.0 , end 4 , zero , end 0 , end x , outer , done. الاستثناء ValueError لا يطابق المقطع الداخلي، فيعمل finally، ثم يصعد إلى الخارج ويترك الحلقة.
المستوى الثاني — متوسط
6.
(المجموعة) لوحتان، ولكل منهما سطر فيه أرقام الأطراف المستعملة. نكتب برنامجا يطبع عدد الأطراف المختلفة كلها، والأطراف المشتركة بين اللوحتين، باستعمال union وin.
الإجابات:
a = set()
for t in input("Board A: ").split():
    a.add(int(t))
b = set()
for t in input("Board B: ").split():
    b.add(int(t))
shared = set()
for pin in a:
    if pin in b:
        shared.add(pin)
print("All:", len(a.union(b)))
print("Shared:", sorted(shared))
للمُدخَلين 2 3 4 9 و4 5 9 يطبع: All: 5 , Shared: [4, 9]
7.
(القاموس) نكتب برنامجا يقرأ قيمة مع بادئة، مثل 4.7k أو 47u، ويطبعها بلا بادئة. البادئات في قاموس: k وM وm وu.
الإجابات:
PREFIX = {"k": 1000, "M": 1000000,
          "m": 0.001, "u": 0.000001}
text = input("Value: ")
if text[-1] in PREFIX:
    print(float(text[:-1]) * PREFIX[text[-1]])
else:
    print(float(text))
للمُدخَلات 4.7k و2M و220 يطبع: 4700.0 , 2000000.0 , 220.0
8.
(التحميل الزائد) نكتب الفئة Res، بحيث تعني a + b التوصيل على التوالي، وa | b (العمليّة __or__) التوصيل على التوازي:
R = R1 · R2R1 + R2
ونضيف __eq__ و__str__.
الإجابات:
class Res:
    def __init__(self, ohms):
        self.ohms = ohms

    def __add__(self, other):
        return Res(self.ohms + other.ohms)

    def __or__(self, other):
        p = self.ohms * other.ohms
        return Res(p / (self.ohms + other.ohms))

    def __eq__(self, other):
        return self.ohms == other.ohms

    def __str__(self):
        return str(self.ohms) + " ohm"

a = Res(100)
b = Res(100)
c = Res(50)
print(a + b)
print(a | b)
print((a | b) == c)
print((a | b) + c)
يطبع: 200 ohm , 50.0 ohm , True , 100.0 ohm
9.
(الملفات) في الملف temps.txt درجة حرارة في كل سطر. نكتب برنامجا يقرأ حدا، وينسخ إلى hot.txt كل درجة أعلى منه، باستعمال with وprint، ثم يطبع عددها.
الإجابات:
limit = float(input("Limit: "))
count = 0
with open("temps.txt") as src, open("hot.txt", "w") as dst:
    for line in src:
        if float(line) > limit:
            print(line.strip(), file=dst)
            count += 1
print(count, "values")
with open("hot.txt") as f:
    print(f.read().splitlines())
للملف 31.5 44.0 39.9 41.2 وللحد 40 يطبع: 2 values , ['44.0', '41.2']
10.
(الاستثناءات) نكتب العمليّة read_int(prompt, low, high)، وهي تطلب عددا صحيحا حتى يصل عدد ضمن الحدين، وتطبع رسالة مناسبة لكل نوع من الفشل.
الإجابات:
def read_int(prompt, low, high):
    while True:
        try:
            value = int(input(prompt))
            if low <= value <= high:
                return value
            print("Out of range")
        except ValueError:
            print("Not a number")

print("Duty:", read_int("Duty: ", 0, 100), "%")
للمُدخَلات x و170 و42 يطبع: Not a number , Out of range , Duty: 42 %. الأمر return يخرج من الحلقة ومن العمليّة معا.
المستوى الثالث — تحدٍّ
11.
(القاموس) العكس من مثال الألوان: نكتب برنامجا يقرأ مقاومة، مثل 4700، ويطبع ألوانها الثلاثة. نبني من القاموس قاموسا معكوسا، مفتاحه الرقم وقيمته اللون.
الإجابات:
CODES = {"black": 0, "brown": 1, "red": 2,
         "orange": 3, "yellow": 4, "green": 5,
         "blue": 6, "violet": 7, "grey": 8,
         "white": 9}
names = {}
for color, digit in CODES.items():
    names[digit] = color
ohms = input("Ohms: ")
power = len(ohms) - 2
print(names[int(ohms[0])], names[int(ohms[1])],
      names[power])
للمُدخَل 4700 يطبع: yellow violet red ، وللمُدخَل 47 يطبع: yellow violet black
12.
(الوراثة) نكتب فئة مجرّدة Source، فيها عمليّة مجرّدة power. نشتقّ منها Battery (الجهد ضرب التيار) وSolar (المساحة ضرب الإشعاع ضرب الكفاءة 0.2). نطبع القدرة الكلية لقائمة مصادر، ونبيّن أن Source لا يُبنى منها كائن.
الإجابات:
from abc import ABC, abstractmethod

class Source(ABC):
    def __init__(self, x, y):
        self.x = x
        self.y = y

    @abstractmethod
    def power(self):
        pass

class Battery(Source):
    def power(self):
        return self.x * self.y

class Solar(Source):
    def power(self):
        return self.x * self.y * 0.2

total = 0
for s in [Battery(12, 2.5), Solar(0.5, 800)]:
    total += s.power()
print("Total:", total, "W")
try:
    Source(1, 1)
except TypeError:
    print("Source is abstract")
يطبع: Total: 110.0 W , Source is abstract
13.
(الملفات) في الملف cfg.txt إعدادات من الشكل key=value. نقرؤها إلى قاموس، نغيّر fan إلى 45، نضيف name=roof، ثم نحفظ الملف من جديد.
الإجابات:
cfg = {}
with open("cfg.txt") as f:
    for line in f.read().splitlines():
        key, value = line.split("=")
        cfg[key] = value
print(cfg)
cfg["fan"] = "45"
cfg["name"] = "roof"
with open("cfg.txt", "w") as f:
    for key in cfg:
        print(key + "=" + cfg[key], file=f)
with open("cfg.txt") as f:
    print(f.read(), end="")
للملف cutoff=11.8 fan=40 يطبع: {'cutoff': '11.8', 'fan': '40'} , cutoff=11.8 , fan=45 , name=roof. الوضع "w" يمحو الملف، لكننا حفظنا كل شيء في القاموس.
14.
(الاستثناءات) في الملف ohm.txt سطور من الشكل V,R، أي جهد ومقاومة. نكتب برنامجا يطبع التيار لكل سطر سليم، ويعدّ كل نوع استثناء في قاموس، بواسطة as وget.
الإجابات:
errors = {}
good = 0
with open("ohm.txt") as f:
    for line in f:
        try:
            v, r = line.split(",")
            print(float(v) / float(r), "A")
            good += 1
        except (ValueError, ZeroDivisionError) as e:
            kind = type(e).__name__
            errors[kind] = errors.get(kind, 0) + 1
print("Good:", good)
print(errors)
للملف 12,4 5,0 abc,3 9 6,2 يطبع: 3.0 A , 3.0 A , Good: 2 , {'ZeroDivisionError': 1, 'ValueError': 2}