كم عدد أسطر CPython اللازمة لتنفيذ a + b في بايثون؟

في هذه المقالة، سنتعمق أكثر في التفاصيل، وننظر إلى ما يحدث بالضبط خلف الكواليس في وقت تشغيل بايثون لتنفيذ شيء بسيط مثل “a + b”. بعبارة أخرى، سنتعلم تفاصيل التنفيذ وراء الأنواع والمشغلات والإرسال الديناميكي في CPython.

لاحظ أنه على الرغم من أننا سنتابع تنفيذ الإرسال الديناميكي لمشغل معين، فإن الأفكار نفسها تنطبق على جميع المشغلات التي يدعمها CPython. وبالتالي، يمكنك باستخدام هذه المعرفة تنفيذ مشغلك الجديد أو نوعك الجديد.

تصميم عالي المستوى للإرسال الديناميكي في CPython

عندما نكتب كود مثل a + b في بايثون، فإن أنواع a وb تحدد السلوك الدقيق لعملية +. كل نوع في بايثون له تنفيذه الخاص لمشغل + (إذا كان هذا النوع يدعم +) ويحدد مفسّر بايثون التنفيذ الصحيح الذي يجب استدعاؤه بناءً على نوع المتغيرات. تسمى هذه العملية بالكامل الإرسال الديناميكي في لغات البرمجة. يقدم الرسم التخطيطي التالي نظرة عامة عالية المستوى حول كيفية عملها في CPython:

تدفق عالي المستوى لتنفيذ المشغل في CPython VM

دعونا نناقش الأجزاء المختلفة بشكل موجز:

  • يتم تجميع كود بايثون إلى كود ثنائي، والذي يتم تنفيذه بواسطة آلة افتراضية (VM) قائمة على المكدس في CPython. تكون تعليمة BINARY_OP مسؤولة عن تنفيذ عملية + على المتعاملين، a وb.
  • لا تعرف الآلة الافتراضية نفسها كيفية تنفيذ الأمر + على كائنين. وبدلاً من ذلك، تقوم بتفويض هذه المهمة إلى واجهة كائن مجردة للتعامل معها.
  • تحدد واجهة الكائن المجردة في CPython واجهة تدعم جميع عمليات مستوى الكائن الشائعة في CPython. وهذا يمنح الآلة الافتراضية طريقة موحدة واحدة لتنفيذ جميع المشغلات دون معرفة أي تفاصيل تنفيذ لنظام الكائن. ترسل الواجهة المجردة التنفيذ إلى التنفيذ الملموس داخل الأنواع عبر البحث في جدول مؤشر الوظيفة في رأس الكائن (المزيد حول هذا لاحقًا).

سنبدأ بالنظر في كيفية تنفيذ الأنواع المختلفة للمشغلين المختلفين، ثم سننظر إلى واجهة الكائن المجرد ونرى كيف تستدعي هذه التنفيذات الملموسة، وأخيرًا، سنرى كيف يتكامل CPython VM مع واجهة الكائن المجرد.

تشريح بنية PyTypeObject

بنية PyTypeObject هي الكتلة الثانية من نظام كائنات CPython (الأولى هي PyObject). وهي تحتوي على معلومات النوع وقت التشغيل حول الكائن. قبل أن نلقي نظرة على الإرسال الديناميكي في CPython، يجب أن نفهم أولاً ما بداخل PyTypeObject.

لكن أولاً، دعنا نراجع ونرى تعريف PyObject، وهو المكان الذي يظهر فيه PyTypeObject:

تعريف بنية PyObject

بالإضافة إلى ذلك، يتضمن كل تعريف نوع PyObject كحقل أول كرأس. على سبيل المثال، هذا هو تعريف النوع العائم:

تعريف نوع float في CPython

يعني هذا أنه يمكن تحويل كل كائن من هذه الكائنات إلى PyObject ولأن PyObject يتضمن مؤشرًا إلى PyTypeObject، فإن وقت تشغيل CPython يحتوي على جميع المعلومات المتعلقة بالنوع حول الكائن المتاحة له في جميع الأوقات.

الآن، دعنا نلقي نظرة على PyTypeObject. إنه كائن كبير جدًا يحتوي على عشرات الحقول. يوضح الشكل التالي تعريفه الكامل:

تعريف بنية PyTypeObject

يخزن هيكل PyTypeObject تفاصيل نوع وقت التشغيل حول الكائن، مثل اسم النوع وحجم النوع والوظائف لتخصيص وإلغاء تخصيص كائن من هذا النوع.

