البرمجة بلغة بايثون · الفصل 8 — تمثيل نوع جديد بواسطة فئةייצוג טיפוס חדש על ידי מחלקה · יסודות התכנות בשפת Python⌂📚🏠
الفصل 8

تمثيل نوع جديد بواسطة فئة

ייצוג טיפוס חדש על ידי מחלקה
· 20 ساعات تعليمية

في الفصل 1 أعطينا أوامر لكائنات من الفئة الجاهزة Turtle، وفي البند 2.3 عرفنا أن كل قيمة في بايثون كائن من فئة ما. حتى الآن كنا نستعمل فئات كتبها غيرنا. في هذا الفصل نكتب فئات بأنفسنا: نقرر ما البيانات التي يحملها النوع الجديد، وما الأعمال التي يعرف أن يقوم بها، فيصبح نوعا نستعمله كما نستعمل str وlist. وفي آخر الفصل تحصل محطة القياس على فئة للحسّاس.

في هذا الفصل:
  • نشرح ما هي الفئة وما هو الكائن، ونصمّم فئة تمثّل نوعا جديدا.
  • نكتب رأس الفئة، ونميّز بين متغيّر الفئة ومتغيّر النسخة.
  • نعرّف صفات الكائن في عمليّة البناء، مع وسائط اختيارية.
  • نفهم دور self، ونكتب عمليّات تقرأ الصفات وتغيّرها، بعضها بقيم افتراضية.
  • نقرر كيف يُطبع الكائن، بواسطة __str__ و__repr__.

8.1 تعريف مفهوم الفئة – הגדרת המושג מחלקה

الفئة (מחלקה): وصف عام لنوع من الأشياء: ما البيانات التي يحملها كل شيء من هذا النوع، وما الأعمال التي يعرف أن يقوم بها. وهي تشبه مخطط التصنيع: المخطط لا يحمل تيارا، لكننا نصنع حسبه ما نشاء من القطع.
الكائن (עצם): قطعة محددة صُنعت حسب الفئة، ولها قيمها الخاصة. ونسميه أيضا نسخة (מופע) من الفئة، والكلمتان تدلان على الشيء نفسه.

كل نوع عرفناه حتى الآن هو فئة جاهزة. الدالة type تقول لنا إلى أي فئة تنتمي القيمة، والنقطة بعد القيمة تصل إلى عمليّة كتبها من صنع الفئة:

print(type(12))
print(type("LED"))
print(type([1, 2]))
print("LED".lower())
print([3, 1, 2].count(1))
المُخرَج
<class 'int'>
<class 'str'>
<class 'list'>
led
1

ولكي نرى أن الفئة الواحدة تعطي كائنات مستقلة، نعود إلى Turtle من الفصل 1، وننشئ كائنين منها:

import turtle
a = turtle.Turtle()
b = turtle.Turtle()
a.color("red")
b.color("blue")
a.forward(100)
b.left(90)
b.forward(60)
print(a.position(), b.position())
print(a.heading(), b.heading())
print(type(a))
المُخرَج
(100.00,0.00) (0.00,60.00)
0.0 90.0
<class 'turtle.Turtle'>
5010050
كائنان من الفئة نفسها: كل كائن يتذكر موقعه واتجاهه ولونه

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

لماذا نكتب فئات خاصة بنا؟:

نفترض أننا نريد أن نصف مقاومات في برنامج. بما تعلمناه حتى الآن نكتب:

r1_name = "R1"
r1_ohms = 220
r2_name = "R2"
r2_ohms = 1000
print(r1_name, 12 / r1_ohms)
print(r2_name, 12 / r2_ohms)
المُخرَج
R1 0.05454545454545454
R2 0.012

البرنامج يعمل، لكنه لا يكبر جيدا. مع عشر مقاومات نحتاج إلى عشرات المتغيّرات، ولا شيء في البرنامج يقول إن r1_name وr1_ohms يخصان القطعة نفسها، أو إن المتغيّر r2_ohms هو مقاومة. والحساب يتكرر كلما احتجنا إليه. الفئة تحل المشكلتين معا: تجمع بيانات القطعة في كائن واحد، وتضع إلى جانبها العمليّات التي تعمل عليها. وهكذا يصبح «مقاومة» نوعا جديدا في البرنامج، كما أن str نوع.

خطأ شائع: نخلط بين الفئة والكائن، فنقول مثلا إن قيمة الفئة «مقاومة» هي 220 Ω. الفئة لا قيمة لها من هذا النوع: هي تقول إن لكل مقاومة قيمة، أما 220 Ω فهي قيمة كائن واحد.
ملاحظة: في النص العربي الرسمي للامتحان تُسمى الفئة قسم، ويُسمى الكائن جسم. أما عمليّة البناء فتُسمى هناك كما نسميها هنا: عمليّة بناء. ونبقى في هذا الكتاب مع المصطلحين فئة وكائن، والمعنى واحد.
سؤال 8.1: نحدد في كل زوج أيهما الفئة وأيهما الكائن:
(أ) "LED" وstr
(ب) Turtle وa
(ج) «ثنائي باعث للضوء» و«الثنائي الأحمر الذي على لوحة التجارب».

8.2 تطوير فئة تمثّل نوعا – פיתוח מחלקה המייצגת טיפוס

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

  1. نحدد ما الذي نريد أن نمثّله: مقاومة في دائرة.
  2. نحدد الصفات: اسم القطعة name، قيمتها بالأوم ohms، وأقصى قدرة تتحملها بالواط watts.
  3. نحدد العمليّات: التيار عند جهد معيّن current، القدرة عند جهد معيّن power، وهل هذا الجهد آمن is_safe.
  4. نكتب الفئة.
  5. ننشئ كائنات، ونفحص النتائج على مثال نعرف جوابه.
I = V R
P = V2 R

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

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

    def current(self, volts):
        return volts / self.ohms

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

    def is_safe(self, volts):
        p = self.power(volts)
        return p <= self.watts


r1 = Resistor("R1", 400, 0.25)
r2 = Resistor("R2", 1000, 0.25)
print(r1.name, r1.ohms)
print(r2.name, r2.ohms)
print(r2.current(12))
print(r1.power(12), r1.is_safe(12))
print(r2.power(12), r2.is_safe(12))
print(type(r1))
المُخرَج
R1 400
R2 1000
0.012
0.36 False
0.144 True
<class '__main__.Resistor'>
الفئة: وصف عامclass Resistornameohmswattscurrent(volts)power(volts)is_safe(volts)كائنان: لكل واحد قيمهr1name = "R1"ohms = 400watts = 0.25r2name = "R2"ohms = 1000watts = 0.25إنشاءResistor("R1", 400, 0.25)Resistor("R2", 1000, 0.25)
فئة واحدة وكائنان: الفئة تحدد أي صفات توجد وأي عمليّات، والكائن يحمل القيم

الفحص هو الخطوة الأخيرة في التطوير: عند الجهد 12 V والمقاومة 1000 Ω نتوقع تيارا مقداره 0.012 A، وهذا ما طبعه السطر الثالث. والقطعة r1 لا تتحمل هذا الجهد، لأن قدرتها ستتجاوز 0.25 W. ويقول السطر الأخير من المُخرَج إن Resistor هو نوع جديد الآن، و__main__ يعني أنه مُعرَّف في برنامجنا نفسه.

