اكتب كل شيء

مرحبا بالجميع!


لدينا بالفعل مقال واحد حول تطوير الكتابة في Ostrovok.ru . وهذا ما يفسر سبب تحولنا من pyContracts إلى typeguard ، ولماذا نتحول إلى typeguard وما ينتهي بنا إليه الأمر. واليوم سوف أخبركم أكثر عن كيفية حدوث هذا الانتقال.



يبدو إعلان الوظيفة مع pyContracts بشكل عام كما يلي:


from contracts import new_contract import datetime @new_contract def User (x): from models import User return isinstance(x, User) @new_contract def dt_datetime (x): return isinstance(x, datetime.datetime) @contract def func(user_list, amount, dt=None): """ :type user_list: list(User) :type amount: int|float :type dt: dt_datetime|None :rtype: bool """ 

هذا مثال تجريدي ، لأنني لم أجد في مشروعنا تعريفًا لدالة قصيرة وذات مغزى من حيث عدد الحالات لفحص النوع. عادة ، يتم تخزين تعريفات pyContracts في الملفات التي لا تحتوي على أي منطق آخر. لاحظ أن المستخدم هنا هو فئة مستخدم معين ، ولا يتم استيراده مباشرةً.


وهذه هي النتيجة المرجوة مع typeguard:


 from typechecked import typechecked from typing import List, Optional, Union from models import User import datetime @typechecked def func (user_list: List[User], amount: Union[int, float], dt: Optional[datetime.datetime]=None) -> bool: ... 

بشكل عام ، هناك العديد من الوظائف والطرق مع التحقق من النوع في المشروع بحيث إذا قمت بتجميعها في مكدس ، يمكنك الوصول إلى القمر. حتى الترجمة يدويا لهم من pyContracts إلى typeguard هو ببساطة غير ممكن (حاولت!). لذلك قررت أن أكتب السيناريو.


ينقسم البرنامج النصي إلى جزأين: أحدهما يخبئ واردات العقود الجديدة ، والثاني يتعامل مع إعادة هيكلة الكود.


أريد أن أشير إلى أنه لا أحد أو السيناريو الآخر يدعي أنه عالمي. لم نهدف إلى كتابة أداة لحل جميع الحالات المطلوبة. لذلك ، غالبًا ما أغفلت المعالجة التلقائية لبعض الحالات الخاصة ، إذا كان من النادر العثور عليها في المشروع ، فمن الأسرع إصلاحها يدويًا. على سبيل المثال ، قام البرنامج النصي لإنشاء عقود تعيينات وواردات بتجميع 90٪ من القيم ، أما الـ 10٪ الباقية فهي خرائط صياغة يدوية.


منطق البرنامج النصي لإنشاء التعيين:


الخطوة 1. انتقل من خلال جميع ملفات المشروع ، وقراءتها. لكل ملف:


  • إذا كانت السلسلة الفرعية "new_contract" غير موجودة ، فتخط هذا الملف ،
  • إذا كان هناك ، فاقسم الملف بسطر "@ new_contract". لكل عنصر:
    - تحليل التعريف والاستيراد ،
    - إذا نجحت ، فاكتب إلى ملف النجاح ،
    - إذا لم يكن كذلك ، فاكتب إلى ملف الخطأ.

الخطوة 2. معالجة الأخطاء يدويا


والآن بعد أن أصبح لدينا أسماء جميع الأنواع التي تستخدمها pyContracts (تم تعريفها باستخدام مُصمم new_contract decorator) ، ولدينا جميع عمليات الاستيراد الضرورية ، يمكننا كتابة التعليمات البرمجية لإعادة التجهيز. بينما كنت أترجم من pyContracts إلى typeguard يدويًا ، أدركت ما أحتاجه من البرنامج النصي:


  1. هذا أمر يأخذ اسم وحدة نمطية كوسيطة (يمكن استخدام العديد منها) ، حيث يجب استبدال بناء جملة التعليقات التوضيحية للوظائف.
  2. انتقل من خلال جميع ملفات الوحدة النمطية ، وقراءتها. لكل ملف:
    • إذا لم تكن هناك سلسلة فرعية "contract" ، فتخط هذا الملف ؛
    • إذا كان الأمر كذلك ، فقم بتحويل الكود إلى ast (شجرة بناء الجملة) ؛
    • العثور على جميع الوظائف التي هي تحت الديكور العقد لكل:
      • الحصول على dockstring ، تحليل ، ثم حذف ،
      • إنشاء قاموس للنموذج {arg_name: arg_type} ، استخدمه لاستبدال التعليق التوضيحي للوظيفة ،
      • تذكر واردات جديدة ،
    • كتابة الشجرة المعدلة إلى ملف من خلال astunparse ؛
    • إضافة واردات جديدة إلى أعلى الملف ؛
    • استبدل السطور "contract" بـ "typechecked" لأنها أسهل من خلال ast.