بصرف النظر عن ذلك، فإنه يخزن أيضًا جداول مؤشرات الوظائف لدعم سلوكيات مختلفة خاصة بأنواع معينة. على سبيل المثال، يعد الحقل tp_as_number أحد هذه الجداول. وهو مؤشر إلى كائن من نوع PyNumberMethods يحدد جدول مؤشرات الوظائف للعمليات العددية.

نظرًا لأننا مهتمون بفهم كيفية تنفيذ CPython لعامل الجمع الثنائي (+)، فسوف نقترب وننظر إلى ما بداخل PyNumberMethods. يوضح الشكل التالي تعريفه:

انظر داخل هيكل PyNumberMethods. المربع الموجود على الجانب الأيسر مُكبر في تعريف PyTypeObject، مع إبراز حقل PyNumberMethods. المربع الموجود على الجانب الأيمن عبارة عن تعريف جزئي لـ PyNumberMethods

يحتاج كل تنفيذ نوع في CPython إلى إنشاء مثيل لبنية PyNumberMethods وملئها بمؤشرات إلى الوظائف التي ينفذها لدعم المشغلات الرقمية. إذا كان النوع لا يدعم العمليات الرقمية، فيمكنه ببساطة تعيين حقل tp_as_number في PyTypeObject إلى NULL، مما يخبر وقت تشغيل CPython أن هذا الكائن لا يدعم أيًا من هذه العمليات.

بعد ذلك، كمثال ملموس، دعنا نرى كيف يقوم نوع float بتنفيذ هذه الوظائف ثم يقوم بإنشاء PyTypeObject عند إنشاء كائن float جديد.

إنشاء أنواع Float باستخدام PyNumberMethods

يوضح الشكل التالي الكود من Objects/floatobject.c والذي يحتوي على تنفيذ نوع float في CPython.

كيف ينفذ نوع float المشغلات الرقمية ويملأ جدول مؤشر الوظيفة في مثيله من PyTypeObject، المسمى PyFloat_Type

دعونا نحللها:

  • يُظهر المربع الموجود على الجانب الأيسر الوظائف التي تنفذ عمليات الجمع والطرح والضرب.
  • بعد ذلك، يعرض المربع الأوسط مثيلًا لبنية PyNumberMethods (تسمى float_as_number) لنوع البيانات float. لاحظ كيف تتضمن مؤشرات الوظيفة لوظائف الجمع والضرب والطرح.
  • يُظهِر المربع الموجود في أقصى اليمين مثالاً لـ PyTypeObject لإنشاء كائنات من نوع float. لاحظ كيف يتضمن مؤشرًا إلى كائن float_as_number.

ويتم تضمين مؤشر إلى float_as_number في رأس كل كائن عائم (أي كقيمة لحقل ob_type في PyObject). يوضح الشكل التالي الدالة PyFloat_FromDouble، التي تنشئ كائنات جديدة من النوع العائم، وتستخدم float_as_number لتهيئة رأس الكائن.

كيف يتم إنشاء مثيل من النوع العائم. لاحظ كيف يمرر المؤشر إلى PyFloat_Type في استدعاء _PyObject_Init لتعيين نوعه.

الشكل مفصل للغاية وموضح، لذا لن أقضي المزيد من الوقت عليه. ولكن هذا هو الكود الذي يتم تنفيذه عندما تكتب “a = 3.14” في كود بايثون.

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

في هذه المرحلة، نفهم أن كل نوع ينفذ عوامل تشغيل مختلفة كوظائف ويستخدمها لملء جدول مؤشر الوظيفة في PyTypeObject، والذي يتم تضمينه في رأس الكائن. لقد رأينا كيف يعمل هذا المخطط في تنفيذ النوع العائم.

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

واجهة الكائن المجرد في CPython

يحدد CPython واجهة كائن مجردة لتوحيد الوصول إلى تنفيذات النوع الملموس. وهذا يحافظ على نظافة كود VM لأنه ببساطة يفوض تنفيذ عامل إلى هذه الواجهة.

تم تعريف هذه الواجهة المجردة في ملف Include/abstract.h. ويُظهر الشكل التالي الدوال الرقمية المعلنة فيه:

يعلن ملف abstract.h header عن واجهة الكائن المجرد، والتي تتضمن وظائف لجميع العمليات المشتركة على مستوى الكائن في CPython. يوضح هذا الشكل قائمة جزئية للعمليات العددية كما تم إعلانها في abstract.h