خطأ شائع: تعريف الفئة وحده لا يفعل شيئا: لا ينشئ مقاومة ولا يحسب تيارا. البرنامج يعمل عندما ننشئ كائنا ونستدعي عمليّاته. والبرنامج يُنفَّذ من الأعلى إلى الأسفل، ولذلك نكتب الفئة قبل السطر الذي يستعملها أول مرة.
سؤال 8.2: ماذا يطبع البرنامج التالي؟
class Heater:
    def __init__(self, volts, ohms):
        self.volts = volts
        self.ohms = ohms

    def amps(self):
        return self.volts / self.ohms

h1 = Heater(230, 46)
h2 = Heater(12, 3)
print(h1.amps())
print(h2.amps())
print(h1.ohms + h2.ohms)

8.3 رأس الفئة، والفرق بين متغيّر الفئة ومتغيّر النسخة – כותרת המחלקה, ההבדל בין משתנה במחלקה (class variable) לבין משתנה במופע (instance variable)

رأس الفئة (כותרת המחלקה) هو السطر الأول في تعريفها، وفيه:

جسم الفئة لا يمكن أن يكون فارغا. عندما لا يكون لدينا ما نكتبه بعد، نضع الأمر pass، وهو لا يفعل شيئا. وحتى مثل هذه الفئة تنشئ كائنات، وكل استدعاء يعطي كائنا جديدا. المعامل is يسأل: هل الاسمان يدلان على الكائن نفسه؟

class Fuse:
    pass

f1 = Fuse()
f2 = Fuse()
print(type(f1))
print(f1 is f2)
المُخرَج
<class '__main__.Fuse'>
False
خطأ شائع: من دون النقطتين الرأسيتين في آخر الرأس لا يبدأ البرنامج أصلا، ونحصل على الخطأ النحوي SyntaxError.

نوعان من المتغيّرات:

متغيّر الفئة (משתנה במחלקה): متغيّر نكتبه في جسم الفئة، خارج كل عمليّة. له نسخة واحدة فقط، مشتركة بين كل الكائنات.
متغيّر النسخة (משתנה במופע): متغيّر نكتبه بالشكل self.x داخل عمليّة. لكل كائن نسخة خاصة به.

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

R = Vsupply − VLED Imax
class Led:
    max_current = 0.02

    def __init__(self, color, drop):
        self.color = color
        self.drop = drop

    def resistor(self, supply):
        v = supply - self.drop
        return round(v / Led.max_current)

red = Led("red", 2.0)
blue = Led("blue", 3.2)
print(red.color, blue.color)
print(red.max_current, blue.max_current)
print(Led.max_current)
print(red.resistor(5), blue.resistor(5))
Led.max_current = 0.01
print(red.max_current, blue.max_current)
print(red.resistor(5), blue.resistor(5))
المُخرَج
red blue
0.02 0.02
0.02
150 90
0.01 0.01
300 180
متغيّر الفئة: نسخة واحدةclass Ledmax_current = 0.02متغيّرات النسخة: لكل كائنredcolor = "red"drop = 2.0bluecolor = "blue"drop = 3.2red.max_currentblue.max_current
كل كائن يقرأ max_current من الفئة، لأنه لا يملك متغيّرا بهذا الاسم

عندما نكتب red.max_current تبحث بايثون أولا في الكائن، ولا تجد المتغيّر، فتقرؤه من الفئة. ولذلك عندما غيّرنا Led.max_current تغيّرت النتيجة عند الكائنين معا.

متغيّر الفئةمتغيّر النسخة
أين يُكتبفي جسم الفئة، خارج العمليّاتداخل عمليّة، مع self.
كم نسخة منهواحدةواحدة لكل كائن
كيف نصل إليهLed.max_current أو red.max_currentred.color فقط
كيف نغيّرهباسم الفئة: Led.max_current = 0.01باسم الكائن: red.color = "pink"
متى نستعملهلقيمة تخص النوع كلهلقيمة تخص القطعة نفسها

والعكس لا يعمل: متغيّر النسخة لا يُقرأ عبر الفئة، فالفئة لا تعرف أي كائن نقصد، وLed.color يوقف البرنامج بالخطأ AttributeError.

مثال 8.1
نعدّ كم مرحّلا أنشأنا في البرنامج. هذا العدد لا يخص مرحّلا واحدا، بل يخص النوع كله، ولذلك نضعه في متغيّر فئة، ونزيده في عمليّة البناء.
الحل:
class Relay:
    count = 0

    def __init__(self, name):
        self.name = name
        Relay.count = Relay.count + 1

k1 = Relay("K1")
print(Relay.count)
k2 = Relay("K2")
k3 = Relay("K3")
print(Relay.count, k1.count)
المُخرَج
1
3 3
خطأ شائع: الإسناد عبر الكائن لا يغيّر متغيّر الفئة، بل ينشئ متغيّر نسخة جديدا بالاسم نفسه عند هذا الكائن فقط، فيغطي عليه:
class Led:
    max_current = 0.02

red = Led()
blue = Led()
red.max_current = 0.03
print(red.max_current)
print(blue.max_current)
print(Led.max_current)
المُخرَج
0.03
0.02
0.02

وبسبب هذا فإن العدّ بالشكل self.count = self.count + 1 لا يعدّ شيئا: كل كائن يقرأ الصفر من الفئة، ثم يخزّن الواحد عند نفسه، ويبقى متغيّر الفئة صفرا. لذلك نغيّر متغيّر الفئة دائما باسم الفئة.

سؤال 8.3: ماذا يطبع البرنامج التالي؟
class Socket:
    volts = 230

    def __init__(self, room):
        self.room = room

a = Socket("lab")
b = Socket("office")
Socket.volts = 120
b.volts = 12
b.room = "store"
print(a.volts, a.room)
print(b.volts, b.room)
print(Socket.volts)

8.4 صفات الفئة – תכונות המחלקה

الصفة (תכונה): متغيّر يعيش داخل الكائن، ويصف حالته. يولد عندما نسند إليه قيمة أول مرة، ويبقى ما دام الكائن موجودا.

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

class Supply:
    def __init__(self, volts, limit):
        self.volts = volts
        self.limit = limit
        self.on = False

bench = Supply(12, 1.5)
small = Supply(5, 0.5)
print(bench.volts, bench.limit, bench.on)
bench.volts = 9
bench.on = True
print(bench.volts, bench.on)
print(small.volts, small.on)
المُخرَج
12 1.5 False
9 True
5 False
خطأ شائع: قراءة صفة لا وجود لها توقف البرنامج بالخطأ AttributeError. أما الكتابة بخطأ إملائي فأخطر، لأنها لا تعطي أي رسالة. الأمر bench.volt = 9 أنشأ صفة جديدة اسمها volt، وبقيت volts كما كانت:
class Supply:
    def __init__(self, volts):
        self.volts = volts

bench = Supply(12)
bench.volt = 9
print(bench.volts)
print(bench.volt)
المُخرَج
12
9

اسمان لكائن واحد:

المتغيّر لا يحمل الكائن نفسه، بل يدل عليه. لذلك فإن الإسناد same = bench لا ينسخ الكائن، بل يعطيه اسما ثانيا، وكل تغيير عبر أحد الاسمين يظهر عبر الآخر. أما other = Supply(12) فأنشأ كائنا آخر، حتى لو كانت قيمه متساوية. والمعامل == بين كائنين من فئتنا يسأل السؤال نفسه الذي يسأله is، ما دمنا لم نعلّمه شيئا آخر، ولذلك أعطى False:

class Supply:
    def __init__(self, volts):
        self.volts = volts

bench = Supply(12)
same = bench
other = Supply(12)
same.volts = 5
print(bench.volts, same.volts, other.volts)
print(bench is same, bench is other)
print(bench == other)
المُخرَج
5 5 12
True False
False

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

ملاحظة: لا شيء يمنعنا من أن نكتب bench.volts = -50 من خارج الفئة، فيصبح لدينا مزوّد بجهد لا معنى له. في البند 8.7 نكتب عمليّة تحرس الصفة من مثل هذه القيم.
سؤال 8.4: ماذا يطبع البرنامج التالي؟
class Lamp:
    def __init__(self, watts):
        self.watts = watts
        self.hours = 0

desk = Lamp(9)
door = desk
door.hours = 5
door = Lamp(60)
door.hours = door.hours + 2
print(desk.watts, desk.hours)
print(door.watts, door.hours)

8.5 عمليّات البناء والوسائط الاختيارية – פעולות בונות (__init__), פרמטרים אופציונאליים בפעולה בונה

عمليّة البناء (פעולה בונה): العمليّة __init__ التي تعمل تلقائيا مرة واحدة عند إنشاء كل كائن، ووظيفتها أن تعطي صفاته قيمها الأولى.

عندما نكتب bench = Supply(12, 1.5) تحدث ثلاثة أشياء بالترتيب:

  1. تنشئ بايثون كائنا جديدا فارغا.
  2. تستدعي __init__، وتمرر إليها هذا الكائن في self، والقيم الأخرى في الوسائط التالية.
  3. تعيد الكائن، فيوضع في المتغيّر bench.

لذلك لا نستدعي __init__ باسمها، ولا نكتب فيها return لقيمة: الكائن يعود من تلقاء نفسه. وفي اسمها خطان سفليان قبل الكلمة وخطان بعدها.

الوسائط الاختيارية:

كما في العمليّات العادية في الفصل 7، نعطي الوسيط قيمة افتراضية (ערך ברירת מחדל) بالإشارة =، فيصبح اختياريا عند إنشاء الكائن. معظم بطاريات الاحتياط عندنا 12 V وسعتها 7 Ah، ولذلك نجعل هاتين القيمتين افتراضيتين، ونكتب ما يختلف فقط:

class Battery:
    def __init__(self, name, capacity=7.0,
                 volts=12):
        self.name = name
        self.capacity = capacity
        self.volts = volts

main = Battery("Main")
back = Battery("Back", 2.2)
cell = Battery("Cell", 2.2, 3.7)
coin = Battery("Coin", volts=3)
print(main.name, main.capacity, main.volts)
print(back.name, back.capacity, back.volts)
print(cell.name, cell.capacity, cell.volts)
print(coin.name, coin.capacity, coin.volts)
المُخرَج
Main 7.0 12
Back 2.2 12
Cell 2.2 3.7
Coin 7.0 3
خطأ شائع: الوسائط الإجبارية تأتي أولا، وإلا لا تعرف بايثون أي قيمة لأي وسيط. فالسطر def __init__(self, capacity=7.0, name): خطأ نحوي. وإذا لم نمرر قيمة لوسيط إجباري توقف البرنامج بالخطأ TypeError.
خطأ شائع: خط سفلي واحد بدل خطين في _init_ يجعلها عمليّة عادية لا تعمل عند الإنشاء، فترفض بايثون القيم التي نمررها. ونسيان self. يجعل الإسناد متغيّرا محليا يضيع عند نهاية العمليّة، فلا تنشأ الصفة:
class Battery:
    def __init__(self, name):
        name = name

main = Battery("Main")
print(main.name)
المُخرَج
AttributeError: 'Battery' object has no attribute 'name'
مثال 8.2
عمليّة البناء هي المكان الطبيعي لفحص القيم، فمنها يبدأ كل كائن. نمثّل منظّم إضاءة لمصباح قدرته 60 W. مستوى الإضاءة بين 0 و100 بالمئة، وأي قيمة أخرى تعني أنه مطفأ. والقدرة صفة نحسبها من المستوى، لا نأخذها من الخارج.
الحل:
class Dimmer:
    def __init__(self, level=0):
        if level < 0 or level > 100:
            level = 0
        self.level = level
        self.watts = 60 * level / 100

value = int(input("Level: "))
hall = Dimmer(value)
print(hall.level, hall.watts)
wall = Dimmer(250)
print(wall.level, wall.watts)
print(Dimmer().level)
المُخرَج
Level: 40
40 24.0
0 0.0
0

الوسيط level قيمته في السطر الأخير هي الافتراضية. وهذا الفحص هو ما يفعله السؤال عن المستطيل في البجروت في آخر هذا الفصل.

خطأ شائع: القيمة الافتراضية تُحسب مرة واحدة، عند تعريف العمليّة. لذلك إذا كانت قائمة، اشتركت فيها كل الكائنات:
class Probe:
    def __init__(self, name, data=[]):
        self.name = name
        self.data = data

p1 = Probe("P1")
p2 = Probe("P2")
p1.data.append(5)
print(p2.data)
print(p1.data is p2.data)
المُخرَج
[5]
True

والحل هو أن ننشئ القائمة داخل عمليّة البناء، بالسطر self.data = []، فيأخذ كل كائن قائمته.

سؤال 8.5: ماذا يطبع البرنامج التالي؟
class Motor:
    def __init__(self, volts=24, rpm=0):
        if rpm < 0:
            rpm = 0
        self.volts = volts
        self.rpm = rpm

m1 = Motor()
m2 = Motor(12)
m3 = Motor(rpm=-300)
m4 = Motor(48, 900)
print(m1.volts, m1.rpm)
print(m2.volts, m2.rpm)
print(m3.volts, m3.rpm)
print(m4.volts, m4.rpm)

8.6 المعامل self – אופרטור self

العمليّة مكتوبة في الفئة مرة واحدة، لكنها تعمل على كائنات كثيرة. فكيف تعرف على أي كائن تعمل؟ الجواب هو الوسيط الأول self: عندما نكتب a.drain(2)، تمرر بايثون الكائن الذي قبل النقطة إلى self. فالأمران a.drain(2) وCell.drain(a, 2) متساويان تماما، وقد استعملنا الشكل الثاني أيضا في البرنامج التالي:

class Cell:
    def __init__(self, name, charge):
        self.name = name
        self.charge = charge

    def drain(self, amount):
        self.charge = self.charge - amount
        print(self.name, self.charge)