حل السؤال "هل تم استيراد هذا الاسم بالفعل في هذا الملف؟" لم أكن أنوي من البداية: مع هذه المشكلة ، سنتعامل مع مجموعة إضافية من مكتبة الجزر.


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


  • في لحظة الانتقال إلى شجرة بناء الجملة ، تختفي جميع التعليقات ذات سطر واحد من الكود ؛
  • خطوط فارغة تختفي أيضا ؛
  • ast لا يميز بين وظائف وطرق الصف ، كان علينا أن نضيف المنطق ؛
  • على العكس من ذلك ، عند الانتقال من شجرة إلى رمز ، تتم كتابة التعليقات متعددة الأسطر في علامات اقتباس ثلاثية في تعليقات مفردة وتحتل سطرًا واحدًا ، ويتم استبدال فواصل الأسطر الجديدة بـ \ n؛
  • تظهر الأقواس غير الضرورية ، على سبيل المثال إذا أصبحت A و B و C أو D إذا ((A و B و C) أو D).

لا تزال الشفرة التي تم تمريرها عبر ast و astunparse تعمل ، لكن قابلية قراءتها منخفضة.


أخطر عيوب ما سبق هو اختفاء التعليقات ذات سطر واحد (في حالات أخرى ، لا نخسر شيئًا ، ولكن الربح فقط - بين قوسين ، على سبيل المثال). مكتبة horast على أساس ast ، astunparse ، و الرمز المميز وعود لمعرفة هذا. الوعود والفعل.


الآن الخطوط الفارغة. كان هناك حلان ممكنان:


  1. يعرف الرمز المميز كيفية تحديد "جزء الكلام" من الثعبان ، ويستفيد منه الرعب عندما يحصل على الرموز المميزة لنوع التعليق. لكن الرمز المميز له أيضًا رموز مثل NewLine و NL. لذا ، فأنت بحاجة إلى معرفة كيفية استعادة horast للتعليقات ، ونسخها ، واستبدال نوع الرمز المميز.
    - اقترح أنيا ، خبرة في تطوير شهرين
  2. نظرًا لأن horast يمكنه استعادة التعليقات ، فإننا نستبدل أولاً جميع الخطوط الفارغة بتعليق معين ، ثم نتخطى horast ونستبدل تعليقنا بخط فارغ.
    - ابتكر يوجين ، خبرة في تطوير 8 سنوات

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


في horast نستخدم وظيفتين: تحليل و unparse ، لكن كلاهما ليس مثاليًا - يحتوي parse على أخطاء داخلية غريبة ، وفي حالات نادرة لا يمكن تحليل الكود المصدر ، ولا يمكن unparse كتابة شيء له نوع كتابة (مثل هذا النوع الذي اتضح إذا قمت بكتابة (any_other_type)).


قررت عدم التعامل مع التحليل ، لأن منطق العمل مربك إلى حد ما ، والاستثناءات نادرة - مبدأ اللامبدأية يعمل هنا.


ولكن unparse يعمل بشكل واضح جدا وأنيق جدا. تقوم الدالة unparse بإنشاء مثيل لفئة Unparser ، والتي تقوم في init بمعالجة الشجرة ثم كتابتها في ملف. يتم توريث Horast.Unparser على التوالي من العديد من Unparsers ، حيث تكون الطبقة الأساسية هي astunparse.Unparser. تعمل كل الطبقات المنحدرة ببساطة على توسيع وظائف الفئة الأساسية ، ولكن يظل منطق العمل كما هو ، لذلك خذ بعين الاعتبار astunparse.Unparser. لديها خمس طرق مهمة:


  1. الكتابة - فقط يكتب شيئا إلى ملف.
  2. fill - يستخدم الكتابة بناءً على عدد المسافات البادئة (يتم تخزين عدد المسافات البادئة كحقل فئة).
  3. أدخل - يزيد المسافة البادئة.
  4. إجازة - يقلل من المسافة البادئة.
  5. إرسال - يحدد نوع عقدة الشجرة (قل T) ، يستدعي الأسلوب المقابل باسم نوع العقدة ، ولكن باستخدام الشرطة السفلية (على سبيل المثال _T). هذه هي طريقة الفوقية.

