المسارات
/
Python
Python
/
التمارين
/
لعبة Ellen مع الكائنات الفضائية
لعبة Ellen مع الكائنات الفضائية

لعبة Ellen مع الكائنات الفضائية

تمرين تعلّمي

مقدمة

البرمجة كائنية التوجه في Python

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

الأصناف

Classes هي تعريفات أنواع جديدة من الكائنات، ومنها تُنشأ instances جديدة من الكائنات. وهي غالبًا ما تجمع البيانات مع الكود أو الدوال التي تعمل على تلك البيانات. وبهذا المعنى، تُعد الأصناف مخططات أو مجموعات من التعليمات يمكن منها بناء العديد من الكائنات من نوع مشابه واستخدامها. ويمكن لبرنامج معقد أن يحتوي على أصناف كثيرة، يبني كل منها أشكالًا مختلفة ومتنوعة من الكائنات. وتُعرف عملية بناء كائن من صنف باسم instantiation (أو إنشاء نسخة من الصنف).

تعريف الصنف في Python مباشر وبسيط:

class MyClass:
    # Class body goes here

خصائص الصنف

يمكن إضافة حقول الصنف (المعروفة أيضًا باسم properties أو attributes أو data members أو variables) إلى جسم الصنف:

class MyClass:
    number = 5
    string = "Hello!"

يمكن إنشاء نسخة (كائن) من MyClass وربطها باسم عبر استدعاء الصنف (تمامًا كما تُستدعى الدالة):

>>> new_object = MyClass()

# Class is instantiated and resulting object is bound to the "new_object" name.
# Note: the object address 'at 0x15adc55b0' will vary by computer.
>>> new_object
<__main__.MyClass at 0x15adc55b0>

Class attributes مشتركة بين جميع الكائنات (أو النسخ) المنشأة من صنف ما، ويمكن الوصول إليها عبر dot notation، أي . توضع بعد اسم الكائن وقبل اسم الخاصية:

>>> new_object = MyClass()

# Accessing the class attribute "number" via dot-notation.
>>> new_object.number
5

# Accessing the class attribute "string" via dot-notation.
>>> new_object.string
'Hello!'

# Instantiating an additional object and binding it to the "second_new_object" name.
>>> second_new_object = MyClass()

>>> second_new_object
# Note: the object address "at 0x15ad99970" will vary by computer.
<__main__.MyClass at 0x15ad99970>

# Second_new_object shares the same class attributes as new_object.
>>> new_object.number == second_new_object.number
True

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

>>> obj_one = MyClass()
>>> obj_two = MyClass()

# Accessing a class attribute from an object.
>>> obj_two.number
5

# Accessing the class attribute from the class itself.
>>> MyClass.number
5

# Modifying the value of the "number" class attribute.
>>> MyClass.number = 27

# Modifying the "number" class attribute changes the "number" attribute for all objects.
>>> obj_one.number
27

>>> obj_two.number
27

أن يكون لديك مجموعة من الكائنات ببيانات متزامنة طوال الوقت ليس مفيدًا كثيرًا. لحسن الحظ، يمكن تخصيص الكائنات المنشأة من صنف بخصائص instance attributes خاصة بها (أو خصائص النسخة أو متغيراتها أو حقولها). وكما يوحي اسمها، فإن خصائص النسخة فريدة لكل كائن، ويمكن تعديلها بشكل مستقل.

تخصيص إنشاء الكائنات باستخدام __init__()

تُستخدم "dunder method" الخاصة (اختصارًا لـ «طريقة الشرطة السفلية المزدوجة»)، وهي __init__()، في تخصيص نسخ الأصناف، ويمكن استخدامها لتهيئة خصائص النسخة أو الخصائص للكائنات. ونظرًا لدورها في تهيئة خصائص النسخة، يُشار إلى __init__() أيضًا باسم class constructor أو initializer. وتأخذ __init__() معاملًا واحدًا مطلوبًا يُسمى self، وهو يشير إلى الكائن المُهيَّأ أو المُنشأ حديثًا. ويمكن بعد ذلك تمرير بيانات خصائص النسخة أو الخصائص كوسائط لـ __init__()، بعد المعامل self.

في المثال التالي، أصبح لـ MyClass خصائص نسخة تُسمى location_x وlocation_y. وكما ترى، أُسندت الخاصيتان إلى الفهرس الأول والثاني من الوسيط location (وهي tuple) الذي مُرِّر إلى __init__(). وسيتم الآن تهيئة الخاصيتين location_x وlocation_y لنسخة الصنف عند إنشاء الصنف، أي عند إنشاء كائن:

class MyClass:
    # These are class attributes, variables, or fields.
    number = 5
    string = "Hello!"

    # This is the class "constructor", with a "location" parameter that is a tuple.
    def __init__(self, location):

        # This is an instance or object property, attribute, or variable.
        # Note that we are unpacking the tuple argument into two separate instance variables.
        self.location_x = location[0]
        self.location_y = location[1]

# Create a new object "new_object_one", with object property (1, 2).
>>> new_object_one = MyClass((1, 2))

# Create a second new object "new_object_two" with object property (-8, -9).
>>> new_object_two = MyClass((-8, -9))

# Note that new_object_one.location_x and new_object_two.location_x two different values.
>>> new_object_one.location_x
1

>>> new_object_two.location_x
-8

لاحظ أنك تحتاج فقط إلى تمرير وسيط واحد عند تهيئة MyClass أعلاه؛ فـ Python تتولى تمرير self عند استدعاء الصنف.

الطُرق

method هي function مرتبطة إما بالصنف نفسه (وتُعرف باسم class method، وسنناقشها في تمرين لاحق) أو بـ نسخة من الصنف (الكائن). والطرق التي تعمل على كائن (نسخة) يجب تعريفها بحيث يكون self هو المعامل الأول. ويمكنك بعد ذلك تعريف بقية المعاملات كما تفعل مع دالة "عادية" أو غير مرتبطة:

class MyClass:
    number = 5
    string = "Hello!"

    # Class constructor.
    def __init__(self, location):
        # Instance properties
        self.location_x = location[0]
        self.location_y = location[1]

    # Instance method. Note "self" as first parameter.
    def change_location(self, amount):
        self.location_x += amount
        self.location_y += amount
        return self.location_x, self.location_y

وكما هو الحال في الوصول إلى الخصائص، فإن استدعاء طريقة ما يتطلب ببساطة وضع . بعد اسم الكائن وقبل اسم الطريقة. ولا تحتاج الطريقة المُستدعاة إلى نسخة من الكائن كمعامل أول؛ فـ Python تملأ self تلقائيًا:

class MyClass:
    number = 5
    string = "Hello!"

    def __init__(self, location):
        self.location_x = location[0]
        self.location_y = location[1]

    def change_location(self, amount):
        self.location_x += amount
        self.location_y += amount
        return self.location_x, self.location_y

# Make a new test_object with location (3,7)
>>> test_object = MyClass((3,7))
>>> (test_object.location_x, test_object.location_y)
(3,7)

# Call change_location to increase location_x and location_y by 7.
>>> test_object.change_location(7)
(10, 14)

يمكن الوصول إلى خصائص الصنف من داخل طرق النسخة بالكيفية نفسها التي يتم بها الوصول إليها من خارج الصنف:

class MyClass:
    number = 5
    string = "Hello!"

    def __init__(self, location):
        self.location_x = location[0]
        self.location_y = location[1]

    # Alter instance variable location_x and location_y
    def change_location(self, amount):
        self.location_x += amount
        self.location_y += amount
        return self.location_x, self.location_y

    # Alter class variable number for all instances from within an instance.
    def increment_number(self):
        # Increment the 'number' class variable by 1.
        MyClass.number += 1


>>> test_object_one = MyClass((0,0))
>>> test_object_one.number
5

>>> test_object_two = MyClass((13, -3))
>>> test_object_two.increment_number()
>>> test_object_one.number
6

حجز مكان التنفيذ أو وضع هيكل مؤقت له باستخدام الكلمة المفتاحية pass

في تمارين المفاهيم السابقة وهياكل التمارين التطبيقية، لا بد أنك رأيت الكلمة المفتاحية pass مستخدمة داخل جسم الدوال بدلًا من الكود الفعلي. الكلمة المفتاحية pass هي عنصر حجز صالح نحويًا؛ فهي تمنع Python من إطلاق خطأ نحوي بسبب تعريف دالة أو صنف فارغ. وهي في جوهرها وسيلة لتقول بها لمفسّر Python: «لا تقلق! سأضع الكود هنا في النهاية، لكنني لم أفعل ذلك بعد.»