a = Cell("A", 10)
b = Cell("B", 30)
a.drain(2)
b.drain(5)
Cell.drain(a, 1)
print(a.charge, b.charge)
المُخرَج
A 8
B 25
A 7
7 25
a.drain(2)def drain(self, amount):self.charge = self.charge- amountaname = "A"charge = 10 → 8تغيّرbname = "B"charge = 30لم يتغيّرselfb.drain(5)def drain(self, amount):self.charge = self.charge- amountaname = "A"charge = 8لم يتغيّرbname = "B"charge = 30 → 25تغيّرself
العمليّة واحدة، وself يشير في كل استدعاء إلى الكائن الذي قبل النقطة

قواعد self:

والاسم self.charge يختلف عن charge تماما، حتى لو كتبناهما في السطر نفسه. self.charge هو صفة تبقى في الكائن، وcharge هو وسيط يختفي عند نهاية العمليّة، ومثله المتغيّر المحلي before:

class Cell:
    def __init__(self, charge):
        self.charge = charge

    def add(self, charge):
        before = self.charge
        self.charge = self.charge + charge
        return self.charge - before

a = Cell(10)
print(a.add(4))
print(a.charge)
المُخرَج
4
14
خطأ شائع: ننسى self في تعريف العمليّة. بايثون تمرره رغم ذلك، فيصل إلى العمليّة وسيط لم تكن تنتظره. وتشير الرسالة إلى صفر وسائط ووسيط واحد، مع أننا لم نمرر شيئا:
class Cell:
    def status():
        return "ready"

a = Cell()
print(a.status())
المُخرَج
TypeError: Cell.status() takes 0 positional arguments but 1 was given
خطأ شائع: وإذا نسينا self. داخل العمليّة، بحثت بايثون عن متغيّر عادي بهذا الاسم، وتوقف البرنامج بالخطأ NameError.
سؤال 8.6: ماذا يطبع البرنامج التالي؟ نلاحظ أن pour يستدعي عمليّة على كائنين مختلفين.
class Tank:
    def __init__(self, level):
        self.level = level

    def fill(self, liters):
        self.level = self.level + liters

    def pour(self, other, liters):
        self.fill(-liters)
        other.fill(liters)

x = Tank(50)
y = Tank(10)
x.pour(y, 20)
y.fill(5)
print(x.level, y.level)

8.7 عمليّات الفئة، ومنها الوسائط ذات القيمة الافتراضية – פעולות המחלקה, כולל פרמטרים עם ערך ברירת מחדל

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

مثال 8.3
نمثّل بطارية. نعرف سعتها الكلية full، 10 Ah، ونتابع الشحنة الباقية فيها left. العمليّتان is_low وuse تعملان حتى من دون أن نمرر كل الوسائط: الحد الافتراضي للبطارية الفارغة تقريبا هو 20 بالمئة، والمدة الافتراضية ساعة واحدة.
الحل:
class Battery:
    def __init__(self, name, full):
        self.name = name
        self.full = full
        self.left = full

    def percent(self):
        return 100 * self.left / self.full

    def is_low(self, limit=20):
        return self.percent() < limit

    def use(self, amps, hours=1):
        self.left -= amps * hours
        if self.left < 0:
            self.left = 0

    def recharge(self):
        self.left = self.full

pack = Battery("Pack", 10)
print(pack.percent(), pack.is_low())
pack.use(2)
print(pack.percent(), pack.is_low())
pack.use(1.5, 4.5)
print(pack.left, pack.percent())
print(pack.is_low(), pack.is_low(10))
pack.use(amps=3, hours=9)
print(pack.left)
pack.recharge()
print(pack.left)
المُخرَج
100.0 False
80.0 False
1.25 12.5
True False
0
10

نلاحظ ثلاثة أمور في المُخرَج. use فيها وسيط إجباري واحد ووسيط اختياري واحد، فكتبنا use(2) مرة، وuse(1.5, 4.5) مرة. ثم is_low() بالقيمة الافتراضية مقابل is_low(10) بحد آخر. وفي الاستدعاء use(amps=3, hours=9) استعملنا أسماء الوسائط، والشحنة لم تنزل تحت الصفر بفضل الشرط داخل العمليّة.

عمليّة تحرس الصفة:

في البند 8.4 رأينا أن الكتابة المباشرة في الصفة تقبل كل قيمة. عندما تهمنا صحة الصفة نغيّرها بعمليّة تفحص أولا، وتعيد True أو False لتقول هل نجحت. وهكذا تعمل العمليّة التي تدفع ثمن السفرة في سؤال البجروت في آخر الفصل. أما الحد الأعلى فيخص كل المزوّدات، ولذلك هو متغيّر فئة:

class Supply:
    MAX = 30

    def __init__(self):
        self.volts = 0

    def set_volts(self, volts):
        if volts < 0 or volts > Supply.MAX:
            return False
        self.volts = volts
        return True

bench = Supply()
print(bench.set_volts(12), bench.volts)
print(bench.set_volts(50), bench.volts)
print(bench.set_volts(-3), bench.volts)
المُخرَج
True 12
False 12
False 12

كائن وسيطا، وكائن ناتجا:

الوسيط قد يكون كائنا من الفئة نفسها، والعمليّة قد تعيد كائنا جديدا. فتوصيل مقاومتين يعطي مقاومة جديدة، قيمتها:

Rs = R1 + R2
Rp = R1 · R2 R1 + R2
class Resistor:
    def __init__(self, name, ohms):
        self.name = name
        self.ohms = ohms

    def series(self, other):
        name = self.name + "+" + other.name
        total = self.ohms + other.ohms
        return Resistor(name, total)

    def parallel(self, other):
        name = self.name + "|" + other.name
        both = self.ohms + other.ohms
        total = self.ohms * other.ohms
        return Resistor(name, total / both)

r1 = Resistor("R1", 100)
r2 = Resistor("R2", 300)
r3 = Resistor("R3", 50)
s = r1.series(r2)
print(s.name, s.ohms)
n = r1.parallel(r2).series(r3)
print(n.name, n.ohms)
print(r1.ohms, r2.ohms)
المُخرَج
R1+R2 400
R1|R2+R3 125.0
100 300

العمليّة series تعمل على كائنين: self هو الذي قبل النقطة، وother هو الذي بين القوسين. وبما أن النتيجة كائن، نستطيع أن نكتب نقطة أخرى بعدها مباشرة كما في r1.parallel(r2).series(r3). والمقاومتان الأصليتان لم تتغيّرا، كما يبيّن السطر الأخير.

خطأ شائع: من دون قوسين لا نستدعي العمليّة، بل نذكر اسمها فقط، فالتعبير pack.percent لا يعطي عددا. وصفة وعمليّة بالاسم نفسه تتصادمان: إذا أسندنا إلى self.power وكتبنا أيضا عمليّة power()، غطّت الصفة على العمليّة عند هذا الكائن. لذلك نعطيهما اسمين مختلفين.
خطأ شائع: عمليّة تأمر ولا تكتب return تعيد القيمة None، فلا فائدة من حفظ نتيجتها:
class Cell:
    def __init__(self, charge):
        self.charge = charge

    def drain(self, amount):
        self.charge = self.charge - amount

