مواضيع اختيارية
هذا الفصل اختياري: مواضيعه موصى بها في المنهاج، لكنها ليست إلزامية، ولا تُحسب ساعاتها في مجموع ساعات المنهاج، ولا تأتي منها أسئلة في امتحان البجروت. ومع ذلك فهي الخطوة الطبيعية بعد الفصول السابقة: كل برنامج حقيقي يحتاج إلى أن يبحث بسرعة، وأن يحفظ ما عرفه في ملف، وأن يبقى واقفا عندما يصله نص فاسد. نأخذ هنا أربعة مواضيع، ونجمعها في النهاية في مرحلة جديدة من محطة القياس.
- نستعمل المجموعة
setعندما يهمنا هل العنصر موجود، لا مكانه. - نستعمل القاموس
dictلنصل إلى قيمة بواسطة مفتاح. - نبني فئة على فئة بالوراثة، ونكتب
superو__add__والفئات المجرّدة. - نقرأ من ملفات نصية ونكتب إليها، ونتحرك داخلها بواسطة
seekوtell. - نميّز بين الخطأ النحوي والاستثناء، ونعالج الاستثناء بواسطة
tryوexceptوfinally. - نكتب برنامجا يقرأ سجل المحطة، يتجاوز السطور الفاسدة، ويحفظ ملخّصا في ملف.
| الموضوع | البنود | الساعات المقترحة في المنهاج |
|---|---|---|
| بنى معطيات إضافية (מבני נתונים נוספים) | 11.1 و11.2 | 10 |
| وراثة الفئات (ירושה של מחלקות) | 11.3 | 15 |
| العمل مع الملفات (עבודה עם קבצים) | 11.4 | 10 |
| معالجة الاستثناءات (טיפול בחריגים) | 11.5 | 10 |
try وimport. لذلك نشغّل برامج هذا الفصل في بايثون على الحاسوب.11.1 بنية المعطيات: المجموعة – מבנה נתונים מטיפוס Set (set)
في القائمة والصفّ الثابت من الفصل 6 ترتيب ومؤشّرات. لكننا أحيانا لا نحتاج إلى الترتيب ولا إلى التكرار. على لوحة أردوينو مثلا نريد أن نعرف: هل الطرف 7 مشغول؟ لا يهمنا متى حجزناه، ولا معنى لأن يُحجز مرتين. هذا هو عمل المجموعة.
التهيئة والمجموعة الفارغة والطباعة – אתחול, מבנה ריק, 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 تضيف عنصرا واحدا وتغيّر المجموعة نفسها. وإذا كان العنصر موجودا فلا يحدث شيء، ولا تظهر رسالة خطأ.
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_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 Falseupdate فتغيّر المجموعة نفسها ولا تعيد شيئا. إذا وضعنا نتيجتها في متغيّر حصلنا على 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'len({1, 1, 2})3 in {1, 2}len(set("ABBA"))sorted({3, 1, 2})len(set())type({}).__name__2 2 False 3 2 4 [1, 2, 3] 5 0 6 dict 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.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'>- بين المفتاح وقيمته نقطتان، وبين الأزواج فاصلة. والقاموس الفارغ هو
{}أوdict(). - المفتاح لا يتكرر، ويجب ألا يكون قابلا للتغيير. أما القيمة فمن أي نوع، وقد تتكرر.
- يحافظ القاموس على ترتيب الإضافة، لكن
d[0]يبحث عن المفتاح0، ولا يعني الزوج الأول.
in، وهو يبحث في المفاتيح فقط لا في القيم، كما يظهر في السطر الثاني:station = {"BAT": 12.42, "TMP": 31.5}
print("TMP" in station)
print(12.42 in station)
print(station["HUM"])True False KeyError: 'HUM'
إضافة أزواج – הוספת איברים:
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)| الأمر | part | kind | counts | kind 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}d["b"]len(d)"a" in d1 in dlist(d)list(d.values())2 2 2 3 True 4 False 5 ['a', 'b'] 6 [1, 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، ولنبنِ عليها.
هرمية واضحة – היררכיה ברורה וחד-משמעית:
الفئات التي ترث بعضها من بعض تكوّن هرمية (היררכיה) نرسمها كشجرة، والسهم يخرج من الفئة الوارثة إلى الفئة التي ترث منها:
الهرمية الجيدة تجيب بوضوح عن سؤال واحد: هل كل كائن من هذه الفئة هو كائن من تلك؟ فنقرأ الجملة بصوت عال، فإذا بدت غريبة فالوراثة خاطئة.
| الجملة | الجواب | ما نكتبه |
|---|---|---|
| المقاومة قطعة إلكترونية | صواب | وراثة: 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 ولا تتعلق بالتردد، أما في المكثّف والملف الكهربائي فمقدارها هو المفاعلة (היגב):
نكتب لكل فئة صيغتها من 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 | صفات الكائن وفئته | ما يخص كائنا واحدا |
عمليّة الفئة (פעולת מחלקה) @classmethod | cls | متغيّرات الفئة | طريقة أخرى لبناء كائن |
عمليّة ساكنة (פעולה סטטית) @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
powerتحتاج إلى مقاومة معيّنة، فهي عمليّة نسخة.from_codeطريقة ثانية لبناء كائن، هنا من رمز المقاومة السطحية من البند 5.3. وإذا استدعيناها من فئة مشتقّة كانclsهو الفئة المشتقّة، فتبني ما طلبناه بالضبط.parallelحساب فقط، ومكانه في الفئة تنظيمي. ونستدعيها باسم الفئة أو من كائن.
التحميل الزائد للعمليّات – העמסת מתודות:
في لغات مثل 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
- تعيد
__add__كائنا جديدا، ولذلك نستطيع أن نكمل:a + b + c. - بعد
__eq__أصبحت مقاومتان بالقيمة نفسها متساويتين رغم أنهما كائنان مختلفان، كما يبيّنa is c. وبايثون تبني!=منها. - بعد
__lt__يعملsortعلى قائمة مقاومات من دون أن نقول له كيف.
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'
والربح الحقيقي يظهر في الفئة المشتقّة التي نسيت أن تكتب العمليّة: بناء كائن منها يعطي الخطأ نفسه، مع اسم العمليّة الناقصة، ولا ينتظر الخطأ حتى أول استدعاء في وسط التشغيل.
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:
العمليّة 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
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:.
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 closedZeroDivisionError.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 / 0 | ZeroDivisionError | قسمة على صفر |
[1, 2][5] | IndexError | مؤشّر خارج القائمة |
{"a": 1}["b"] | KeyError | مفتاح غير موجود |
"V=" + 5 | TypeError | نوعان لا يعملان معا |
volts | NameError | اسم لم يُعرَّف |
"AB".size | AttributeError | صفة أو عمليّة غير موجودة |
open("no.txt") | FileNotFoundError | ملف غير موجود |
المقطع try – בלוק try:
نضع في مقطع try الأسطر التي قد تفشل. فإذا ظهر استثناء تتوقف بقية المقطع، وينتقل التنفيذ إلى مقطع except المناسب. وإذا لم يظهر استثناء يُتجاوز مقطع except كله.
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.
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 , 4 | Current: 3.0 A |
12 , x | Need a number |
x | Need 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.
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")كانت المحطة تنسى كل شيء عند إطفائها. نكتب الآن برنامجا يقرأ سجلا من القرص، فيه سطور من الشكل الذي رأيناه في الفصل 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 من ملف بدل أن يكون مكتوبا في البرنامج.
أهمّ أفكار الفصل:
- المجموعة بلا تكرار وبلا ترتيب، وسؤالها الوحيد هو
in. والمجموعة الفارغةset()لا{}. - القاموس يصل إلى القيمة بالمفتاح، و
inيبحث في المفاتيح، وd[k] = vتضيف أو تغيّر. - الوراثة علاقة «هو»: الفئة المشتقّة تأخذ ما في الأساس، وتعيد تعريف ما يختلف، و
super()تصل إلى الأصل. - تعدّد الأشكال يعني أن الحلقة لا تسأل عن النوع، وكل كائن ينفّذ صيغته من العمليّة.
- لا تحميل زائد بعدد الوسائط في بايثون: التعريف الأخير يغطي ما قبله، والبديل هو القيمة الافتراضية.
- الوضع
"w"يمحو الملف، والوضع"a"يضيف إليه. والأمرwithيغلق الملف عنا، حتى عند الخطأ. - كل قراءة تتقدم بمؤشّر الملف، و
seekتعيده. ما تعطيهtellنعيده إلىseekكما هو. - الخطأ النحوي يمنع التشغيل كله، أما الاستثناء فيظهر في أثناء التشغيل، ونستطيع أن نعالجه.
- نحدد نوع الاستثناء الذي نتوقعه، ونكتب الخاص قبل العام، و
finallyيعمل دائما.
ملخّص أوامر الفصل:
| الأمر | عمله |
|---|---|
| {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 |
|---|---|---|
| المجموعة | Set | Set |
| القاموس | מילון | 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 |
2 3 , False True , [2, 5] [1, 2, 3]3 2 , 9 3 , ('L', 2) , False11 21 25 , True FalseBAT= , 12.42 , 3 , END2.0 , F , Z , F , F , TypeError , endأسئلة للتمرين:
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]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}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 الخاص به.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"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، ثم يصعد إلى الخارج ويترك الحلقة.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]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.0Res، بحيث تعني a + b التوصيل على التوالي، وa | b (العمليّة __or__) التوصيل على التوازي:__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 ohmtemps.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']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 يخرج من الحلقة ومن العمليّة معا.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 blackSource، فيها عمليّة مجرّدة 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 abstractcfg.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" يمحو الملف، لكننا حفظنا كل شيء في القاموس.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}