الآن، ملف abstract.h هو ملف رأس، لذا فهو يعلن فقط عن النماذج الأولية لهذه الوظائف. توجد تنفيذات هذه الوظائف في الملف Objects/abstract.c. سنركز فقط على تنفيذ وظيفة PyNumber_Add فيه، والتي يتم استدعاؤها بواسطة VM للتعامل مع تنفيذ عامل +. يوضح الشكل التالي الكود الخاص به، وتشرح التعليقات التوضيحية ما يحدث:

يحتوي ملف abstract.c على تنفيذات للوظائف المعلنة في ملف abstrac.h. يوضح هذا الشكل تنفيذ وظيفة PyNumber_Add

يتم دعم عملية + بواسطة فئتين من أنواع البيانات في بايثون: الأنواع الرقمية (int، float، complex وما إلى ذلك) وأنواع التسلسل (list، tuple، وما إلى ذلك).

تحاول دالة PyNumber_Add أولاً استدعاء تنفيذ الإضافة الثنائية على الوسائط. إذا كانت هذه الأنواع لا تدعم الإضافة الثنائية، فإنها تحاول التحقق مما إذا كانت هذه الأنواع أنواع تسلسل، وإذا كانت كذلك، فإنها تحاول استدعاء دالة التجميع عليها.

دعنا نركز على الأنواع الرقمية هنا. بالنسبة للأنواع الرقمية، تستدعي الدالة PyNumber_Add الماكرو BINARY_OP1، الذي يستدعي ببساطة الدالة binary_op1. يوضح الشكل التالي  binary_op1:

بقية تنفيذ PyNumber_Add في abstract.c

تؤدي الوظيفة الكثير من الأشياء، لكن التعليقات التوضيحية تشرح كل شيء. والنقطة الأساسية هنا هي أن abstract.c تقوم ببساطة بالبحث عن مؤشر الوظيفة في جدول التوابع الموجود في رأس الكائن، ثم تستدعي تلك الوظيفة.

ربط تنفيذ المشغل بواجهة الكائن المجرد في CPython VM

هذا هو الفصل الأخير حيث تقوم آلة CPython الافتراضية بدمج تنفيذ المشغل مع واجهة الكائن المجرد.

يوضح الشكل التالي دالة بايثون بسيطة وتعليمات البايت كود الخاصة بها:

كيف تقوم الآلة الافتراضية المستندة إلى مكدس CPython بتنفيذ التعليمات

تعليمات البايت كود التي سنركز عليها هي BINARY_OP. توضح الصورة التالية كيفية التعامل معها بواسطة الآلة الافتراضية:

تنفيذ تعليمة BINARY_OP في CPython VM

فلنلق نظرة على هذا الكود لأن هذا هو المكان الذي تفوض فيه الآلة الافتراضية إلى abstract.c.

في الكود أعلاه، نرى هذا السطر من الكود:

res = binary_ops[oparg](lhs, rhs);

يقوم هذا الكود بالبحث عن مؤشر الدالة في جدول يسمى binary_ops، باستخدام التعليمات البرمجية للعملية الثنائية كمؤشر، واستدعاء هذه الدالة. دعنا نلقي نظرة على هذا الجدول المحدد في الملف ceval.c (حيث يتم تنفيذ معظم التعليمات البرمجية لتنفيذ الآلة الافتراضية).

جدول binary_ops في CPython VM الذي يفهرس وظائف المشغل كما هو محدد في abstract.c باستخدام المشغلات الثنائية كمؤشر

يشير كل مؤشر دالة في جدول binary_ops إلى دالة تم تنفيذها في Objects/abstract.c. في القسم السابق، رأينا بالفعل تعريف PyNumber_Add في abstract.c، وفهمنا كيف يقوم بالإرسال الديناميكي إلى التنفيذ الصحيح للمشغل بناءً على أنواع المتغيرات.

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

كانت هذه جولة قصيرة لجميع أكواد CPython التي يتم تنفيذها عند تنفيذ شيء بسيط مثل “a + b” في كود بايثون. على الرغم من أن هذا قد يكون أمرًا صعبًا للغاية، إلا أنه ليس معقدًا للغاية إذا كنت تفهم مؤشرات الوظيفة.

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


اكتشاف المزيد من بايثون العربي

اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

Scroll to Top

اكتشاف المزيد من بايثون العربي

اشترك الآن للاستمرار في القراءة والحصول على حق الوصول إلى الأرشيف الكامل.

Continue reading