تساعدك دالة isinstance في بايثون على تحديد ما إذا كان كائن ما نسخةً من فئة معينة أو من فئتها الأصلية، مما يُسهم في كتابة كود أكثر وضوحًا وموثوقية. تُستخدم هذه الدالة للتأكد من أن مُعاملات الدالة من الأنواع المتوقعة، مما يسمح لك بمعالجة المشكلات المتعلقة بالأنواع استباقيًا. يشرح هذا الدرس كيفية عمل دالة ()isinstance()، واستخدامها مع الفئات الفرعية، وكيف تختلف عن دالة type().
سيؤدي استكشاف دالة ()isinstance إلى تعميق فهمك للكائنات التي تعمل معها ومساعدتك على كتابة كود أكثر قوة وخالٍ من الأخطاء.
لتحقيق أقصى استفادة من هذا الدرس، يُنصح بأن يكون لديك فهم أساسي للبرمجة كائنية التوجه. وبشكل أكثر تحديدًا، يجب أن تفهم مفاهيم الفئات، والكائنات، والوراثة.
في هذا الدرس، ستستخدم بشكل أساسي بيئة تفاعل بايثون (REPL) وبعض ملفات بايثون. لن تحتاج إلى تثبيت أي مكتبات، فكل ما تحتاجه موجود ضمن بايثون الأساسية.
لماذا قد تستخدم دالة ()isinstance في بايثون؟
تحدد الدالة ()isinstance ما إذا كان الكائن نسخة من فئة معينة، كما تحدد ما إذا كان نسخة من فئة أصلية. لاستخدام الدالة ()isinstance، عليك تمرير وسيطين إليها:
- الحالة التي تريد تحليلها
- الفئة التي تريد مقارنة الكائن بها
يجب تمرير هذه الوسائط فقط عن طريق الموضع، وليس عن طريق الكلمة المفتاحية.
إذا كان الكائن الذي تمرره كوسيط أول هو نسخة من الفئة التي تمررها كوسيط ثانٍ، فإن الدالة ()isinstance تُرجع القيمة True. وإلا، فإنها تُرجع القيمة False.
عندما تبدأ بتعلم لغة بايثون، ستُخبر بأن الكائنات موجودة في كل مكان. هل يعني هذا أن كل عدد صحيح، أو سلسلة نصية، أو قائمة، أو دالة تصادفها هي كائن؟ نعم، هذا صحيح! في الكود أدناه، ستحلل بعض أنواع البيانات الأساسية:
>>> shape = "sphere"
>>> number = 8
>>> isinstance(shape, str)
True
>>> isinstance(number, int)
True
>>> isinstance(number, float)
False
تقوم بإنشاء متغيرين، shape و number، يحتويان على كائنات من نوع str و int على التوالي. ثم تمرر shape و str إلى أول استدعاء للدالة ()isinstance لإثبات ذلك. تُرجع الدالة ()isinstance القيمة True، مما يُظهر أن “sphere” هي بالفعل سلسلة نصية.
بعد ذلك، تُمرر نوعي البيانات number و int إلى الاستدعاء الثاني للدالة ()isinstance، والتي تُرجع أيضًا القيمة True. هذا يُخبرك أن 8 عدد صحيح. أما الاستدعاء الثالث فيُرجع القيمة False لأن 8 ليس عددًا عشريًا.
يُعدّ معرفة نوع البيانات التي تُمرّرها إلى دالة أمرًا بالغ الأهمية لتجنّب المشاكل الناجمة عن أنواع البيانات غير الصالحة. مع أنّه من الأفضل تجنّب تمرير بيانات غير صحيحة من البداية، فإنّ استخدام دالة ()isinstance يُتيح لك تجنّب أيّ عواقب غير مرغوب فيها.
ألقِ نظرة على الكود أدناه:
>>> def calculate_area(length, breadth):
... return length * breadth
>>> calculate_area(5, 3)
15
>>> calculate_area(5, "3")
'33333'
تأخذ دالتك قيمتين عدديتين، وتضربهما، ثم تُرجع الناتج. تعمل دالتك، ولكن فقط إذا مررت إليها رقمين. إذا مررت إليها رقمًا وسلسلة نصية، فلن يتعطل البرنامج، ولكنه لن يُنفذ ما تتوقعه أيضًا.
يتم تكرار السلسلة عند تمرير سلسلة وعدد صحيح إلى عامل الضرب (*). في هذه الحالة، يتم تكرار الرقم “3” خمس مرات لتكوين “33333”، وهي على الأرجح النتيجة التي لم تتوقعها.
تزداد الأمور سوءًا عند تمرير سلسلتين نصيتين:
>>> calculate_area("5", "3")
Traceback (most recent call last):
File "<python-input-8>", line 1, in <module>
calculate_area("5", "3")
~~~~~~~~~~~~~~^^^^^^^^^^
File "<python-input-5>", line 2, in calculate_area
return length * breadth
~~~~~~~^~~~~~~~~
TypeError: can't multiply sequence by non-int of type 'str'
لا يستطيع عامل الضرب التعامل مع سلسلتين نصيتين، لذا يتعطل البرنامج. هنا يمكنك استخدام الدالة ()isinstance لتنبيه المستخدم بشأن البيانات غير الصالحة.
يوضح الإصدار المحسن من دالة ()calculate_area الخاصة بك هذا الأمر:
>>> def calculate_area(length, breadth):
... if isinstance(length, int) and isinstance(breadth, int):
... return length * breadth
... raise TypeError("Both arguments must be integers")
>>> calculate_area(5, 3)
15
>>> calculate_area(5, "3")
Traceback (most recent call last):
...
TypeError: Both arguments must be integers
>>> calculate_area("5", "3")
Traceback (most recent call last):
...
TypeError: Both arguments must be integers
لتجنب النتائج غير المتوقعة، يمكنك استخدام استدعاءين للدالة ()isinstance، بالإضافة إلى عامل التشغيل المنطقي and، للتحقق من أنك لم تقم بتمرير الطول أو العرض كسلسلة نصية – أو أي عدد غير صحيح آخر – عن طريق الخطأ.
إذا اكتشفت الدالة ()isinstance بيانات غير صالحة، فإن عبارة if ستؤدي إلى ظهور استثناء TypeError في دالتك. بالطبع، إذا تم تمرير عددين صحيحين، فستكون النتائج كما هي.
عمليًا، يجب عليك أيضًا التحقق مما إذا كان من الممكن أن تكون معلمات الطول والعرض من نوع الأعداد العشرية. ستتعلم لاحقًا كيفية دمج عمليات التحقق المتعددة في دالة ()isinstance.
يوضح هذا المثال للتحقق من نوع البيانات استخدامًا شائعًا للدالة ()isinstance. لقد قللت بذلك من احتمالية ظهور نتائج غير صالحة في أجزاء أخرى من التعليمات البرمجية.
الآن بعد أن تعرفت على أساسيات الدالة ()isinstance، ستنتقل إلى تعلم كيفية استخدامها لتحليل المثيلات داخل التسلسل الهرمي للفئات.
هل تستطيع الدالة ()isinstance اكتشاف الفئات الفرعية؟
إضافةً إلى تحديد نوع الكائن، يمكن للدالة ()isinstance أن تخبرك أيضًا ما إذا كان الكائن كائنًا من فئة أصلية. تذكر أن الكائن يُعتبر كائنًا من فئته الأصلية، وأي من فئاتها الأصلية، والفئة التي استخدمتها لإنشائه.
لنفترض أنك تُنشئ تسلسلًا هرميًا للفئات لمحاكي لعبة البلياردو. يمكنك البدء بفئة Ball التي تحتوي على سمات البيانات color. و shape.، بالإضافة إلى دالتي .rebound() و .detect_collision(). ثم يمكنك إنشاء فئة فرعية PoolBall خاصة بلعبتك. سترث هذه الفئة جميع سمات Ball، ولكن يمكنك أيضًا تعديل محتوياتها لتلبية المتطلبات الخاصة بلعبة البلياردو.
بعد القيام بذلك، قد ترغب في تصميم المزيد من محاكاة ألعاب الكرة. بدلاً من إنشاء فئة كرة جديدة، يمكنك إعادة استخدام فئتك الحالية وإنشاء المزيد من الفئات الفرعية.
على سبيل المثال، يمكن أن تكون الكرة بمثابة الفئة العليا لفئات كرة القدم، وكرة البلياردو، وكرة القدم الأمريكية، حيث تشترك كل منها في نفس المحتوى الأساسي ولكنها تنفذ الأساليب بشكل مختلف للتصرف بشكل مناسب داخل ألعابها الخاصة.
لنأخذ على سبيل المثال دالة .rebound() المُعرّفة ضمن فئة AmericanFootBall، والتي يشبه شكلها شكلاً كروياً مُطوّلاً. تتطلب هذه الدالة حسابات مختلفة عن تلك الخاصة بفئتي SoccerBall وPoolBall، وهما كرتان.
يُعرّف الكود أدناه بعض الفئات الفرعية للكرة:
class Ball:
def __init__(self, color, shape):
self.color = color
self.shape = shape
class PoolBall(Ball):
def __init__(self, color, number):
super().__init__(color, shape="sphere")
self.number = number
class AmericanFootBall(Ball):
def __init__(self, color):
super().__init__(color, shape="prolate spheroid")
يُعرّف هذا الكود تسلسلًا هرميًا بسيطًا للفئات، يحتوي على فئة أساسية تُسمى Ball، ولها فئتان فرعيتان تُسميان PoolBall و AmericanFootBall. عند إنشاء نسخة من فئة Ball، يجب تمرير قيم لسمات اللون (.color) والشكل (.shape).
تُعرَّف البيانات التي تُمرِّرها عند إنشاء مثيلك ضمن دالة تهيئة المثيل .__init__(). تُستدعى هذه الدالة تلقائيًا في كل مرة تُنشئ فيها مثيلًا من الفئة. يمكنك استخدامها لإنشاء أي كرة ملونة ومنحها أي شكل تريده.
والآن، لنلقِ نظرة فاحصة على فئة PoolBall. لإنشاء هذه الفئة كفئة فرعية من Ball، يمكنك تعريفها باستخدام class PoolBall(Ball). ستتمكن فئة PoolBall من الوصول إلى جميع الطرق وخصائص البيانات الخاصة بفئة Ball.
عند إنشاء كائن PoolBall، يتم تمرير اللون والرقم إليه، ولكن ليس الشكل، لأن هذا كل ما تتطلبه الدالة .__init__() مع ذلك، لا تزال كائنات PoolBall تحتوي على خاصية بيانات .shape التي تحتاج إلى تهيئة.
لتهيئة خاصية .shape، استدعِ الدالة الأصلية .__init__() المُعرَّفة في الفئة الأصلية Ball باستخدام super().__init__(color, shape="sphere"). يُمرِّر هذا اللون المطلوب والسلسلة النصية “sphere” إلى مُهيئ الفئة الأصلية. تُسند السلسلة النصية “sphere” إلى مُعامل shape في الفئة الأصلية. ستكون جميع كائنات PoolBall كروية الشكل دائمًا وباللون الذي اخترته.
لضمان تعيين قيمة الرقم لخاصية .number، استخدم self.number مرة أخرى. سيحتوي كائن PoolBall الجديد على لون وشكل “كرة” ورقم.
وبالمثل، ستحتوي جميع نسخ AmericanFootBall على سمة color. وقيمة سمة shape. هي “prolate spheroid”. ولن تحتوي على سمة number. لأنها غير ضرورية.
دراسة كيفية تعامل الدالة ()isinstance مع الفئات الفرعية
باستخدام التسلسل الهرمي للفئات الذي قمت بتطويره للتو، تحتاج إلى إنشاء بعض الحالات لكي تقوم الدالة ()isinstance بتحليلها:
>>> from balls import AmericanFootBall, Ball, PoolBall
>>> eight_ball = PoolBall("black", 8)
>>> football = AmericanFootBall("brown")
>>> ball = Ball("green", "sphere")
لإنشاء نسخ من الكائن، عليك تمرير القيم المطلوبة إلى كل فئة. على سبيل المثال، سيُنشئ الأمر PoolBall(“black”, 8) نسخة جديدة من الكائن PoolBall، ستكون سوداء اللون، كروية الشكل، وتحمل الرقم ثمانية. وبالمثل، يمكنك إنشاء كرة قدم أمريكية بنية اللون، كروية الشكل، وكرة خضراء كروية الشكل.
بعد ذلك، ستقوم بفحص هذه الفئات والحالات باستخدام الدالة ()isinstance، بدءًا من eight_ball:
>>> isinstance(eight_ball, PoolBall)
True
>>> isinstance(eight_ball, Ball)
True
>>> isinstance(eight_ball, AmericanFootBall)
False
انظر جيدًا إلى أول استدعاءين للدالة ()isinstance. وكما هو متوقع، تم تصنيف eight_ball على أنها PoolBall و Ball في آنٍ واحد. هذه النتيجة الثانية صحيحة لأن أي نسخة من فئة فرعية هي أيضًا نسخة من فئاتها الأصلية.
والآن انظر إلى الاستدعاء الثالث. يُرجع قيمة خاطئة لأن eight_ball، كونه كرة بلياردو، ليس كرة قدم أمريكية. من الناحية البشرية، يشتركان في نفس الأصل، لذا فهما أشبه بالأشقاء.
لقد رأيت للتو أن أي نسخة من فئة فرعية هي أيضًا نسخة من فئتها الأصلية. يمكن اعتبار هذه حالة خاصة. أما القاعدة العامة فهي أن أي نسخة من فئة فرعية هي أيضًا نسخة من جميع فئاتها الأصلية. هذا يعني أن النسخة لا تنتمي فقط إلى الفئة التي أُنشئت منها، بل أيضًا إلى فئتها الأصلية، وفئة جدها، وهكذا، وصولًا إلى أعلى التسلسل الهرمي.
لا شيء يدوم إلى الأبد، وشجرة الوراثة في بايثون ليست استثناءً. لا بدّ أن يتوقف النسب عند نقطة ما. هذه النقطة هي الفئة الأساسية للكائنات، التي تقع في قمة التسلسل الهرمي، وهي الفئة التي تُشتق منها جميع الفئات الأخرى.
ألقِ نظرة على هذا الكود:
>>> isinstance(eight_ball, object)
True
>>> isinstance(football, object)
True
>>> isinstance(ball, object)
True
>>> isinstance(object, object)
True
كما ترون، كل شيء عبارة عن نسخة من كائن – حتى الكائن نفسه.
رأيتَ سابقًا كيف أن int و float و str هي فئات تُستخدم عادةً عند التعامل مع أنواع البيانات الأساسية. وهناك نوع آخر هو bool، الذي لا يقبل إلا القيمتين True أو False. بالمناسبة، هذا هو أيضًا نوع البيانات الذي تُرجعه الدالة ()isinstance.
مثل جميع الأنواع الأخرى في بايثون، يُعدّ bool فئة. ومع ذلك، فهو أيضًا فئة فرعية من int. هذا يعني أنه بالإضافة إلى كونهما مثالين على bool، فإن كلاً من True و False هما أيضًا مثالان على int.
>>> isinstance(True, int)
True
>>> isinstance(True, bool)
True
>>> isinstance(False, int)
True
>>> isinstance(False, bool)
True
كما تلاحظ، فإنّ True و False هما مثالان على كلٍّ من النوعين int و bool. مع ذلك، فبينما يُعيد الكود int(True) القيمة 1 ويُعيد int(False) القيمة 0، فإنّ العددين الصحيحين 1 و 0 يختلفان عن النوعين المنطقيين True و False. فالنوع bool هو عدد صحيح، بينما النوع int ليس قيمة منطقية.
ما لم تكن حذرًا، فقد يتسبب هذا في مشاكل عندما تحتاج إلى التحقق من الأعداد الصحيحة غير المنطقية في المواقف التي قد توجد فيها قيم منطقية أيضًا:
>>> test_data = [10, True, False]
>>> for element in test_data:
... print("int" if isinstance(element, int) else "bool")
int
int
int
كما ترون، يتم تصنيف كل شيء على أنه عدد صحيح. أحد الحلول الممكنة هو التالي:
>>> for element in test_data:
... print("bool" if isinstance(element, bool) else "int")
'int'
'bool'
'bool'
هذه المرة، النتائج دقيقة. يُمكن استخدام طريقة بديلة وهي استخدام الدالة ()type. وكما يوحي اسمها، ستُخبرك هذه الدالة بنوع البيانات المُمرَّرة إليها.
>>> for element in test_data:
... print("bool") if type(element) is bool else print("int")
int
bool
bool
على الرغم من أن استخدام الدالة ()type يُجدي في هذه الحالة، إلا أن الغرض منها يختلف عن الغرض من الدالة ()isinstance. ستتعرف على المزيد حول هذا الموضوع لاحقًا.
قبل أن تمضي قدماً، حان الوقت لترسيخ ما تعلمته.
كيف تختلف الدالة ()isinstance عن الدالة ()type؟
تُعدّ الدالة isinstance() مثالًا واحدًا فقط من بين العديد من دوال الاستبطان التي تُمكّنك من فحص كائنات بايثون لمعرفة المزيد عنها. وكما تعلمتَ للتو، تُوفّر بايثون أيضًا الدالة type(). إذا مررتَ إليها كائنًا، فستحصل على الفئة التي ينتمي إليها هذا الكائن.
لنعد مرة أخرى إلى فئتي الكرة وكرة البلياردو اللتين أنشأتهما سابقًا. للبدء، عليك إنشاء نسخة جديدة من فئة كرة البلياردو:
>>> from balls import Ball, PoolBall
>>> eight_ball = PoolBall("black", 8)
يمكنك أن ترى من الكود أن eight_ball هي نسخة من PoolBall. يمكنك أيضًا استخدام ()type للتأكد من ذلك:
>>> type(eight_ball)
<class 'balls.PoolBall'>
>>> type(eight_ball) is PoolBall
True
كما هو متوقع، تُرجع الدالة ()type تفاصيل الفئة التي تنتمي إليها eight_ball – في هذه الحالة، فئة PoolBall.
مع ذلك، يجب توخي الحذر عند استخدامها لأنها غير مصممة لتحديد انتماء الكائن إلى فئة أساسية. على سبيل المثال، بما أن eight_ball هي PoolBall، وPoolBall هي فئة فرعية من Ball، فإن ()isinstance ستؤكد أن eight_ball هي أيضًا Ball، بينما ()type لن تفعل ذلك.
>>> isinstance(eight_ball, Ball)
True
>>> type(eight_ball) is Ball
False
بينما تؤكد الدالة )isinstance بالفعل ما تعرفه أنه صحيح، إلا أن الدالة ()type تبدو للوهلة الأولى أنها لا توافق على ذلك.
السبب في أن الدالة ()type تُرجع القيمة False هو أنها غير مصممة للتعرف على التسلسلات الهرمية للوراثة. على عكس الدالة ()isinstance، فإن ()type مصممة لفحص نسخة من الكائن وإخبارك بالفئة التي أُنشئت منها. عندما تحاول استخدام ()type للاستعلام عن أي مستوى أعلى في التسلسل الهرمي للفئات، فأنت تستخدم الأداة الخاطئة لهذه المهمة.
في السابق، استخدمتَ الدالة type() للتحقق من نوع البيانات المنطقية (bool). هذا آمن تمامًا لأن فئة bool مصممة بحيث لا يمكن اشتقاق فئات فرعية منها. بعبارة أخرى، لن يكون هناك أي فئات فرعية من bool يمكن تمريرها إلى الدالة type(). لذا، فإن استخدام type() للتحقق مما إذا كانت bool فئة فرعية من int سيكون غير مجدٍ.
ملاحظة: في الإصدارات السابقة من بايثون، كان يُعتبر استخدام الدالة
type()أبطأ من استخدام الدالةisinstance(). أما في الإصدارات الحالية، فالفرق ضئيل للغاية. بل قد تتفوق الدالةtype()أحيانًا على الدالةisinstance()بشكل طفيف، وذلك بحسب البيانات المُمررة لكل دالة. إذا كنت في موقف يُمكن فيه استخدام أيٍّ منهما، فقد يكون من المفيد إجراء اختبار زمني لكلتيهما لمعرفة أيّهما أسرع.
بعد ذلك، ستتعلم كيفية توسيع الوظائف الأساسية لـ ()isinstance.
هل يمكنك استخدام الدالة ()isinstance للتحقق من أنواع متعددة؟
إضافةً إلى إمكانية تحديد ما إذا كان الكائن ينتمي إلى فئة واحدة، يمكنك استخدام الدالة isinstance() لتحديد ما إذا كان ينتمي إلى إحدى الفئات المتعددة. وللقيام بذلك، مرّر مجموعة من الفئات. لا حاجة لإجراء استدعاءات منفصلة للدالة isinstance().
لنفترض أنك تريد تحديد ما إذا كانت البيانات عددًا صحيحًا أم عددًا عشريًا. إليك إحدى الطرق التي يمكنك اتباعها:
>>> "Number" if isinstance(3.14, (int, float)) else "Not a number"
'Number'
>>> "Number" if isinstance("3.14", (int, float)) else "Not a number"
'Not a number'
باستخدام هذا الكود، تتحقق مما إذا كانت القيمة، أولًا 3.14، ثم “3.14”، عددًا صحيحًا أم عددًا عشريًا. وللقيام بذلك، تُمرر زوجًا مرتبًا يحتوي على كلا نوعي البيانات المراد التحقق منهما كمعامل ثانٍ للدالة ()isinstance. في الحالة الأولى، يُظهر أن 3.14 عدد صحيح لأنه عدد عشري. أما في الحالة الثانية، فإن “3.14” سلسلة نصية، لذا لا يُعتبر عددًا.
يمكنك حتى تمرير مجموعة متداخلة، أي مجموعة تحتوي على مجموعات أخرى، كوسيط ثانٍ للدالة ()isinstance:
>>> "Number" if isinstance(3.14, ((int,), (float,))) else "Not a number"
'Number'
>>> "Number" if isinstance(3.14, (int, float)) else "Not a number"
'Number'
باستخدامك للصفوف المتداخلة، تكون قد أنشأتَ جزءًا من التعليمات البرمجية مكافئًا دلاليًا للمثال السابق. يحتوي المثال الأول على صف متداخل، بينما يحتوي الثاني على صف عادي، كما في السابق. الفواصل التي تحيط بـ int و float في المثال الأول ضرورية لضمان وجود الصفوف لأن كل صف يحتوي على عنصر واحد فقط. جرّب استبدال 3.14 بـ “3.14” وسترى نفس نتيجة “ليس رقمًا” كما في السابق.
يُعدّ استخدام الصفوف المتداخلة مفيدًا إذا تم إنشاء الصف المتداخل الذي يحتوي على الأنواع المراد فحصها باستخدام صفوف موجودة من مصادر مختلفة. في معظم الحالات، نادرًا ما ستستخدم الصفوف المتداخلة بهذه الطريقة.
على الرغم من شيوع تمرير أنواع متعددة إلى دالة ()isinstance، يمكنك أيضًا استخدام تعبير نوع الاتحاد. يتيح لك هذا تجميع أنواع بيانات متعددة مفصولة بمعامل OR الثنائي (|). عند تمرير نوع لاختباره إلى دالة ()isinstance، ستُرجع الدالة القيمة True إذا كان هذا النوع مطابقًا لأي من الأنواع المُعرَّفة في الاتحاد.
بدلاً من تمرير المجموعة (int, float) إلى الدالة ()isinstance، يمكنك القيام بما يلي:
>>> "Number" if isinstance(3.14, int | float) else "Not a number"
'Number'
>>> "Number" if isinstance("3.14", int | float) else "Not a number"
'Not a number'
في كلا المثالين، استبدلتَ المجموعة (int, float) السابقة بتعبير من نوع اتحاد int | float. وكما هو متوقع، فإن النتائج متطابقة.
ملاحظة: أنواع البيانات التي تدعمها الدالة ()isinstance هي فقط أنواع الفئات المفردة، ومجموعات أنواع الفئات، ومجموعات أنواع الفئات المتداخلة، وتعبيرات أنواع الاتحاد. إذا حاولت استخدام قائمة أو قاموس أو أي نوع آخر، فستفشل الدالة ()isinstance. تقبل دوال الاستبطان الأخرى، مثل ()issubclass، المجموعات فقط، مما يضمن التناسق.
اتخذ غيدو فان روسوم خياراً تصميمياً يسمح فقط باستخدام الصفوف لعدة أسباب:
1 تُعتبر الصفوف غير قابلة للتغيير، أي لا يمكن تعديلها. وهذا يضمن لك أن مجموعة الأنواع التي تُمررها لا يمكن تعديلها بواسطة أي جزء آخر من برنامجك.
2 لم يُصمم تطبيق دالة
isinstance()للتعامل مع عدد كبير من العناصر. عند استخدامك للصفوف، يكون ذلك بسبب محدودية عدد عناصرها، بينما صُممت القوائم لتنمو. تمرير عدد كبير من الأنواع إلىisinstance()سيؤدي إلى ضعف أدائها.
بعد ذلك، سترى كيف تعمل الدالة ()isinstance مع الفئات الأساسية المجردة.
كيف يمكنك استخدام الدالة ()isinstance مع الفئات الأساسية المجردة؟
تعلمتَ سابقًا كيف تُحدد الدالة isinstance() ما إذا كان الكائن نسخةً من فئةٍ ما أو من إحدى فئاتها الأصلية. كما تعلم أن إنشاء الفئات الفرعية يُجنّبك إعادة اختراع العجلة عند الحاجة إلى شيءٍ مشابهٍ لإحدى فئاتك الحالية. في بعض الحالات، لن تحتاج أبدًا إلى استخدام نسخٍ من هذه الفئات الأصلية. وهنا قد تجد الفئات الأساسية المجردة مفيدة.
إنشاء فئة أساسية مجردة
بالعودة إلى مثال الكرة، لكل كرة استخدام محدد. عندما تلعب بالكرة في حياتك اليومية، فأنت في الواقع تلعب بكرة قدم، وكرة بلياردو، وهكذا. ولأن هذه الكرات في العالم الحقيقي ليست حالات مباشرة من فئة الكرة، بل حالات من فئاتها الفرعية، فإن فئة الكرة تُعدّ مرشحًا جيدًا للنظر فيها كفئة أساسية مجردة. في المقابل، تُسمى الفئات الفرعية المصممة للتنفيذ فئات ملموسة.
قبل أن تتمكن من رؤية كيفية تعامل الدالة ()isinstance مع الفئات الفرعية، ستعيد تصميم التسلسل الهرمي السابق لجعل فئة الكرة مجردة:
from abc import ABC, abstractmethod
class Ball(ABC):
def __init__(self, color, shape):
self.color = color
self.shape = shape
@abstractmethod
def get_state(self):
pass
class PoolBall(Ball):
def __init__(self, color, number):
super().__init__(color, shape="sphere")
self.number = number
def get_state(self):
print(f"Color {self.color}, Number {self.number}, Shape {self.shape}")
class AmericanFootBall(Ball):
def __init__(self, color):
super().__init__(color, shape="prolate spheroid")
def get_state(self):
print(f"Color {self.color}, Shape {self.shape}")
هنا، ستستخدم وحدة abc المُسماة بشكل مناسب لمساعدتك في إنشاء فئات أساسية مجردة. لجعل فئة Ball فئةً مجردة، يجب أن ترث من فئة ABC، التي تستوردها من وحدة abc في مكتبة بايثون القياسية. لذلك، يمكنك جعل Ball فئةً أساسية مجردة باستخدام الصيغة Ball(ABC) عند تعريف الفئة.
من أبرز سمات الفئات المجردة وجود الدوال المجردة. تعمل هذه الدوال كعناصر نائبة للدوال التي ستحتاجها الفئات الفرعية. وعادةً لا تحتوي على أي تفاصيل تنفيذية لأن التنفيذ خاص بكل فئة فرعية.
عند إنشاء فئة فرعية قابلة للإنشاء من فئة أساسية مجردة، عادةً ما تُعرّف تطبيقًا لكل طريقة مجردة في الفئة الفرعية. إذا لم تفعل ذلك، فستُثير خطأً من نوع TypeError عند محاولة إنشاء نسخ من هذه الفئات الفرعية لأنها ستظل مجردة.
لتعيين دالة كدالة مجردة، يجب زخرفتها باستخدام مُزخرف @abstractmethod. هذا المُزيّن مُقدّم لك أيضًا من قِبل abc.
في ملف balls_v2.py، تقوم أولاً بتعريف نسخة مجردة جديدة من فئة الكرة (Ball). وكما هو الحال في النسخة السابقة، فإن دالة .__init__() الخاصة بها تُهيئ القيم الأولية لسمات البيانات .color و .shape تمامًا كما فعلت في النسخة الأصلية.
أضفتَ أيضًا دالةً مجردةً جديدةً باسم .get_state(). يجب أن يظهر تنفيذ هذه الدالة في جميع الفئات الفرعية للفئة Ball. مع ذلك، في صيغتها المجردة، تستخدم عبارة pass بدلًا من التنفيذ. هذا يُجنّبك خطأ المسافة البادئة الذي قد يظهر لك إذا حاولتَ إنشاء دالة بدون أي كود في جسمها.
كما تقوم بإعادة تعريف فئتي PoolBall و AmericanFootBall اللتين أنشأتهما سابقًا. هذه المرة، أنت مُلزم بتوفير دالة ()get_state في كل منهما. تذكر أنه بدون هذه الدالة، لن تتمكن من إنشاء نسخ منهما.
كإجراء سريع للتحقق مما إذا كان التجريد يعمل، حاول إنشاء مثيل جديد من نوع Ball:
>>> from balls_v2 import Ball
>>> test_ball = Ball("white", "sphere")
Traceback (most recent call last):
...
TypeError: Can't instantiate abstract class Ball without an
⮑ Implementation for abstract method 'get_state'
كما ترى، فشلت محاولتك لإنشاء نسخة من Ball فشلاً ذريعاً. مع ذلك، لا يزال Ball مفيداً للدالة ()isinstance، كما سترى لاحقاً.
ما الذي يجب أن تتعلمه بعد ذلك؟
تهانينا على إتمام هذا الدرس! نأمل أن يكون قد أثار اهتمامك باستخدام دالة ()isinstance لإجراء فحص داخلي على الكائنات. سيساعدك هذا على فهم الكائنات التي يستخدمها برنامجك والعلاقات بينها بشكل أفضل.
| الدالة | الغرض |
|---|---|
dir(object) | يُعيد قائمة بخصائص وأساليب الكائن المُعطى. |
hasattr(object, name) | يتحقق مما إذا كان للكائن سمة بالاسم المحدد. |
id(object) | يُرجع عددًا صحيحًا يمثل الهوية الفريدة للكائن. |
issubclass(class, classinfo) | يتحقق مما إذا كانت الفئة فئة فرعية من أي فئة في معلومات الفئة. |
ليست هذه هي الدوال المدمجة الوحيدة أو أدوات الفحص المتاحة. يمكنك العثور على المزيد في وحدة inspect الخاصة بلغة بايثون.
شرح هذا الدرس كيفية استخدام دالة ()isinstance للتحقق من نوع الكائن. الآن لديك فهمٌ جيد لكيفية استخدامها، بالإضافة إلى بعض النقاط التي قد تُسبب مشاكل.
على الرغم من أنك قد اكتسبت نظرة عامة جيدة حول وظائف الدالة ()isinstance، إلا أننا نشجعك بشدة على ممارسة ما تعلمته واستكشاف مفهوم الاستبطان بشكل أعمق. سيساعدك فهم ما تكشفه هذه الدوال على فهم أفضل لكيفية عمل الكود الخاص بك والكائنات التي تتعامل معها.
اكتشاف المزيد من بايثون العربي
اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.