a = Cell(10)
result = a.drain(4)
print(result)
print(a.charge)
المُخرَج
None
6
سؤال 8.7: ماذا يطبع البرنامج التالي؟
class Timer:
    def __init__(self, limit=60):
        self.limit = limit
        self.time = 0

    def tick(self, seconds=1):
        self.time = self.time + seconds

    def left(self):
        return self.limit - self.time

    def done(self):
        return self.left() <= 0

t = Timer(10)
t.tick()
t.tick(4)
print(t.time, t.left(), t.done())
t.tick(seconds=5)
print(t.time, t.left(), t.done())
print(Timer().limit)

8.8 التحميل الزائد للعمليّة __repr__ أو __str__ – העמסת הפעולה __repr__ או __str__ לצורך החזרת מחרוזת לתיאור העצם

ماذا يحدث عندما نطبع كائنا من فئتنا؟

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

r1 = Resistor("R1", 220)
print(r1)
المُخرَج
<__main__.Resistor object at 0x000001F2A4C8>

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

التحميل الزائد (העמסה): أن نكتب في فئتنا عمليّة تحمل اسما له معنى عند بايثون، مثل __str__، فتعمل نسختنا بدل السلوك الجاهز.

العمليّة __str__ لا تستقبل غير self، وتعيد جملة نصية تصف الكائن. وكل من print(r1) وstr(r1) يستدعيها من تلقاء نفسه:

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

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

r1 = Resistor("R1", 220)
print(r1)
text = str(r1)
print(len(text))
print("Part:", r1)
print("Part: " + str(r1))
المُخرَج
R1 = 220 ohm
12
Part: R1 = 220 ohm
Part: R1 = 220 ohm
خطأ شائع: نكتب print بدل return. الطباعة تحدث، ثم لا يعود شيء، فيتوقف البرنامج:
class Resistor:
    def __init__(self, ohms):
        self.ohms = ohms

    def __str__(self):
        print(self.ohms, "ohm")

r1 = Resistor(220)
print(r1)
المُخرَج
220 ohm
TypeError: __str__ returned non-string (type NoneType)

والقيمة التي تعود يجب أن تكون جملة نصية، حتى لو كانت الصفة نفسها عددا. لذلك يعطي return self.ohms الخطأ نفسه، ونحوّل بواسطة str.

العمليّة __repr__:

__str__ تصف الكائن للقارئ، أما __repr__ فتصفه للمبرمج، وجرت العادة أن تعطي ما يشبه الأمر الذي ينشئ كائنا مثله. والفرق العملي مهم: عندما نطبع قائمة من الكائنات تُستدعى __repr__ لكل عنصر:

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

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

    def __repr__(self):
        text = str(self.ohms)
        return "Part(" + text + ")"

part = Part(220)
print(part)
print(repr(part))
print([part, Part(470)])
المُخرَج
220 ohm
Part(220)
[Part(220), Part(470)]
ما نكتبهما يُستدعى
print(x) ، str(x)__str__، وإذا لم توجد فـ __repr__
repr(x)__repr__
print([x, y])__repr__ لكل عنصر في القائمة

إذا كنا سنكتب عمليّة واحدة فقط، فالأسهل أن تكون __str__. ولكن إذا كتبنا __str__ وحدها ثم طبعنا قائمة، ظهرت العناوين مرة أخرى. وإذا أردنا عمليّة واحدة تخدم الطباعة والقوائم معا، كتبنا __repr__ وحدها.

بناء الجملة النصية بالبادئة f:

بناء الوصف بالوصل وبالدالة str طويل. والبادئة f قبل علامة الاقتباس تتيح لنا أن نكتب داخل الجملة النصية تعبيرا بين قوسين معقوفين، فتوضع قيمته مكانه وتُحوَّل إلى جملة نصية وحدها. سنلتقي هذا الشكل في سؤال البجروت عن المستطيل:

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

    def __str__(self):
        return f"R = {self.ohms} ohm"

r1 = Resistor(220)
print(r1)
print(f"Twice: {r1.ohms * 2}")
المُخرَج
R = 220 ohm
Twice: 440
سؤال 8.8: ماذا يطبع البرنامج التالي؟
class Bulb:
    def __init__(self, watts):
        self.watts = watts
        self.on = False

    def switch(self):
        self.on = not self.on

    def __str__(self):
        text = str(self.watts) + " W: "
        if self.on:
            return text + "ON"
        return text + "OFF"

b = Bulb(40)
print(b)
b.switch()
print(b)
print("Bulb " + str(b))
▶تجربة حية — مختبر الفئاتنكتب فئة، ننشئ كائنات، ونتابع البرنامج خطوة بعد خطوة. يعرف مفسّر المختبر متغيّر الفئة، والقيم الافتراضية، و__str__، و__repr__، والبادئة f. ويختلف عن بايثون في أنه يطبع الكائن الذي لا يملك __str__ باسم فئته فقط، بلا عنوان. ولا يوجد فيه إدخال من المستخدم، ولذلك نكتب القيمة مباشرة بدل input. وتسمى الفئة في صفحة المختبر «صنفا»، وعمليّة البناء «البانِي».
⚠
المختبر غير متاح الآن. المادة في هذا الفصل كاملة من دونه — نواصل القراءة، ونعود إلى التجربة لاحقا أو نفتحها مباشرة من الرابط الذي تحت الإطار.
https://fahed-lab.com/electronics/python_basics#classes
نفتح المختبر في صفحة مستقلة ↗ fahed-lab.com/electronics/python_basics#classes
مشروع الكتاب: محطة القياس · المرحلة 8 — فئة للحسّاس

في الفصل 2 حسبنا جهد البطارية ودرجة الحرارة بسطرين متشابهين، ولكل واحد منهما متغيّراته. الآن نرى ما هو مشترك: كل حسّاس له اسم ووحدة وقراءة خام من المحوّل، وله معامل scale يحوّل الجهد عند منفذ المحوّل إلى القيمة الحقيقية: 4 لمقسّم الجهد الذي على البطارية، و100 لحسّاس الحرارة الذي يعطي 10 mV لكل درجة. أما الخطوة step فتخص المحوّل نفسه، وهو واحد لكل الحسّاسات، ولذلك هي متغيّر فئة.

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):
        volts = self.raw * Sensor.step
        return round(volts * self.scale, 2)

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

battery = Sensor("Battery", "V", 4)
temp = Sensor("Temp", "C", 100)
print(battery)
battery.read(int(input("ADC battery: ")))
temp.read(int(input("ADC temp: ")))
print(battery)
print(temp)
print(battery.value() > 11.5)
المُخرَج
Battery: 0.0 V
ADC battery: 620
ADC temp: 82
Battery: 12.12 V
Temp: 40.08 C
True

القراءة نفسها التي أدخلناها في الفصل 2 تعطي هنا الجهد نفسه، وهذا فحص جيد. قبل أول قراءة يكون raw صفرا، ولذلك طبع السطر الأول صفرا. وإضافة حسّاس جديد تحتاج إلى سطر واحد، من دون أن نكرر أي حساب.

نجرّب بأنفسنا: في لوحة ESP32 المحوّل ذو 12 خانة وجهده 3.3 V. أي سطر واحد نضيفه لكي يعمل كل الحسّاسات بهذا المحوّل؟ وفي الفصل التالي تدخل هذه الحسّاسات إلى فئة المحطة نفسها.

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

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

