أنواع المعطيات المركبة
الفئة Sensor التي كتبناها في الفصل 8 تصف حسّاسا واحدا، وكل صفاتها أعداد وجمل نصية. لكن الأشياء الحقيقية مبنية من أشياء أخرى: الجهاز فيه بطارية، والدائرة لها مركز، ولوحة الدائرة الكهربائية تحمل ثنائيات باعثة للضوء، والمحطة تجمع حسّاسات كثيرة. في هذا الفصل نبني فئة من فئات أخرى: نجعل كائنا صفة في كائن، نجمع كائنات في قائمة، ونرسم ذلك كله في مخطّط قبل أن نكتب الشيفرة. وفي آخر الفصل تصبح المحطة نفسها كائنا يحمل قائمة حسّاساتها.
- نعرّف النوع المركّب، ونميّز بين ما يكون صفة بسيطة وما يستحق فئة خاصة به.
- ننشئ كائنات صفاتها كائنات، ونصل إلى ما بداخلها بسلسلة من النقاط.
- نعرف متى تشير صفتان إلى الكائن نفسه، ونمنع ذلك أو نستفيد منه.
- نقرأ مخطّط UML ونرسمه، وننتقل منه إلى الشيفرة ومن الشيفرة إليه.
- نعمل على قائمة من الكائنات، حتى لو كانت من أنواع مختلفة.
- نكتب فئة تحمل قائمة من الكائنات، وتخدمها بعمليّات الإضافة والبحث والعدّ.
9.1 تعريف النوع المركّب – הגדרה של טיפוס מורכב
نكتب فئة تصف جهازا يعمل على بطارية، ونريد أن نعرف كم ساعة يعمل. أول ما يخطر لنا هو أن نضع كل شيء في فئة واحدة:
class Device:
def __init__(self, name, watts, volts, amp_hours):
self.name = name
self.watts = watts
self.volts = volts
self.amp_hours = amp_hoursلكن volts وamp_hours لا يصفان الجهاز، بل يصفان بطاريته. وإذا ركّبنا في الجهاز بطارية أخرى، فعلينا أن نغيّر صفتين معا. وإذا احتجنا إلى طاقة البطارية في فئة أخرى، كررنا الحساب هناك. البطارية شيء قائم بذاته، فنعطيها فئة خاصة بها، ونجعل الجهاز يحوي بطارية، وهذا هو الاحتواء (הכלה):
class Battery:
def __init__(self, volts, amp_hours):
self.volts = volts
self.amp_hours = amp_hours
def energy(self):
return self.volts * self.amp_hours
class Device:
def __init__(self, name, watts, battery):
self.name = name
self.watts = watts
self.battery = battery
def hours(self):
return round(self.battery.energy() / self.watts, 1)
pack = Battery(12, 7)
fan = Device("Fan", 6, pack)
print(fan.name, fan.hours())
print(fan.battery.volts)Fan 14.0 12
الصفة battery تتلقى كائنا كاملا، لا عددا. والتعبير self.battery.energy() يعمل على مرحلتين: self.battery هو الكائن Battery، وenergy() تُستدعى عليه. الجهاز لا يعرف كيف تُحسب الطاقة، بل يطلبها من البطارية، وكل فئة تتولى ما يخصها.
battery لا تحوي البطارية نفسها، بل مرجعا إليهاb = a في الفصل 6 يعطي القائمة نفسها اسما آخر.نستبدل الآن بطارية المروحة. نغيّر صفة واحدة فتتغيّر مدة العمل، والبطارية القديمة تبقى كما هي:
fan.battery = Battery(24, 20) print(fan.hours(), pack.volts)
المُخرَج: 80.0 12
ومتى نكتب فئة جديدة؟ نسأل: هل هو قيمة واحدة تصف الكائن، أم شيء له أجزاء وعمل خاص به؟ فقدرة الجهاز عدد من النوع float، أما بطاريته فلها جهد وسعة وتحسب طاقتها، ولذلك هي من النوع Battery.
"Fan"، كما رأينا في البند 2.3. الجديد هنا أن الفئة الداخلية من كتابتنا نحن.Device("Fan", 6, 12) فوضعنا عددا مكان البطارية، فلن يعترض أحد عند إنشاء الكائن. الخطأ AttributeError يظهر لاحقا عند استدعاء hours()، بعيدا عن سببه. لذلك نبحث في مثل هذا الخطأ عن المكان الذي أنشأنا فيه الكائن، لا عن السطر الذي توقف عنده البرنامج.radio = Device("Radio", 4, Battery(6, 4))
print(radio.hours())
print(radio.battery.energy())
radio.battery.volts = 12
print(radio.hours())(أ) مدى القياس
(ب) شاشة لها عدد خانات وتعرض قيمة
(ج) اسم الشركة
(د) مجسّان، لكل منهما لون وطول ومقاومة
9.2 الكائنات كقيم للصفات: إنشاء كائنات من نوع مركّب واستعمالها – עצמים כערכי תכונות: יצירת ושימוש בעצמים מטיפוס מורכב
الدائرة لها مركز ونصف قطر. نبدأ بالجزء، نقطة في المستوى: لها عمليّة تحرّكها، وعمليّة تحسب بُعدها عن نقطة أخرى حسب فيثاغورس، والوسيط other فيها هو نفسه كائن من الفئة Point. أما في الفئة Circle فالصفة center كائن من الفئة Point، والصفة radius عدد من النوع float:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def move(self, dx, dy):
self.x += dx
self.y += dy
def distance(self, other):
dx = self.x - other.x
dy = self.y - other.y
return (dx ** 2 + dy ** 2) ** 0.5
def __str__(self):
return "(" + str(self.x) + ", " + str(self.y) + ")"class Circle:
def __init__(self, center, radius):
self.center = center
self.radius = radius
def move_to(self, x, y):
self.center.x = x
self.center.y = y
def contains(self, point):
return self.center.distance(point) <= self.radius
def __str__(self):
c = str(self.center)
return "center " + c + ", radius " + str(self.radius)c = Circle(Point(3, 4), 2.5) print(c) print(c.center) print(c.center.x) print(c.center.distance(Point(0, 0)))
المُخرَج: center (3, 4), radius 2.5 , (3, 4) , 3 , 5.0
نقرأ التعبير c.center.x من اليسار إلى اليمين، وكل نقطة تعني: ندخل إلى ما قبلها. والفئة Circle لا تعرف كيف تُكتب النقطة، فتستدعي str(self.center) وتتولى Point ذلك. وكذلك contains تسأل النقطة عن البُعد ولا تحسبه بنفسها.
c = Circle(Point(3, 4), 2)c.radiusc.center.ystr(c.center)c.contains(Point(3, 5))2 2 4 3 (3, 4) 4 True تغيير المركز:
نبدأ من c = Circle(Point(3, 4), 2.5)، وننفذ الأوامر التالية بالتتابع، ونطبع الدائرة بعد كل أمر:
| الأمر | المُخرَج | ما يحدث |
|---|---|---|
c.center.x = 10 | center (10, 4), radius 2.5 | يغيّر صفة داخل كائن النقطة |
c.center.move(1, -2) | center (11, 2), radius 2.5 | يطلب من النقطة أن تتحرك |
c.move_to(0, 0) | center (0, 0), radius 2.5 | يطلب من الدائرة، وهي تغيّر نقطتها. وهذه أوضح طريقة لمن يستعمل الفئة |
c.center = Point(7, 7) | center (7, 7), radius 2.5 | يضع في الصفة مرجعا إلى كائن جديد، والنقطة القديمة لا تتغيّر |
(0, 0) يصل إرسالها إلى 40 m، فمنطقة تغطيتها دائرة. نفحص هل تصل إلى عقدة قياس، ثم ننقل الوحدة.gateway = Circle(Point(0, 0), 40) node = Point(30, 30) print(gateway.contains(node)) gateway.move_to(10, 10) print(gateway.contains(node))
المُخرَج: False , True
لم نكتب حساب البُعد في الفئة Circle، بل استعملنا ما كتبناه في Point.
صفتان تشيران إلى الكائن نفسه:
self.center = center تحفظ المرجع فقط، فتصبح للدائرتين نقطة واحدة، وإذا حرّكنا مركز إحداهما تحرّك مركز الأخرى:p = Point(0, 0) a = Circle(p, 5) b = Circle(p, 8) a.center.x = 3 print(a.center, b.center, p) print(a.center is b.center)
المُخرَج: (3, 0) (3, 0) (3, 0) , True
المعامل is يسأل: هل هذان كائن واحد؟ والعلاج أن ننشئ لكل دائرة نقطة خاصة بها، Circle(Point(0, 0), 5)، أو أن تصنع الدائرة نسختها الخاصة في عمليّة البناء، فلا يهم عندها ما يُمرَّر إليها:
class Circle:
def __init__(self, center, radius):
self.center = Point(center.x, center.y)
self.radius = radiusوالأوامر نفسها تعطي الآن: (3, 0) (0, 0) (0, 0) , False
== لا يقارن صفات الكائنين ما دامت الفئة لا تطلب ذلك، بل يسأل عن الكائن نفسه. فالتعبير Point(1, 2) == Point(1, 2) يعطي False. لذلك نقارن الصفات واحدة واحدة.وليست المشاركة خطأ دائما. إنذار يراقب حسّاسا Sensor من الفصل 8، ويجب أن يرى ما يراه الحسّاس. نبني إنذارين على البطارية نفسها، تحذيرا عند 12.2 V وفصلا للحمل عند 11.8 V من الفصل 3. نحدّث الحسّاس مرة واحدة، فيرى الإنذاران القراءة الجديدة معا. فالحسّاس شيء واحد في العالم، أما مركزا دائرتين منفصلتين فشيئان:
class Alarm:
def __init__(self, sensor, limit):
self.sensor = sensor
self.limit = limit
def active(self):
return self.sensor.value() < self.limit
batt = Sensor("Battery", "V", 4)
warn = Alarm(batt, 12.2)
cut = Alarm(batt, 11.8)
batt.read(620)
print(batt, warn.active(), cut.active())
batt.read(590)
print(batt, warn.active(), cut.active())المُخرَج: Battery: 12.12 V True False , Battery: 11.53 V True True
p = Point(1, 1) a = Circle(p, 2) b = Circle(Point(1, 1), 2) p.x = 9 print(a.center.x, b.center.x) print(a.center is p, b.center is p)
9.3 مخطّط UML – תרשים UML
عندما تكثر الفئات، يصعب علينا أن نرى بناء البرنامج من الشيفرة. مخطّط الفئات صورة لهذا البناء: أي فئات لدينا، ماذا في كل منها، وكيف ترتبط. نرسمه قبل الكتابة لنفكر، وبعدها لنشرح. ولغة الرسم هذه موحّدة عالميا، واسمها UML، ويفهمها كل مبرمج مهما كانت لغته.
Pointيُكتب كل سطر بطريقة ثابتة. النوع يأتي بعد نقطتين، ولا نكتب self أبدا، لأنه موجود في كل عمليّة:
| في المخطّط | في الشيفرة | معناه |
|---|---|---|
+ x : float | self.x = x | صفة من النوع float |
+ move(dx: float, dy: float) | def move(self, dx, dy): | عمليّة لها وسيطان |
+ value() : float | def value(self): | عمليّة تعيد عددا |
+ step : float | step = 5 / 1023 | خط تحت السطر: متغيّر فئة |
والإشارة في أول السطر هي محدِّد الوصول (הרשאת גישה). + تعني عامّ (ציבורי): يصل إليه كل من يملك الكائن. و- تعني خاصّ (פרטי): للاستعمال داخل الفئة فقط. في بايثون لا يوجد منع حقيقي، بل اتفاق بين المبرمجين: الاسم الذي يبدأ بخط سفلي _ نعدّه خاصا، فلا نلمسه من الخارج.
class Fuse:
def __init__(self, amps):
self.amps = amps
self._blown = False
def carry(self, current):
if current > self.amps:
self._blown = True
def is_blown(self):
return self._blown
f = Fuse(2)
f.carry(3)
f.carry(0.5)
print(f.is_blown())True
لو كتب أحد f._blown = False لعمل الأمر، لكن الخط السفلي والإشارة - يقولان له إنه يخالف تصميم الفئة. وفي بقية الأمثلة نترك الصفات عامة، حتى تبقى قصيرة.
الخطوط بين الفئات:
الخط بين مستطيلين يعني أن كائنا من الفئة الأولى يعرف كائنا من الثانية، أي أن فيه صفة تشير إليه. ونوع الخط يقول كيف يعرفه، والأرقام على طرفيه هي التعدّدية (ריבוי): الرقم المكتوب قرب فئة يقول كم كائنا منها يقابل كائنا واحدا من الفئة الأخرى.
| في المخطّط | الاسم | معناه | في الشيفرة |
|---|---|---|---|
| خط وسهم مفتوح | الارتباط (שיוך) | يعرفه ويستعمله، وذلك الكائن قائم بذاته وقد يخدم غيره | الكائن يُمرَّر من الخارج، ويبقى عند من أنشأه |
| خط ومعيّن ممتلئ عند الكلّ | التركيب (הרכבה) | هو جزء منه: لا يخدم كائنا آخر | الكلّ يُنشئ الجزء بنفسه، أو ينسخه، أو يستقبل كائنا جديدا لا يحمله غيره |
| الرمز | معناه | مثال |
|---|---|---|
1 | واحد بالضبط | لكل دائرة مركز واحد |
0..1 | واحد أو لا شيء | جهاز قد لا تكون فيه بطارية |
* | أي عدد، حتى صفر | لوحة تبدأ فارغة ثم تحمل ثنائيات |
1..* | واحد على الأقل | سلسلة فيها مقاومة واحدة على الأقل |
Device وBattery من البند 9.1.نأخذ من كل فئة صفاتها وعمليّاتها. البطارية تُمرَّر من الخارج ويمكن نقلها إلى جهاز آخر، فنرسم ارتباطا. السهم يقول إن الجهاز يعرف البطارية، والبطارية لا تعرف الجهاز:
Thermostat يشغّل مروحة Fan. المعيّن يقول إن المروحة جزء منه، ولذلك يُنشئها بنفسه في عمليّة البناء.class Fan:
def __init__(self):
self.speed = 0
def set_speed(self, speed):
self.speed = speed
class Thermostat:
def __init__(self, target):
self.target = target
self.fan = Fan()
def update(self, temp):
if temp > self.target:
self.fan.set_speed(1)
else:
self.fan.set_speed(0)
t = Thermostat(30)
for temp in [28, 32]:
t.update(temp)
print(temp, t.fan.speed)28 0 32 1
أعطانا المخطّط الأسماء والأنواع والبناء، ولم يقل شيئا عن عمل update: القواعد تأتي من المسألة.
* قرب LED، لأنها تقول كم منها في اللوحة الواحدة. ونرسم أحيانا سهما من الجزء إلى الكلّ وليس في الشيفرة صفة كهذه: النقطة لا تعرف أي دائرة تستعملها.(أ) كم مفتاحا يمكن أن يكون في اللوحة؟
(ب) هل يستطيع المفتاح أن يعرف لوحته؟
(ج) من يُنشئ كائنات
Switch حسب المعيّن؟(د) ما نوع عناصر
switches؟9.4 قائمة من الكائنات – רשימה של עצמים
القائمة من الفصل 6 تحفظ أي قيمة، والكائن قيمة. وكل ما تعلمناه عن المؤشّر والحلقة for يبقى كما هو، والجديد هو النقطة بعد العنصر: parts[1] يعطي كائنا، وparts[1].ohms يعطي صفة فيه. وفي أغلب الأحيان تأتي المعطيات من الخارج، فنبني الكائنات في حلقة، صفًّا ثابتا بعد صف:
class Resistor:
def __init__(self, name, ohms, watts):
self.name = name
self.ohms = ohms
self.watts = watts
def power(self, amps):
return round(amps ** 2 * self.ohms, 3)
def __str__(self):
return self.name + ": " + str(self.ohms) + " ohm"
data = [("R1", 220, 0.25),
("R2", 1000, 0.25),
("R3", 47, 0.5)]
parts = []
for name, ohms, watts in data:
parts.append(Resistor(name, ohms, watts))
print(len(parts))
print(parts[1], parts[1].ohms)
for r in parts:
print(r)3 R2: 1000 ohm 1000 R1: 220 ohm R2: 1000 ohm R3: 47 ohm
amps = 0.02
count = 0
for r in parts:
if r.power(amps) > r.watts:
print(r.name, "overloaded:", r.power(amps), "W")
count += 1
print("Overloaded:", count)
big = parts[0]
for r in parts:
if r.ohms > big.ohms:
big = r
print("Largest:", big)
big.watts = 1
print(parts[1].watts)المُخرَج: R2 overloaded: 0.4 W , Overloaded: 1 , Largest: R2: 1000 ohm , 1
هذه خوارزميات الفصل 6 نفسها. الفرق أن big يحفظ مرجعا إلى الكائن كله، فنطبعه بعد الحلقة أو نغيّره: غيّرنا عبر big، فظهر التغيير في parts[1].
طباعة القائمة: __str__ أم __repr__؟:
__str__، فصار print(leds[0]) يعمل كما نحب. ثم طبعنا القائمة كلها، فخرجت عناوين بدل الأوصاف:class Led:
def __init__(self, pin):
self.pin = pin
def __str__(self):
return "LED on pin " + str(self.pin)
leds = [Led(13), Led(12)]
print(leds[0])
print(leds)LED on pin 13 [<__main__.Led object at 0x000001F2A4C8>, <__main__.Led object at 0x000001F2A4C8>]
العنوان يختلف من كائن إلى كائن ومن تشغيل إلى تشغيل، وعلى حاسوبنا سيظهر بأرقام أخرى. وقد ثبّتناه في الكتاب كي لا يتغيّر المُخرَج، ولذلك يُظهر الكائنان هنا العدد نفسه. والسبب أن __str__ هو الوصف للقارئ، وهو ما يطلبه print وstr. أما القائمة فتعرض عناصرها بالوصف التقني __repr__، وإذا لم نعرّفه حصلنا على الوصف الافتراضي الذي فيه العنوان. نعرّفه في الفئة:
def __repr__(self):
return "repr" + str(self.pin)وهذه الحالات الثلاث بتشغيل حقيقي، مع led = Led(13). وإذا لم نعرّف __str__ استعمل print الوصف __repr__ بدلا منه، والعكس لا يصح:
| ما عرّفناه في الفئة | print(led) | print([led]) |
|---|---|---|
__str__ | str13 | [<__main__.Led object at 0x000001F2A4C8>] |
__repr__ | repr13 | [repr13] |
| كلاهما | str13 | [repr13] |
أنواع مختلفة في قائمة واحدة:
في القائمة نفسها يمكن أن نضع كائنات من فئات مختلفة. لكل فئة __str__ خاص بها. الأمر في الحلقة واحد، والنتيجة حسب نوع الكائن:
class Resistor:
def __init__(self, name, ohms):
self.name = name
self.ohms = ohms
def __str__(self):
return self.name + ": " + str(self.ohms) + " ohm"
class Capacitor:
def __init__(self, name, uf):
self.name = name
self.uf = uf
def __str__(self):
return self.name + ": " + str(self.uf) + " uF"
parts = [Resistor("R1", 220), Capacitor("C1", 100)]
for part in parts:
print(part)R1: 220 ohm C1: 100 uF
هذا هو التحميل الزائد (העמסה) كما يفيدنا هنا: عمليّة باسم واحد، ولكل فئة كتابتها، والمُفسّر يختار حسب الكائن الذي بين يديه، وفي الفصل 11 نسمّي هذا باسمه الدقيق: تعدّد الأشكال. وقد رأينا الفكرة نفسها في الإشارة +، التي تجمع الأعداد وتصل الجمل النصية. وبدون ذلك يجب أن تعرف الحلقة كل الأنواع، وكل نوع جديد يعني تعديلها:
for part in parts:
if type(part) == Resistor:
print(part.name + ": " + str(part.ohms) + " ohm")
elif type(part) == Capacitor:
print(part.name + ": " + str(part.uf) + " uF")[Led(0)] * 3 لا ينشئ ثلاثة كائنات، بل ثلاثة مراجع إلى كائن واحد. والقصّ good[:] ينشئ قائمة جديدة، لكن عناصرها هي الكائنات نفسها: حذف عنصر من النسخة لا يمس الأصل، أما تغيير كائن عبر النسخة فيظهر في الأصل:class Led:
def __init__(self, pin):
self.pin = pin
self.lit = False
def __repr__(self):
return str(self.pin) + ":" + str(self.lit)
bad = [Led(0)] * 3
bad[0].lit = True
print(bad)
good = []
for pin in range(3):
good.append(Led(pin))
spare = good[:]
spare[1].lit = True
spare.pop()
print(good)
print(len(good), len(spare))[0:True, 0:True, 0:True] [0:False, 1:True, 2:False] 3 2
class Cell:
def __init__(self, volts):
self.volts = volts
cells = [Cell(3.7), Cell(3.6), Cell(3.9)]
low = cells[0]
for c in cells:
if c.volts < low.volts:
low = c
low.volts = 4.2
print(cells[1].volts)
print(cells[0].volts + cells[2].volts)9.5 قائمة من الكائنات كصفة في فئة – רשימה של עצמים כתכונה במחלקה
حتى الآن كانت القائمة متغيّرا في البرنامج، وكانت الحلقات منثورة حوله. الخطوة التالية أن نضعها داخل كائن يملكها، ويقدم عمليّات تعمل عليها. مثالنا سلسلة مقاومات موصولة على التوالي:
Chainclass Resistor:
def __init__(self, name, ohms):
self.name = name
self.ohms = ohms
def __repr__(self):
return self.name + "=" + str(self.ohms)class Chain:
def __init__(self, volts):
self.volts = volts
self.parts = []
def add(self, resistor):
self.parts.append(resistor)
def total(self):
t = 0
for r in self.parts:
t += r.ohms
return t
def current(self):
return self.volts / self.total()
def drops(self):
i = self.current()
for r in self.parts:
print(r.name, round(i * r.ohms, 2), "V")c = Chain(12)
c.add(Resistor("R1", 100))
c.add(Resistor("R2", 200))
c.add(Resistor("R3", 300))
print(c.parts)
print(c.total(), "ohm", c.current(), "A")
c.drops()
print(c.parts[1].ohms)المُخرَج:[R1=100, R2=200, R3=300]600 ohm 0.02 AR1 2.0 VR2 4.0 VR3 6.0 V200
تُهيَّأ القائمة فارغة في عمليّة البناء، وتملؤها add، وتمر عليها total وdrops. والمستعمل يتعامل مع السلسلة، ولا يحتاج إلى أن يعرف كيف تُحفظ مقاوماتها. والسطر الأخير يبيّن مسار الوصول كله: كائن، ثم قائمة، ثم عنصر، ثم صفة.
c.parts[1].ohms__init__، بالصيغة self.parts = []. إذا كتبناها في جسم الفئة كانت متغيّر فئة، كما تعلمنا في الفصل 8، فتشترك فيها كل الكائنات:class Chain:
parts = []
def add(self, ohms):
self.parts.append(ohms)
a = Chain()
b = Chain()
a.add(100)
print(b.parts)[100]
self.parts وليس parts وحدها، وإلا توقف البرنامج لأن هذا الاسم غير معروف. ومن الخارج لا نمر على الكائن نفسه: for r in c: يوقف البرنامج، لأن القائمة في الصفة وليست هي الكائن.Board تحمل ثنائيات باعثة للضوء LED. لا يجوز أن يكون ثنائيان على الطرف نفسه، ولذلك تفحص add ذلك قبل الإضافة، وتعيد True أو False.class LED:
def __init__(self, pin, color):
self.pin = pin
self.color = color
self.on = False
def toggle(self):
self.on = not self.on
def __repr__(self):
return self.color + ":" + str(self.on)class Board:
def __init__(self, name):
self.name = name
self.leds = []
def led_at(self, pin):
for led in self.leds:
if led.pin == pin:
return led
return None
def add(self, led):
if self.led_at(led.pin) is None:
self.leds.append(led)
return True
return False
def __str__(self):
return self.name + " " + str(self.leds)uno = Board("Uno")
print(uno.add(LED(13, "red")))
print(uno.add(LED(13, "blue")))
uno.add(LED(12, "green"))
uno.led_at(13).toggle()
print(uno)
print(uno.led_at(9))المُخرَج:TrueFalseUno [red:True, green:False]None
العمليّة add تعتمد على led_at ولا تعيد كتابة البحث. وled_at(13) تعيد الكائن نفسه الذي في القائمة، ولذلك أضاء الثنائي الموجود في اللوحة. وعندما لا تجده تعيد None، وإذا قرأنا منها صفة بلا فحص توقف البرنامج بالخطأ AttributeError، فنفحص أولا بالصيغة led is None. والتعبير str(self.leds) يعرض القائمة بالوصف __repr__، وهذا سبب تعريفنا له في LED.
Resistor وChain كما في بداية البند. ماذا يطبع البرنامج التالي؟c = Chain(6)
c.add(Resistor("A", 10))
c.add(Resistor("B", 20))
d = Chain(6)
d.add(c.parts[0])
d.parts[0].ohms = 30
print(c.total(), d.total())
print(len(c.parts), len(d.parts))في الفصل 8 صار لكل حسّاس فئة، لكن الحسّاسات بقيت متغيّرات منفصلة في البرنامج. نجمعها الآن في كائن واحد: محطة لها اسم وقائمة حسّاسات، تضيف إليها، تبحث فيها، تعدّ الإنذارات، وتطبع تقريرا. نبدأ بالمخطّط:
الفئة Sensor هي نفسها التي كتبناها في الفصل 8، ونضعها في الملف قبل الفئة Station:
class Station:
cutoff = 11.8
def __init__(self, name):
self.name = name
self.sensors = []
def add(self, sensor):
self.sensors.append(sensor)
def find(self, name):
for s in self.sensors:
if s.name == name:
return s
return None
def alarm_count(self):
count = 0
for s in self.sensors:
if s.unit == "V" and s.value() < Station.cutoff:
count += 1
return count
def report(self):
print("Station", self.name)
for s in self.sensors:
print(" ", s)
print("Alarms:", self.alarm_count())st = Station("Roof")
st.add(Sensor("Battery", "V", 4))
st.add(Sensor("Backup", "V", 4))
st.add(Sensor("Temp", "C", 100))
st.find("Battery").read(620)
st.find("Backup").read(585)
st.find("Temp").read(65)
st.report()
print(st.find("Fan"))المُخرَج:Station Roof Battery: 12.12 V Backup: 11.44 V Temp: 31.77 CAlarms: 1None
الحسّاسات مختلفة، و__str__ في الفئة يجعل التقرير يطبع كلا منها بأمر واحد. وحد فصل الحمل 11.8 V من الفصل 3 قيمة واحدة لكل المحطات، فهو متغيّر فئة. ونعدّ الإنذار لحسّاسات الجهد فقط، لأن حرارة منخفضة ليست بطارية فارغة. والأمر st.find("Battery").read(620) يعمل لأن find تعيد الكائن نفسه الذي في القائمة، والسطر الأخير يبيّن ما يعود عندما لا يوجد الحسّاس.
نجرّب بأنفسنا: نضيف إلى Station عمليّة active_count تعيد عدد الحسّاسات التي قرأت شيئا، أي أن raw لها غير صفر. وفي الفصل 10 ستصل القراءات عبر المنفذ التسلسلي من أردوينو بدل أن نكتبها في البرنامج.
أهمّ أفكار الفصل:
- النوع المركّب فئة إحدى صفاتها كائن من فئة أخرى. والكلّ يطلب من الجزء، ولا يعيد كتابة حسابه.
- الصفة تحفظ مرجعا لا نسخة. مرجعان إلى كائن واحد يعنيان أن كل تغيير يظهر في الاثنين: خطأ مرة، وتصميم مقصود مرة.
isيسأل هل هما كائن واحد، و==في فئاتنا يسأل السؤال نفسه ما لم نعرّف غير ذلك.- مخطّط UML ثلاث خانات، وإشارة وصول، وخطوط عليها تعدّدية، ولا يُكتب فيه
self. print(lst)يطبع القائمة بواسطة__repr__، لا بواسطة__str__.- كتابة
__str__لكل فئة تجعل الأمر الواحد يعمل على أنواع مختلفة بلا فحص. - القائمة التي هي صفة تُهيَّأ في
__init__، لا في جسم الفئة. - البحث يعيد الكائن نفسه أو
None، ونفحص قبل أن نستعمل النتيجة.
ملخّص أوامر الفصل:
| الأمر | عمله |
|---|---|
| self.part = part | صفة تحفظ مرجعا إلى كائن من فئة أخرى |
| self.part = Part(x, y) | الكائن يُنشئ جزءه بنفسه: تركيب |
| a.b.c a.b.f() | الوصول عبر كائن داخلي |
| a is b x is None | هل هما كائن واحد، وهل لا يوجد كائن |
| items = [Part(1), Part(2)] | قائمة من الكائنات |
| for item in items: | المرور على الكائنات |
| items[i].x | صفة في عنصر من القائمة |
| self.items = [] | قائمة كصفة، تُهيَّأ في عمليّة البناء |
| def __repr__(self): | الوصف الذي يظهر عند طباعة القائمة |
| self._x | صفة خاصة، باتفاق بين المبرمجين |
قاموس المصطلحات – מילון מונחים:
| بالعربية | بالعبرية (كما تظهر في الامتحان) | English |
|---|---|---|
| النوع المركّب | טיפוס מורכב | Compound type |
| الاحتواء | הכלה | Containment |
| المرجع | הפניה | Reference |
| مخطّط UML | תרשים UML | UML diagram |
| مخطّط الفئة | תרשים מחלקה | Class diagram |
| محدِّد الوصول | הרשאת גישה | Access modifier |
| عامّ | ציבורי | Public |
| خاصّ | פרטי | Private |
| الارتباط | שיוך | Association |
| التركيب | הרכבה | Composition |
| التجميع | קיבוץ | Aggregation |
| التعدّدية | ריבוי | Multiplicity |
| التحميل الزائد | העמסה | Overloading |
6.0 , 24 , 12.09 1 , True FalseSwitch صفة تشير إلى اللوحة. (ج) اللوحة نفسها. (د) كائنات من الفئة Switch.4.2 , 7.650 30 , 2 1أسئلة للتمرين:
Point وCircle كما في البند 9.2. ماذا يطبع البرنامج؟c = Circle(Point(1, 2), 3) c.center.move(2, 2) print(c) print(c.center.x + c.radius) c.center = Point(0, 0) print(c.contains(Point(3, 0)))
center (3, 4), radius 3 , 6 , True__str__ في الطرف يعيد مثلا pin 2 INPUT. ننشئ زرا ونغيّر نمط طرفه عبر الزر.class Pin:
def __init__(self, number, mode):
self.number = number
self.mode = mode
def __str__(self):
return "pin " + str(self.number) + " " + self.mode
class Button:
def __init__(self, label, pin):
self.label = label
self.pin = pin
def __str__(self):
return self.label + " on " + str(self.pin)
b = Button("Start", Pin(2, "INPUT"))
b.pin.mode = "OUTPUT"
print(b)Start on pin 2 OUTPUTclass Motor:
def __init__(self, volts):
self.volts = volts
class Robot:
def __init__(self, name):
self.name = name
self.motors = []
def add(self, motor):
self.motors.append(motor)
def count(self):
return len(self.motors)class Relay:
def __init__(self, pin):
self.pin = pin
def __str__(self):
return "Relay on " + str(self.pin)
relays = [Relay(4), Relay(5)]
print(relays)__repr__، وهو غير معرّف. نغيّر اسم العمليّة، فيعمل عندها print(relays[0]) أيضا:class Relay:
def __init__(self, pin):
self.pin = pin
def __repr__(self):
return "Relay on " + str(self.pin)
relays = [Relay(4), Relay(5)]
print(relays)
print(relays[0])[Relay on 4, Relay on 5] , Relay on 4class Cell:
def __init__(self, volts):
self.volts = volts
class Pack:
def __init__(self, cell):
self.cell = cell
c = Cell(3.7)
p1 = Pack(c)
p2 = Pack(c)
p3 = Pack(Cell(3.7))
p1.cell.volts = 3.2
print(p2.cell.volts, p3.cell.volts, c.volts)3.2 3.7 3.2. الكائنان p1 وp2 يشيران إلى الخلية نفسها c، أما p3 فله خلية خاصة به.Wire التي تحمل نقطتين من الفئة Point، ولها length() وresistance()، على فرض أن مقاومة السلك 0.02 Ω لكل وحدة طول.class Wire:
ohm_per_m = 0.02
def __init__(self, start, end):
self.start = start
self.end = end
def length(self):
return self.start.distance(self.end)
def resistance(self):
return round(self.length() * Wire.ohm_per_m, 3)
w = Wire(Point(0, 0), Point(6, 8))
print(w.length(), w.resistance())
w.end.move(-6, 0)
print(w.length(), w.resistance())10.0 0.2 , 8.0 0.16Bank لمقاومات موصولة على التوازي، لها قائمة مقاومات وعمليّة total حسب القانون:class Resistor:
def __init__(self, ohms):
self.ohms = ohms
class Bank:
def __init__(self):
self.parts = []
def add(self, r):
self.parts.append(r)
def total(self):
s = 0
for r in self.parts:
s += 1 / r.ohms
return round(1 / s, 2)
b = Bank()
b.add(Resistor(100))
b.add(Resistor(100))
print(b.total())
b.add(Resistor(50))
print(b.total())50.0 , 25.0Board من البند 9.5 عمليّة remove(pin) تحذف الثنائي الموجود على الطرف، وتعيد True أو False. نستعمل led_at ولا نكتب البحث من جديد.def remove(self, pin):
led = self.led_at(pin)
if led is None:
return False
self.leds.remove(led)
return TrueFalse , True , Uno [green:False]class Lamp:
def __init__(self, volts, amps):
self.volts = volts
self.amps = amps
def watts(self):
return self.volts * self.amps
class Heater:
def __init__(self, volts, ohms):
self.volts = volts
self.ohms = ohms
def watts(self):
return self.volts ** 2 / self.ohms
loads = [Lamp(12, 0.5), Heater(12, 24), Lamp(12, 1)]
total = 0
for load in loads:
total += load.watts()
print(total, "W")24.0 W. لكل فئة عمليّة watts خاصة بها، كما فعلنا مع __str__.Station عمليّة sort_by_value() ترتّب الحسّاسات تصاعديا حسب القيمة بالفرز الفقاعي من الفصل 6. عند التبديل، ماذا ينتقل فعلا في القائمة؟def sort_by_value(self):
n = len(self.sensors)
for i in range(n - 1):
for j in range(n - 1 - i):
a = self.sensors[j]
b = self.sensors[j + 1]
if a.value() > b.value():
self.sensors[j] = b
self.sensors[j + 1] = aBattery وBackup، يصبح أول عنصر بعد الفرز: Backup: 11.44 VNetwork التي تحمل قائمة من الفئة Station، ولها total_alarms(). نرسم مخطّط الفئات الثلاث.class Network:
def __init__(self):
self.stations = []
def add(self, station):
self.stations.append(station)
def total_alarms(self):
total = 0
for st in self.stations:
total += st.alarm_count()
return totalYard، يطبع: 2center=Point(0, 0). ماذا يطبع البرنامج، ولماذا؟ نصلحه.class Circle:
def __init__(self, radius, center=Point(0, 0)):
self.center = center
self.radius = radius
a = Circle(5)
b = Circle(8)
a.center.move(3, 4)
print(b.center)(3, 4). القيمة الافتراضية تُنشأ مرة واحدة، عند تعريف العمليّة، فتتشارك الدائرتان نقطة واحدة. نجعلها None، وننشئ النقطة داخل العمليّة:class Circle:
def __init__(self, radius, center=None):
if center is None:
center = Point(0, 0)
self.center = center
self.radius = radius
a = Circle(5)
b = Circle(8)
a.center.move(3, 4)
print(a.center, b.center)(3, 4) (0, 0)Circle عمليّة copy() تعيد دائرة جديدة مستقلة تماما عن الأصلية. نثبت بالتشغيل أن تحريك النسخة لا يحرّك الأصل.def copy(self):
twin = Point(self.center.x, self.center.y)
return Circle(twin, self.radius)a = Circle(Point(1, 1), 4) b = a.copy() b.center.move(5, 5) print(a.center, b.center) print(a is b, a.center is b.center)
(1, 1) (6, 6) , False False. لو كتبنا Circle(self.center, self.radius) مع عمليّة البناء الأصلية، التي تحفظ المرجع فقط، لحصلنا على دائرة جديدة لها المركز القديم نفسه.