جميع الطرق الأخرى هي طرق النموذج _T ، على سبيل المثال ، _Module أو _Str. في كل طريقة ، يمكن أن: 1) الإرسال بشكل متكرر للعقد الشجرة الفرعية ، أو 2) استخدام الكتابة لكتابة محتويات العقدة أو إضافة أحرف وكلمات رئيسية بحيث تكون النتيجة تعبير صحيح في بيثون.


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


دعنا نعود إلى مشكلتنا المتمثلة في استحالة معالجة نوع النوع. الآن بعد أن أدركت كيف تعمل unparse ، أصبح إنشاء نوعك أمرًا سهلاً. لنقم بإنشاء نوع:


 class NewType(object): def __init__ (self, t): self.s = ts 

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


للقيام بذلك ، قم بتوسيع horast.Unparser - اكتب UnparserWithType الخاص بك ، ورثًا من horast.Unparser ، وأضف معالجة النوع الجديد لدينا.


 class UnparserWithType(horast.Unparser): def _NewType (self, t): self.write(ts) 

يجمع هذا مع روح المكتبة. مصنوعة أسماء المتغيرات في أسلوب ast ، وهذا هو السبب أنها تتكون من حرف واحد ، وليس لأنني لا أستطيع التفكير في الأسماء. أعتقد أن هذا t قصير بالنسبة للشجرة و s للخيط. بالمناسبة ، NewType ليس سلسلة. إذا أردنا أن يتم تفسيرها كنوع سلسلة ، فسنضطر إلى كتابة علامات الاقتباس قبل وبعد مكالمة الكتابة.


و الان السحر قرد التصحيح: استبدال horast.Unparser مع UnparserWithType لدينا.


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


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


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


 class StrType(object): def __init__ (self, s, indent): self.s = s self.indent = indent def __repr__ (self): return '"""\n' + self.s + '\n' + ' ' * 4 * self.indent + '"""\n' 

في repr نحدد كيف ينبغي أن يبدو خطنا. أعتقد أن هذا أبعد ما يكون عن الحل الوحيد ، لكنه يعمل. يمكن للمرء تجربة مع astunparse.fill و astunparse.Unparser.indent ، بعد ذلك سيكون أكثر عالمية ، ولكن هذه الفكرة جاءت إلى ذهني بالفعل في وقت كتابة هذا المقال.


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


صعوبة أخرى واجهتها هي عدم وجود معالجة تعبيرية في ast من astro الاستيراد ، حيث يعرف القارئ المتأني بالفعل أن التصحيح القرد هو العلاج لجميع الأمراض. فليكن هذا واجبه المنزلي بالنسبة له ، لكنني قررت القيام بذلك: ما عليك سوى إضافة هذه الواردات إلى ملف التعيين ، لأن هذا البناء يستخدم عادة لتجاوز تعارض الأسماء ، ولدينا القليل منها.


على الرغم من العيوب التي تم العثور عليها ، فإن النص يفعل ما كان الغرض منه القيام به. ما هي النتيجة:


  1. تم تقليل وقت إطلاق المشروع من 10 إلى 3 ثوان ؛
  2. انخفض عدد الملفات بسبب إزالة تعريفات العقد الجديد. تم تخفيض الملفات نفسها: لم أكن أقيسها ، ولكن في المتوسط ​​بلغ مجموع الخطوط المضافة n و 2n المحذوفة ؛
  3. بدأت IDE الذكية في صنع تلميحات مختلفة ، لأنها الآن ليست تعليقات ، بل واردات صادقة ؛
  4. تحسنت قابلية القراءة ؛
  5. ظهرت في مكان ما بين قوسين.

شكرا لك


روابط مفيدة:


  1. است
  2. Horast
  3. جميع أنواع العقد ast وما يتم تخزينها فيها
  4. يظهر جميل شجرة بناء الجملة
  5. Isort

Source: https://habr.com/ru/post/ar480930/


All Articles