الأمرعمله
class Led:رأس الفئة، وتحته جسمها بإزاحة
max_current = 0.02في جسم الفئة: متغيّر فئة
def __init__(self, a, b=1):عمليّة البناء، و b وسيط اختياري
self.x = aصفة، أي متغيّر نسخة
led = Led(2.0)إنشاء كائن
led.x Led.max_currentالوصول إلى صفة، وإلى متغيّر فئة
led.f() led.f(3) led.f(n=3)استدعاء عمليّة، قيمة n فيها قد تكون افتراضية
def __str__(self):ما يطبعه print للكائن: تعيد جملة نصية
def __repr__(self):وصف الكائن للمبرمج وداخل القوائم
f"{self.x} V"جملة نصية فيها تعابير بين قوسين معقوفين

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

بالعربيةبالعبرية (كما تظهر في الامتحان)English
الفئةמחלקהClass
الكائنעצםObject
النسخةמופעInstance
الصفةתכונהAttribute
العمليّةפעולהMethod
رأس الفئةכותרת המחלקהClass header
متغيّر الفئةמשתנה במחלקהClass variable
متغيّر النسخةמשתנה במופעInstance variable
عمليّة البناءפעולה בונהConstructor
وسيط اختياريפרמטר אופציונליOptional parameter
قيمة افتراضيةערך ברירת מחדלDefault value
التحميل الزائدהעמסהOverloading
الإجابات النهائية لأسئلة الفصل
8.1 (أ) str فئة و"LED" كائن ، (ب) Turtle فئة وa كائن ، (ج) الأول فئة والثاني كائن.
8.2 5.0 , 4.0 , 49
8.3 120 lab , 12 store , 120
8.4 9 5 , 60 2
8.5 24 0 , 12 0 , 24 0 , 48 900
8.6 30 35
8.7 5 5 False , 10 0 True , 60
8.8 40 W: OFF , 40 W: ON , Bulb 40 W: ON

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

المستوى الأول — أساسي
1.
ماذا يطبع البرنامج؟
class Fan:
    speed = 1200

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

a = Fan("A")
b = Fan("B")
b.speed = 900
Fan.speed = 1500
print(a.speed, b.speed, Fan.speed)
الإجابات: 1500 900 1500
2.
في هذه الفئة خطآن يمنعانها من العمل. نجدهما ونصلحهما.
class Lamp:
    def __init__(self, watts):
        watts = watts

    def energy(hours):
        return self.watts * hours / 1000

desk = Lamp(9)
print(desk.energy(100))
الإجابات: ينقص self.watts = في عمليّة البناء، وينقص الوسيط self في تعريف energy:
class Lamp:
    def __init__(self, watts):
        self.watts = watts

    def energy(self, hours):
        return self.watts * hours / 1000

desk = Lamp(9)
print(desk.energy(100))
يطبع: 0.9
3.
نكتب فئة Fuse فيها الصفة rating، والعمليّة blows(amps) التي تعيد True إذا كان التيار أكبر من قيمة المصهر.
الإجابات:
class Fuse:
    def __init__(self, rating):
        self.rating = rating

    def blows(self, amps):
        return amps > self.rating

f = Fuse(16)
print(f.blows(16), f.blows(20))
يطبع: False True
4.
نكتب فئة Switch لمفتاح يبدأ مفتوحا، فيها العمليّة toggle التي تعكس حالته، و__str__ التي تعيد اسمه وحالته.
الإجابات:
class Switch:
    def __init__(self, name):
        self.name = name
        self.on = False

    def toggle(self):
        self.on = not self.on

    def __str__(self):
        if self.on:
            return self.name + ": ON"
        return self.name + ": OFF"

s = Switch("S1")
print(s)
s.toggle()
print(s)
يطبع: S1: OFF , S1: ON
المستوى الثاني — متوسط
5.
نكتب فئة Capacitor بسعة وبجهد قيمته الافتراضية 12 V، فيها العمليّة charge، Q = C · V، والعمليّة energy، E = C · V2 2، والعمليّة __str__.
الإجابات:
class Capacitor:
    def __init__(self, c, v=12):
        self.c = c
        self.v = v

    def charge(self):
        return self.c * self.v

    def energy(self):
        e = self.c * self.v ** 2 / 2
        return round(e, 4)

    def __str__(self):
        text = str(self.c) + " F, "
        return text + str(self.v) + " V"

c = Capacitor(0.001)
print(c)
print(c.charge(), c.energy())
d = Capacitor(0.002, 5)
print(d)
print(d.charge(), d.energy())
يطبع: 0.001 F, 12 V , 0.012 0.072 , 0.002 F, 5 V , 0.01 0.025
6.
نكتب فئة PulseCounter لعدّاد نبضات: تبدأ من صفر، فيها pulse التي تضيف n نبضات وقيمتها الافتراضية 1، وreset.
الإجابات:
class PulseCounter:
    def __init__(self):
        self.count = 0

    def pulse(self, n=1):
        self.count = self.count + n

    def reset(self):
        self.count = 0

c = PulseCounter()
c.pulse()
c.pulse()
c.pulse(5)
print(c.count)
c.reset()
c.pulse(2)
print(c.count)
يطبع: 7 , 2
7.
نكتب فئة Part لمقاومة قيمتها معروفة ولها سماح بالمئة، قيمته الافتراضية 5. فيها low وhigh اللتان تعطيان حدّي المجال، وaccepts التي تفحص قيمة مقيسة.
الإجابات:
class Part:
    def __init__(self, ohms, tol=5):
        self.ohms = ohms
        self.tol = tol

    def low(self):
        d = self.ohms * self.tol / 100
        return self.ohms - d

    def high(self):
        d = self.ohms * self.tol / 100
        return self.ohms + d

    def accepts(self, x):
        return self.low() <= x <= self.high()

r = Part(1000)
print(r.low(), r.high())
print(r.accepts(1040), r.accepts(1060))
s = Part(1000, 1)
print(s.accepts(1040))
يطبع: 950.0 1050.0 , True False , False
8.
نريد أن يحصل كل لوح يُصنع على رقم تسلسلي خاص به، بدءا من 100. نكتب فئة Board تحقق ذلك من دون أن نمرر الرقم عند الإنشاء.
الإجابات:
class Board:
    next_id = 100

    def __init__(self, model):
        self.model = model
        self.serial = Board.next_id
        Board.next_id = Board.next_id + 1

    def __str__(self):
        text = str(self.serial)
        return self.model + "-" + text

a = Board("ESP")
b = Board("UNO")
c = Board("ESP")
print(a, b, c)
print(Board.next_id)
يطبع: ESP-100 UNO-101 ESP-102 , 103. الرقم التالي يخص الفئة كلها، ولذلك هو متغيّر فئة، أما رقم كل لوح فهو متغيّر نسخة.
9.
لدينا قائمة ثنائيات باعثة للضوء، ولكل ثنائي لون وجهد وتيار. نكتب فئة، وبرنامجا يطبع لون الثنائي صاحب أعلى قدرة. نضع الكائنات في قائمة كما في البند 8.8، ونبحث فيها عن القيمة القصوى كما في البند 6.12.
الإجابات:
class Led:
    def __init__(self, color, volts, amps):
        self.color = color
        self.volts = volts
        self.amps = amps

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