class MyClass:
    number = 5
    string = "Hello!"

    def __init__(self, location):
        self.location_x = location[0]
        self.location_y = location[1]

    # Alter instance variable location_x and location_y
    def change_location(self, amount):
        self.location_x += amount
        self.location_y += amount
        return  self.location_x, self.location_y

    # Alter class variable number for all instances
    def increment_number(self):
        # Increment the 'number' class variable by 1.
        MyClass.number += 1

    # This will compile and run without error, but has no current functionality.
    def pending_functionality(self):
       # Stubbing or placholding the body of this method.
       pass

التعليمات

تصنع إيلين لعبة على اللاعب أن يحارب فيها كائنات فضائية. لقد تعلّمت للتو البرمجة كائنية التوجه (OOP)، وهي متشوّقة للاستفادة مما قد يقدمه استخدام classes لبرنامجها.

ولسرور إيلين، عرضتَ عليها المساعدة، وقد أوكلت إليك مهمة برمجة الكائنات الفضائية التي على اللاعب أن يحاربها.

1. إنشاء صنف Alien

عرّف صنف Alien بمُنشئ يقبل معاملين هما <x_coordinate> و<y_coordinate>، ويضعهما في متغيري النسخة x_coordinate وy_coordinate. كما تبدأ كل الكائنات الفضائية بمستوى صحة قدره 3، لذا ينبغي تهيئة المتغير health أيضًا.

>>> alien = Alien(2, 0)
>>> alien.x_coordinate
2
>>> alien.y_coordinate
0
>>> alien.health
3

والآن، ينبغي أن يكون كل كائن فضائي قادرًا على تتبع موقعه وصحته من الداخل.

2. الطريقة hit

تريد إيلين أن يحتوي صنف Alien class على طريقة hit تُنقص صحة كائن فضائي بمقدار 1 عند استدعائها. وبهذه الطريقة، تستطيع ببساطة استدعاء <alien>.hit() بدلًا من أن تضطر إلى تغيير صحة الكائن الفضائي يدويًا. الأمر متروك لك فيما إذا كان hit() ينقص نقاط الصحة إلى الصفر أم إلى ما دون الصفر.

>>> alien = Alien(0, 0)

# Initialized health value.
>>> alien.health
3

# Decrements health by 1 point.
>>> alien.hit()
>>> alien.health
2

3. الطريقة is_alive

أنت تدرك أنه إذا استمرت الصحة في الانخفاض، فمن المحتمل أن تصل عند نقطة ما إلى 0 (أو حتى أقل!). سيكون من الجيد إضافة طريقة is_alive تستطيع إيلين استدعاءها بسرعة للتحقق مما إذا كان الكائن الفضائي... حسنًا... على قيد الحياة. 😉 ينبغي أن تُرجع <alien>.is_alive() قيمة منطقية.

>>> alien.health
1
>>> alien.is_alive()
True
>>> alien.hit()
>>> alien.health
0
>>> alien.is_alive()
False

4. الطريقة teleport

في لعبة إيلين، تمتلك الكائنات الفضائية قدرة على الانتقال الفوري! ستحتاج إلى كتابة طريقة teleport تأخذ قيمتي x_coordinate وy_coordinate جديدتين، وتغيّر إحداثيات الكائن الفضائي وفقًا لذلك.

>>> alien.teleport(5, -4)
>>> alien.x_coordinate
5
>>> alien.y_coordinate
-4

5. الطريقة collision_detection

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

>>> alien.collision_detection(other_object)
>>>

6. عداد الكائنات الفضائية

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

على سبيل المثال:

>>> alien_one = Alien(5, 1)
>>> alien_one.total_aliens_created
1
>>> alien_two = Alien(3, 0)
>>> alien_two.total_aliens_created
2
>>> alien_one.total_aliens_created
2
# Accessing the variable from the class directly
>>> Alien.total_aliens_created
2

7. إنشاء مصفوفة من الكائنات الفضائية

تحب إيلين ما أنجزته حتى الآن، لكن لديها خدمة أخيرة تطلبها. تريد دالة مستقلة (خارج صنف Alien()) تُنشئ list من كائنات Alien()، بالاعتماد على مصفوفة من المواقع (على شكل tuples).

على سبيل المثال:

>>> alien_start_positions = [(4, 7), (-1, 0)]
>>> aliens = new_aliens_collection(alien_start_positions)
...
>>> for alien in aliens:
...     print(alien.x_coordinate, alien.y_coordinate)
(4, 7)
(-1, 0)
تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة
Python Exercism

مستعد لبدء لعبة Ellen مع الكائنات الفضائية؟

سجّل في Exercism لتتعلّم وتتقن Python عبر 17 مفهومًا146 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.