ننشر اليوم الجزء الثاني من ترجمة كيفية تنظيم Dropbox للتحكم في أنواع عدة ملايين سطر من شفرة Python.

→
اقرأ الجزء الأولالدعم الرسمي للنوع (PEP 484)
أجرينا أول تجربة جادة مع mypy على Dropbox خلال Hack Week 2014. Hack Week هو حدث أقيم بواسطة Dropbox لمدة أسبوع واحد. في هذا الوقت ، يمكن للموظفين العمل على أي شيء! بدأت بعض مشاريع التكنولوجيا الأكثر شهرة في Dropbox في أحداث مماثلة. نتيجة لهذه التجربة ، توصلنا إلى استنتاج مفاده أن mypy تبدو واعدة ، على الرغم من أن هذا المشروع لم يكن جاهزًا للاستخدام بعد على نطاق واسع.
في ذلك الوقت ، كانت فكرة توحيد أنظمة التلميح لأنواع بايثون في الجو. كما قلت ، بدءًا من Python 3.0 ، يمكنك استخدام التعليقات التوضيحية للنوع للوظائف ، لكن هذه كانت مجرد تعبيرات تعسفية ، دون أي بناء جملة ودلالات محددة. أثناء تنفيذ البرنامج ، تم ببساطة تجاهل هذه التعليقات التوضيحية. بعد Hack Week ، بدأنا العمل على توحيد دلالات. أدى هذا العمل إلى ظهور
PEP 484 (كويدو فان روسوم ولوكاس لانجا وتعاونت في هذه الوثيقة).
يمكن رؤية دوافعنا من جانبين. أولاً ، كنا نأمل أن يتبع نظام Python البيئي بأكمله طريقة عامة لاستخدام تلميحات الكتابة (تلميحات الكتابة هي مصطلح يستخدم في Python كتناظرية لـ "شروح الكتابة"). هذا ، بالنظر إلى المخاطر المحتملة ، سيكون أفضل من استخدام العديد من المناهج غير المتوافقة. ثانياً ، أردنا مناقشة آليات التعليق التوضيحي بشكل مفتوح مع العديد من أعضاء مجتمع بيثون. في جزء منه ، كانت هذه الرغبة تمليها حقيقة أننا لا نريد أن ننظر مثل "المرتدين" من الأفكار الأساسية للغة في عيون الجماهير العريضة من المبرمجين بيثون. إنها لغة مكتوبة بشكل حيوي تُعرف باسم "كتابة البط". في المجتمع ، في البداية ، لم يساعد الموقف المريب إلى حد ما تجاه فكرة الكتابة الثابتة في الظهور. لكن هذا الموقف أضعف في النهاية - بعد أن أصبح من الواضح أن الكتابة الثابتة لم يكن مخططًا لها أن تكون إلزامية (وبعد أن أدرك الناس أنها كانت مفيدة حقًا).
كان بناء الجملة الناتج عن تلميحات الكتابة مشابهاً للغاية للنمط الوحيد المدعوم في ذلك الوقت. جاء PEP 484 مع Python 3.5 في عام 2015. لم تعد Python لغة تدعم الكتابة الديناميكية فقط. أحب أن أفكر في هذا الحدث باعتباره علامة فارقة في تاريخ بيثون.
بداية الهجرة
في نهاية عام 2015 ، تم إنشاء فريق من ثلاثة أشخاص في دروببوإكس للعمل على mypy. وشملت غيدو فان روسوم وغريغ برايس وديفيد فيشر. منذ تلك اللحظة بدأ الوضع يتطور بسرعة كبيرة. وكان أول عقبة أمام نمو mypy الأداء. كما أشرت بالفعل أعلاه ، في الفترة المبكرة من تطوير المشروع ، فكرت في ترجمة تنفيذ mypy إلى C ، ولكن تم حذف هذه الفكرة من القوائم حتى الآن. نحن متمسكون بحقيقة أننا استخدمنا مترجم CPython لبدء النظام ، وهو ليس سريعًا بما يكفي لأدوات مثل mypy. (لم يساعدنا مشروع PyPy ، وهو تطبيق بديل لبيثون مع مترجم JIT ، كذلك.)
لحسن الحظ ، هنا بعض التحسينات الحسابية جاءت لمساعدتنا. أول "مسرع" قوي كان تنفيذ التحقق التدريجي. كانت فكرة هذا التحسين بسيطة: إذا لم تتغير جميع تبعيات الوحدة النمطية منذ إطلاق mypy السابق ، فيمكننا حينئذٍ استخدام البيانات المخزنة مؤقتًا أثناء الجلسة السابقة أثناء العمل مع التبعيات. كل ما كان علينا القيام به هو كتابة التدقيق في الملفات المعدلة وفي تلك الملفات التي تعتمد عليها. ذهب Mypy إلى أبعد من ذلك قليلاً: إذا لم تتغير الواجهة الخارجية للوحدة النمطية - اعتقد mypy أنه لا ينبغي التحقق من الوحدات النمطية الأخرى التي تستورد هذه الوحدة مرة أخرى.
ساعدنا التحقق التزايدي بشكل كبير في التعليق على الكميات الكبيرة من الكود الموجود. والحقيقة هي أن هذه العملية عادة ما تنطوي على العديد من عمليات التكرار من mypy ، حيث يتم إضافة التعليقات التوضيحية تدريجيا إلى الكود ويتم تحسينها تدريجيا. كان الإطلاق الأول لل mypy بطيئًا للغاية ، لأنه كان بحاجة إلى التحقق من الكثير من التبعيات أثناء تنفيذه. ثم ، من أجل تحسين الوضع ، قمنا بتنفيذ آلية التخزين المؤقت عن بعد. إذا اكتشف mypy أن ذاكرة التخزين المؤقت المحلية قد تكون قديمة ، فإنه يقوم بتنزيل لقطة ذاكرة التخزين المؤقت الحالية لقاعدة التعليمات البرمجية بأكملها من مستودع مركزي. ثم يقوم بإجراء فحص تدريجي باستخدام هذه اللقطة. هذه خطوة كبيرة أخرى دفعتنا نحو زيادة إنتاجية الغموض.
كانت هذه فترة التقديم السريع والطبيعي لنظام فحص نوع Dropbox. بحلول نهاية عام 2016 ، كان لدينا بالفعل حوالي 420،000 سطرًا من شفرة Python مع تعليقات توضيحية للنوع. كان العديد من المستخدمين متحمسين لفحص النوع. وقد تم استخدام دروببوإكس mypy من قبل فرق التطوير أكثر وأكثر.
بدا كل شيء جيدًا بعد ذلك ، لكن ما زال أمامنا الكثير لنفعله. بدأنا في إجراء استطلاعات دورية للمستخدمين الداخليين من أجل تحديد مجالات المشكلات في المشروع وفهم المشكلات التي يجب معالجتها أولاً (يتم استخدام هذه الممارسة في الشركة اليوم). الأهم ، كما أصبح واضحا ، مهمتان. الأول - كنت بحاجة إلى مزيد من تغطية الرمز مع الأنواع ، والثاني - كان من الضروري أن يعمل mypy بشكل أسرع. كان من الواضح تمامًا أن عملنا على تسريع عملية mypy وتنفيذها في مشاريع الشركة ما زال بعيدًا عن الاكتمال. لقد أدركنا تمامًا أهمية هاتين المهمتين.
المزيد من الأداء!
عمليات الفحص التزايدية تسارعت mypy ، ولكن هذه الأداة لا تزال غير بسرعة كافية. استمرت العديد من الشيكات الإضافية حوالي دقيقة واحدة. وكان السبب في ذلك الواردات الدورية. ربما هذا لن يفاجئ أي شخص يعمل مع أكواد كبيرة كتب في بيثون. كان لدينا مجموعات من مئات الوحدات ، كل منها استوردت جميع الوحدات الأخرى بشكل غير مباشر. إذا تبين أن أي ملف في دورة الاستيراد قد تم تعديله ، فقد تعين على mypy معالجة جميع الملفات المدرجة في هذه الدورة ، وأيضًا في الغالب أيضًا أي وحدات نمطية تستورد وحدات من هذه الدورة. إحدى هذه الدورات كانت "سلسلة التبعيات" سيئة السمعة ، والتي تسببت في الكثير من المتاعب في Dropbox. بمجرد احتواء هذا الهيكل على عدة مئات من الوحدات ، بينما تم استيراده ، بشكل مباشر أو غير مباشر ، الكثير من الاختبارات ، تم استخدامه أيضًا في رمز الإنتاج.
لقد نظرنا في إمكانية "كشف" التبعيات الدورية ، لكن لم يكن لدينا الموارد للقيام بذلك. كان هناك الكثير من التعليمات البرمجية التي لم نكن نعرفها. نتيجة لذلك ، اتخذنا نهجا بديلا. قررنا جعل mypy يعمل بسرعة حتى لو كانت هناك "كرات تبعية". أنجزنا هذا مع الشيطان الغامض. البرنامج الخفي عبارة عن عملية خادم تقوم بتنفيذ ميزتين شيقتين. أولاً ، يحتفظ في معلومات الذاكرة حول قاعدة التعليمات البرمجية بالكامل. هذا يعني أنه في كل مرة تقوم فيها بتشغيل mypy ، لن تضطر إلى تنزيل البيانات المخزنة مؤقتًا المتعلقة بآلاف التبعيات المستوردة. ثانياً ، يحلل بعناية ، على مستوى الوحدات الهيكلية الصغيرة ، العلاقات بين الوظائف والكيانات الأخرى. على سبيل المثال ، إذا كانت الدالة
foo
تستدعي
bar
الوظائف ، فهناك اعتماد
foo
على
bar
. عندما يتم تغيير الملف ، يقوم البرنامج الخفي أولاً بمعزل عن الملف الذي تم تغييره فقط. ثم ينظر إلى التغييرات التي يتم إجراؤها على هذا الملف والتي تكون مرئية من الخارج ، مثل تواقيع الوظيفة التي تم تغييرها. يستخدم البرنامج الخفي معلومات استيراد مفصلة فقط للتحقق من الوظائف التي تستخدم الوظيفة التي تم تغييرها حقًا. عادة ، مع هذا النهج ، لا بد من التحقق من عدد قليل جدا من الوظائف.
لم يكن تنفيذ كل هذا أمرًا سهلاً ، نظرًا لأن التنفيذ الأصلي لـ mypy كان يركز بشدة على معالجة ملف واحد في كل مرة. كان علينا أن نتعامل مع العديد من المواقف الحدودية ، التي تتطلب حدوثها عمليات تفتيش متكررة في تلك الحالات عندما يتغير شيء ما في الكود. على سبيل المثال ، يحدث هذا عندما يتم تعيين فئة أساسية جديدة لفئة. بعد أن قمنا بما أردنا ، تمكنا من تقليل وقت تنفيذ معظم عمليات الفحص الإضافية إلى بضع ثوانٍ. بدا لنا نصرا كبيرا.
المزيد من الأداء!
جنبا إلى جنب مع التخزين المؤقت عن بعد ، الذي وصفته أعلاه ، حل برنامج mypy daemon بالكامل تقريبا للمشاكل التي تنشأ عندما يقوم المبرمج في كثير من الأحيان بإجراء فحص النوع ، وإجراء تغييرات على عدد صغير من الملفات. ومع ذلك ، فإن أداء النظام في البديل الأقل ملائمة لاستخدامه كان لا يزال بعيدًا عن المستوى الأمثل. قد تستغرق البداية النظيفة للطفح أكثر من 15 دقيقة. وكان أكثر بكثير مما نود. كل أسبوع ، تزداد الحالة سوءًا ، حيث استمر المبرمجون في كتابة التعليمات البرمجية الجديدة وإضافة التعليقات التوضيحية إلى التعليمات البرمجية الموجودة. كان مستخدمونا لا يزالون يتوقون إلى مزيد من الأداء ، لكننا كنا سعداء أن نكون مستعدين للقاءهم.
قررنا العودة إلى واحدة من أقرب أفكاري فيما يتعلق mypy. وهي تحويل كود بايثون إلى كود سي. إن التجارب مع Cython (هذا نظام يسمح لك بترجمة شفرة Python إلى رمز C) لم تمنحنا أي تسارع مرئي ، لذلك قررنا إحياء فكرة كتابة مترجمنا الخاص. نظرًا لأن قاعدة بيانات mypy codebase (المكتوبة في Python) تتضمن بالفعل جميع التعليقات التوضيحية اللازمة للنوع ، فإن محاولة استخدام هذه التعليقات التوضيحية لتسريع النظام تبدو جديرة بالاهتمام. أنا بسرعة إنشاء نموذج أولي لاختبار هذه الفكرة. لقد أظهر في مختلف المعايير الصغيرة أكثر من 10 أضعاف زيادة في الإنتاجية. كانت فكرتنا هي ترجمة وحدات Python إلى وحدات C باستخدام أدوات Cython ، وتحويل التعليقات التوضيحية إلى اختبارات نوع تم إجراؤها في وقت التشغيل (عادةً ما يتم تجاهل التعليقات التوضيحية في وقت التشغيل ولا يتم استخدامها إلا عن طريق أنظمة فحص الكتابة ). لقد خططنا بالفعل لترجمة تطبيق mypy من Python إلى لغة تم إنشاؤها بشكل ثابت ، والتي ستبدو (وفي معظمها ، تعمل) تمامًا مثل Python. (أصبح هذا النوع من الهجرة عبر اللغات شيئًا من تقاليد مشروع mypy. وقد تمت كتابة التطبيق الأولي لـ mypy في Alore ، ثم كان هناك مزيج هجائي من Java و Python).
كان التركيز على واجهة برمجة تطبيقات امتداد CPython هو المفتاح لعدم فقدان قدرات إدارة المشروع. لم نكن بحاجة إلى تطبيق آلة افتراضية أو أي مكتبات تحتاجها mypy. بالإضافة إلى ذلك ، سيظل نظام Python بالكامل متاحًا لنا ، وستكون جميع الأدوات (مثل pytest) متاحة. هذا يعني أنه يمكننا الاستمرار في استخدام شفرة Python التي تم تفسيرها أثناء التطوير ، مما سيتيح لنا مواصلة العمل باستخدام نظام سريع جدًا لإجراء تغييرات على الرمز واختباره ، بدلاً من انتظار تجميع الشفرة. بدا الأمر كما لو كنا قادرين بشكل رائع ، إذا جاز التعبير ، على الجلوس على كرسيين ، وقد أحببنا ذلك.
المترجم ، الذي أطلقنا عليه اسم mypyc (لأنه يستخدم mypy كواجهة أمامية لتحليل النوع) ، تبين أنه مشروع ناجح للغاية. الكل في الكل ، حققنا حوالي 4X تسريع أسرع من يدير mypy المتكررة دون التخزين المؤقت. استغرق تطوير قلب مشروع mypyc حوالي 4 أشهر تقويمية من فريق صغير ضم مايكل سوليفان وإيفان ليفكيفسكي وهيو هان وأنا. كان مقدار العمل أقل طموحًا مما كان مطلوبًا لإعادة كتابة mypy ، على سبيل المثال ، في C ++ أو Go. واضطررنا إلى إجراء تغييرات على المشروع أقل بكثير مما كان يتعين علينا القيام به عند إعادة كتابته بلغة أخرى. كما كنا نأمل أن نتمكن من رفع مستوى mypyc إلى مستوى يمكن لمبرمجي Dropbox الآخرين استخدامه لتجميع وتسريع الكود الخاص بهم.
لتحقيق هذا المستوى من الأداء ، كان علينا تطبيق بعض الحلول الهندسية المثيرة للاهتمام. لذلك ، يمكن للمترجم تسريع العديد من العمليات باستخدام تصميمات C منخفضة المستوى سريعة ، على سبيل المثال ، يتم ترجمة استدعاء دالة مترجمة إلى استدعاء دالة C. ويتم إجراء مثل هذه المكالمة أسرع بكثير من استدعاء وظيفة مترجمة. لا تزال بعض العمليات ، مثل عمليات البحث في القاموس ، تتلخص في استخدام مكالمات C-API العادية من CPython ، والتي تبين بعد التجميع أنها أسرع قليلاً فقط. تمكنا من التخلص من العبء الإضافي على النظام الذي أنشأه التفسير ، لكن في هذه الحالة لم يحقق سوى مكسب صغير من حيث الأداء.
لتحديد أكثر العمليات "البطيئة" شيوعًا ، أجرينا عملية تعريف للشفرة. مُسلحين بالبيانات التي تم الحصول عليها ، حاولنا إما تعديل mypyc بحيث يؤدي إلى إنشاء كود C أسرع لهذه العمليات ، أو إعادة كتابة رمز Python المقابل باستخدام عمليات أسرع (وأحيانًا لم يكن لدينا ببساطة حل بسيط كافي لذلك أو مشكلة أخرى). غالبًا ما أثبتت إعادة كتابة رمز Python أنه حل أسهل للمشكلة من تنفيذ نفس التحويل تلقائيًا في برنامج التحويل البرمجي. على المدى البعيد ، أردنا أتمتة العديد من هذه التحولات ، ولكن في تلك اللحظة كنا نهدف إلى تسريع الغموض بأقل جهد ممكن. ونحن ، نتحرك نحو هذا الهدف ، قطعنا عدة أركان.
أن تستمر ...
أعزائي القراء! ما هي انطباعاتك عن مشروع mypy عندما علمت بوجوده؟