leds = [Led("red", 2.0, 0.02),
        Led("blue", 3.2, 0.02),
        Led("white", 3.0, 0.35)]
best = leds[0]
for led in leds:
    if led.power() > best.power():
        best = led
print(best.color, round(best.power(), 2))
يطبع: white 1.05
المستوى الثالث — تحدٍّ
10.
نكتب فئة Uptime تحفظ زمن عمل المحطة بالثواني، فيها tick(n=1)، و__str__ التي تعيد الزمن بالشكل hh:mm:ss. وعمليّة مساعدة تكمل الصفر قبل العدد ذي الخانة الواحدة.
الإجابات:
class Uptime:
    def __init__(self, seconds=0):
        self.seconds = seconds

    def tick(self, n=1):
        self.seconds = self.seconds + n

    def two(self, n):
        if n < 10:
            return "0" + str(n)
        return str(n)

    def __str__(self):
        h = self.seconds // 3600
        m = self.seconds % 3600 // 60
        s = self.seconds % 60
        text = self.two(h) + ":"
        text = text + self.two(m) + ":"
        return text + self.two(s)

t = Uptime(3599)
print(t)
t.tick()
print(t)
t.tick(7384)
print(t)
يطبع: 00:59:59 , 01:00:00 , 03:03:04
11.
نكتب فئة Signal تحفظ قائمة عينات لإشارة، فيها add وmean وpeak و__str__. كل كائن يجب أن يحصل على قائمته الخاصة.
الإجابات:
class Signal:
    def __init__(self, name):
        self.name = name
        self.data = []

    def add(self, value):
        self.data.append(value)

    def mean(self):
        n = len(self.data)
        return sum(self.data) / n

    def peak(self):
        return max(self.data)

    def __str__(self):
        n = str(len(self.data))
        text = self.name + ": " + n
        return text + " samples"

s = Signal("A0")
for x in [2, 4, 9, 5]:
    s.add(x)
print(s)
print(s.mean(), s.peak())
يطبع: A0: 4 samples , 5.0 9
12.
نكتب فئة Reg8 لمسجّل من 8 خانات: is_set(bit) وset_bit(bit) وclear_bit(bit) بالحساب فقط، و__str__ التي تعطي الخانات الثماني من الأعلى إلى الأدنى.
الإجابات:
class Reg8:
    def __init__(self, value=0):
        self.value = value

    def is_set(self, bit):
        v = self.value // 2 ** bit
        return v % 2 == 1

    def set_bit(self, bit):
        if not self.is_set(bit):
            self.value += 2 ** bit

    def clear_bit(self, bit):
        if self.is_set(bit):
            self.value -= 2 ** bit

    def __str__(self):
        text = ""
        for bit in range(7, -1, -1):
            if self.is_set(bit):
                text = text + "1"
            else:
                text = text + "0"
        return text

r = Reg8()
r.set_bit(0)
r.set_bit(3)
r.set_bit(3)
print(r, r.value)
r.clear_bit(0)
r.clear_bit(5)
print(r, r.value)
يطبع: 00001001 9 , 00001000 8. نفحص قبل أن نضيف أو نطرح، ولذلك لم يتغيّر شيء عندما كررنا الخانة 3، ولا عندما مسحنا الخانة 5.

שאלות בגרות – أسئلة بجروت للفصل 8

أسئلة الفئات الأربعة التالية من امتحان البجروت. في 2023 و2024 يظهر النص العربي الرسمي، وفي 2025 و2026 يظهر السؤال بالعبرية، إذ لا يوجد له نص عربي رسمي في هاتين السنتين. الأسماء مثل addNum وDoSomething هي أسماء الامتحان، ونتركها كما هي حتى لو لم تكن على أسلوب هذا الكتاب.
بجروت 2023 السؤال 11 — المتوسط الحسابي
بجروت 2023
البندالمطلوبالإجابة
(أ)عمليّة بناء بلا وسائط تضع في الصفتين صفرا، وعمليّة display تطبع قيمتيهما.في الأسفل
(ب)ماذا يطبع المقطع المعطى؟Average is 0.0
(ج)نغيّر addNum بحيث تدخل في الحساب العلامات من 0 حتى 100 فقط.في الأسفل
الحل الكامل بالعربية: fahed-lab.com/exams/electronics-815381/815381/solutions/solution-2023

(أ) الشيفرة المعطاة تعتمد على صفتين لا ينشئهما أحد، ومن دون عمليّة بناء يتوقف البرنامج عند أول استدعاء بالخطأ AttributeError. نضيف العمليّتين المطلوبتين، ونشغّل المقطع الذي في البند (ب). أضفنا إليه في النهاية السطر a.display() فقط لكي نفحص. مجموع الأعداد الخمسة صفر، فالمتوسط، وهو ما يسميه السؤال «المعدّل الحسابي»، صفر، ونتيجة القسمة عدد عشري:

class Average:
    def __init__(self):
        self.sum = 0
        self.count = 0

    def addNum(self, num):
        self.sum = self.sum + num
        self.count = self.count + 1

    def calcAverage(self):
        return self.sum / self.count

    def display(self):
        print(self.sum, self.count)

arr = [90, 100, -40, -60, -90]
a = Average()
for item in arr:
    a.addNum(item)
x = a.calcAverage()
print("Average is", x)
a.display()
المُخرَج
Average is 0.0
0 5

(ج) نحيط السطرين بشرط، فلا يدخل العدد خارج المجال لا في المجموع ولا في العدد. نغيّر العمليّة addNum فقط:

def addNum(self, num):
    if 0 <= num <= 100:
        self.sum = self.sum + num
        self.count = self.count + 1

الآن يمر العددان 90 و100 فقط، ويطبع المقطع نفسه: Average is 95.0 , 190 2.

ملاحظة: في نص السؤال كُتب AddNum بحرف كبير في البند (ج)، وفي الشيفرة addNum. بايثون تميّز بينهما، والصحيح هو ما في الشيفرة. وإذا لم تدخل أي علامة صالحة كان count صفرا، وأدّت القسمة إلى خطأ. السؤال لا يطلب معالجة هذه الحالة، ولكن من الجيد أن ننتبه إليها.
بجروت 2024 السؤال 11 — قائمة أعداد بخطوة ثابتة
بجروت 2024
بجروت 2024
البندالمطلوبالإجابة
(أ) 1ماذا يطبع المقطع؟False , True , False , True , False
(أ) 2جملة واحدة: ماذا تفعل DoSomething؟تفحص هل يقع العدد على إحدى خطوات القائمة، أي هل الفرق بينه وبين start من مضاعفات step.
(ب)العمليّة printRange تطبع عناصر القائمة.في الأسفل
(ج)العمليّة getNumberByIndex تعيد العنصر الذي عند مؤشّر معيّن.في الأسفل
الحل الكامل بالعربية: fahed-lab.com/exams/electronics-815381/815381/solutions/solution-2024

(أ) القائمة هنا هي 10 , 14 , 18. نشغّل المقطع، فيطبع True عند العددين اللذين فيها من بين ما فحصناه:

class ListClass:
    def __init__(self, start, end, step):
        self.start = start
        self.end = end
        self.step = step

    def DoSomething(self, number):
        if (number - self.start) % self.step != 0:
            return False
        return True

lst = ListClass(10, 21, 4)
print(lst.DoSomething(11))
print(lst.DoSomething(14))
print(lst.DoSomething(16))
print(lst.DoSomething(18))
print(lst.DoSomething(21))
المُخرَج
False
True
False
True
False
ملاحظة: العمليّة لا تفحص الحدود: العددان 6 و22 خارج القائمة، لكن فرقهما عن start من مضاعفات step، فتعطي True لهما: True True. وفحص كامل يحتاج إلى الشرط self.start <= number <= self.end أيضا. أما في نص السؤال فكُتب steep، وفي الشيفرة step، والصحيح هو ما في الشيفرة.

(ب) نبدأ من start، ونزيد في كل دورة بمقدار step، ما دمنا لم نتجاوز end. أما 12 فلا تظهر في المثال، لأنها الحد الأقصى الممكن فقط، والخطوة تقفز من 11 إلى 13. ونطبع بالوسيط end=" " كي تبقى الأعداد في سطر واحد. ويمكن أيضا أن نكتب الحلقة for number in range(self.start, self.end + 1, self.step). (ج) لا نحتاج إلى حلقة: العنصر الذي مؤشّره index يبعد index خطوة عن البداية، ومؤشّر العنصر الأول هو 0:

def printRange(self):
    number = self.start
    while number <= self.end:
        print(number, end=" ")
        number = number + self.step
    print()

def getNumberByIndex(self, index):
    return self.start + index * self.step

للكائن ListClass(5, 12, 2) يطبع المثالان اللذان في السؤال: 5 7 9 11 , 7.

بجروت 2025 السؤال 10 — المستطيل
بجروت 2025
بجروت 2025
البندالمطلوبالإجابة
(أ)ماذا يطبع المقطع؟في الأسفل
(ب)العمليّة IsSquare تعيد True إذا كان المستطيل مربعا، وإلا False.self.length == self.width
(ج)ننشئ مستطيلين، 25 في 17 و25 في 25، ونفحص كلا منهما.False , True
الحل الكامل بالعربية: fahed-lab.com/exams/electronics-815381/815381/solutions/solution-2025

(أ) عمليّة البناء تفحص القيم كما في البند 8.5. العدد −1 سالب، فينفذ فرع else، ويصبح الطول والعرض 2. والعمليّة DoSomething تحسب في الحقيقة مساحة المستطيل. ثم تمر الحلقة على القيم −1 , 0 , 1 , 2، وكلها تترك الطول موجبا، فيُقبل كل تغيير. وفي عرض النتيجة نستعمل البادئة f من البند 8.8:

class Rectangle:
    def __init__(self, length, width):
        if length >= 0 and width >= 0:
            self.length = length
            self.width = width
        else:
            self.length = 2
            self.width = 2

    def AddToDimensions(self, lengthToAdd, widthToAdd):
        if (self.length + lengthToAdd > 0
                and self.width + widthToAdd > 0):
            self.length = self.length + lengthToAdd
            self.width = self.width + widthToAdd

    def DoSomething(self):
        return self.length * self.width

    def print_details(self):
        print(f"L={self.length} W={self.width}", end=" ")
        print(f"D1={self.DoSomething()}")

rect = Rectangle(-1, 1)
rect.print_details()

for i in range(-1, 3):
    rect.AddToDimensions(i, 0)
    rect.print_details()
المُخرَج
L=2 W=2 D1=4
L=1 W=2 D1=2
L=1 W=2 D1=2
L=2 W=2 D1=4
L=4 W=2 D1=8

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

(ب) المقارنة نفسها تعطي القيمة المطلوبة، فلا نحتاج إلى if. والسؤال يكتب true وfalse، أما في بايثون فالقيمتان بحرف كبير. (ج) ننشئ الكائنين، ونستدعي العمليّة على كل واحد:

def IsSquare(self):
    return self.length == self.width
rect1 = Rectangle(25, 17)
rect2 = Rectangle(25, 25)
print(rect1.IsSquare())
print(rect2.IsSquare())

يطبع البرنامج: False , True.

بجروت 2026 السؤال 10 — بطاقة السفر
بجروت 2026
البندالمطلوبالإجابة
(أ)ماذا يطبع المقطع؟في الأسفل
(ب)نطبع اسم صاحب البطاقة ذات الرصيد الأعلى من بين بطاقتين.حللناه في آخر الفصل 3
(ج)نضيف صفة تعدّ السفرات الناجحة، ونحدّث pay_for_trip وget_info.في الأسفل
الحل الكامل بالعربية: fahed-lab.com/exams/electronics-815381/815381/solutions/solution-2026

(أ) الرصيد 55.5، ويصبح 75.5 بعد الشحن. الدفعة الأولى تنجح فيبقى 35.5. والثانية تفشل لأن الرصيد أقل من 40، فتعود False ولا يتغيّر الرصيد. قسمنا سطر get_info لطوله، والنتيجة هي نفسها:

class TravelCard:
    def __init__(self, owner_name, balance):
        self.owner_name = owner_name
        self.balance = balance

    def top_up(self, amount):
        if amount > 0:
            self.balance = self.balance + amount

    def pay_for_trip(self, cost):
        if self.balance >= cost:
            self.balance = self.balance - cost
            return True
        return False

    def get_info(self):
        text = self.owner_name + " has "
        text = text + str(self.balance)
        return text + " shekels left."

card = TravelCard("Guest", 55.5)
card.top_up(20)
print("Pay 40:", card.pay_for_trip(40))
print("Pay 40:", card.pay_for_trip(40))
print(card.get_info())
المُخرَج
Pay 40: True
Pay 40: False
Guest has 35.5 shekels left.

(ب) كتبنا هذا البرنامج في آخر الفصل 3 بمتغيّرات منفصلة. الآن ننشئ كائنين c1 وc2، ونكتب c1.balance بدل balance1، وc1.owner_name بدل name1، ويبقى الشرط كما هو.

(ج) ثلاثة تغييرات: الصفة الجديدة trips تبدأ من صفر في عمليّة البناء، وتزيد داخل الشرط لأن السؤال يعدّ السفرات التي نجح دفعها فقط، وget_info تضيفها إلى الجملة. نكتب هنا ما تغيّر فقط:

class TravelCard:
    def __init__(self, owner_name, balance):
        self.owner_name = owner_name
        self.balance = balance
        self.trips = 0

    def pay_for_trip(self, cost):
        if self.balance >= cost:
            self.balance = self.balance - cost
            self.trips = self.trips + 1
            return True
        return False

    def get_info(self):
        text = self.owner_name + " has "
        text = text + str(self.balance)
        text = text + " shekels left. Trips: "
        return text + str(self.trips)

المقطع نفسه يطبع في سطره الأخير: Guest has 35.5 shekels left. Trips: 1. سفرة واحدة، مع أننا حاولنا مرتين.