Pylint: اختبار مفصل لمحلل الكود

عندما عمل Luke مع Flake8 وراقب Pylint ، كان لديه انطباع بأن 95٪ من الأخطاء الناتجة عن Pylint خاطئة. كان لدى المطورين الآخرين تجربة مختلفة في التفاعل مع هؤلاء المحللين ، لذلك قرر لوك النظر في الموقف بالتفصيل ودراسة عمله على 11 ألف سطر من شفرته. بالإضافة إلى ذلك ، أشاد بفوائد Pylint ، واعتبرها إضافة إلى Flake8.



Luke ( Luke Plant ) - أحد المطورين البريطانيين ، الذين ظهرنا مؤخرًا حول مقالهم مع تحليل محللي الشفرة الشائعة. تدرس Linters الرمز ، وتساعدك في العثور على الأخطاء ، وتجعله متسقًا من حيث الأسلوب مع المعايير والكود الذي يكتبه المطورون في فريقك. الأكثر شيوعًا هي Pylint و Flake8. نحن في هوية القائدنحن نستخدمهم أيضًا ، لذلك قمنا بترجمة مقالته بسعادة. نأمل أن يثري تجربتك مع هذه الأدوات.

الإعدادات الأولية وقاعدة الاختبار


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

القليل من المساعدة حول المشروع الذي تم أخذ الرمز منه:

  • تطبيق منتظم مكتوب بلغة جانغو (أي داخل نفس Python). تمتلك Django خصائصها الخاصة ، وكإطار لها حدودها ، ولكنها تسمح لك بكتابة كود Python عادي. المكتبات الأخرى التي تستخدم القوالب (وظائف رد الاتصال أو قوالب تصميم طريقة القالب) لديها أيضًا بعض السلبيات الخاصة بها كإطار عمل.
  • يتكون من 22000 سطر من التعليمات البرمجية. مر ما يقرب من 11000 خط عبر Pylint (9000 إذا تم حذف الفجوات). يتألف هذا الجزء من المشروع بشكل رئيسي من المشاهدات ورمز الاختبار.
  • لتحليل الرمز لهذا المشروع ، استخدمت بالفعل Flake8 ، بعد أن قمت بمعالجة جميع الأخطاء التي تم تلقيها. كان الهدف من هذه التجربة هو تقييم فوائد Pylint كإضافة إلى Flake8.
  • يتمتع المشروع بتغطية اختبار جيدة للكود ، ولكن بما أنني مؤلفه الوحيد ، لم تتح لي الفرصة لاستخدام مراجعة الأقران.

آمل أن يكون هذا التحليل مفيدًا للمطورين الآخرين لاتخاذ قرار بشأن استخدام Pylint في المكدس للتحقق من جودة الشفرة. أفترض أنه إذا كنت تستخدم بالفعل شيء مثل Pylint ، فستستخدمه بشكل منهجي لمراقبة الجودة الضرورية للكود من أجل تقليل المشاكل.
لذلك ، أصدر Pylint 1650 مطالبة برمز بلدي.

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

البق


عثر Pylint على خطأ واحد بالضبط في الرمز الخاص بي. أعتبر الخطأ خطأ يحدث أو يحتمل أن ينشأ أثناء تشغيل البرنامج. في هذه الحالة ، استخدمت استثناءات - broad-except.وهذا ليس استثناءً except Exceptionفقط ، وهو ما يجذبه Flake8. سيؤدي هذا إلى سلوك وقت التشغيل مع بعض الاستثناءات. إذا ظهر هذا الخطأ في وقت التشغيل (وليس حقيقة ظهوره) ، فلن يكون لسلوك التعليمات البرمجية غير الصحيح عواقب وخيمة ، على الرغم من ...

الإجمالي: 1

مفيد


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

سبعة منهم كانوا too-many-locals/ too-many-branches/ too-many-local-variables. كانت تنتمي إلى ثلاثة أجزاء من الكود الخاص بي والتي كانت سيئة التنظيم. سيكون من الجيد أن نفكر في الهيكل ، وأنا متأكد من أنه يمكنني القيام بعمل أفضل.

أخطاء أخرى:

  • unused-argument× 3 - كان أحدهم بالفعل دعامة ، وتم تنفيذ الرمز بشكل صحيح بشكل عشوائي. والحجتان الأخريان غير الضروريتين وغير المستخدمتين ستؤدي إلى مشاكل في المستقبل إذا استخدمتهما.
  • redefined-builtin × 2
  • dangerous-default-value × 2 - ليس أخطاء ، لأنني لم أستخدم القيم الافتراضية مطلقًا ، ولكن سيكون من الجيد إصلاح ذلك فقط في حالة.
  • stop-iteration-return × 1 - هنا تعلمت شيئًا جديدًا لنفسي ، ما كنت لأجده بنفسي.
  • no-self-argument × 1

المجموع: 16

تعديلات تجميلية


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

invalid-name× 192

كانت هذه أسماء متغيرة ذات حرف واحد. تستخدم في تلك السياقات حيث لم يكن مخيفا ، على سبيل المثال:


أو


كان الكثير في رمز الاختبار:

  • len-as-condition × 20
  • useless-object-inheritance × 16 (إرث Python 2)
  • no-else-return × 11
  • no-else-raise × 1
  • bad-continuation × 6
  • redefined-builtin × 4
  • inconsistent-return-statements × 1
  • consider-using-set-comprehension × 1
  • chained-comparison × 1

المجموع: 252

بدون فائدة


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

  • too-many-ancestors × 76

كان في رمز الاختبار حيث استخدمت العديد من فئات الشوائب لوضع المرافق أو بذرة.

  • unused-variable × 43

تم العثور عليه طوال الوقت تقريبًا في رمز الاختبار ، حيث حطمت الرقم القياسي:


... ولم تستخدم أي من العناصر. هناك عدة طرق لمنع Pylint من الإبلاغ عن الأخطاء (على سبيل المثال ، إعطاء أسماء unused). ولكن إذا تركته بالشكل الذي كتبته ، فسيكون قابلاً للقراءة ، وسيتمكن الأشخاص (بما فيهم أنا) من فهمه ودعمه.

  • invalid-name × 26

كانت هذه حالات قمت فيها بتعيين أسماء مناسبة في السياق ، ولكن تلك التي لم تستوف معايير تسمية Pylint. على سبيل المثال ، db (هذا اختصار شائع لقاعدة البيانات) وبعض الأسماء الأخرى غير القياسية ، والتي ، في رأيي ، كانت مفهومة أكثر. لكنك قد لا توافقني.

  • redefined-outer-name × 16

في بعض الأحيان يتم كتابة اسم متغير بشكل صحيح لكل من السياقات الداخلية والخارجية. ولن تضطر أبدًا إلى استخدام اسم خارجي من سياق داخلي.

  • too-few-public-methods × 14

تشمل الأمثلة الفصول التي تحتوي على البيانات التي تم إنشاؤها باستخدام attrs ، حيث لا توجد طرق عامة ، وفئة تطبق واجهة القاموس ، ولكنها ضرورية لضمان عمل الطريقة بشكل صحيح__getitem__

  • no-self-use × 12

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

  • attribute-defined-outside-init × 10

في هذه الحالات ، كانت هناك أسباب جيدة لكتابة الرمز كما هو. في الأساس ، حدثت هذه الأخطاء في رمز الاختبار.

  • too-many-locals× 6 ، too-many-return-statements× 6 ، too-many-branches× 2 ، too-many-statements× 2

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

  • arguments-differ × 6

هذا يرجع بشكل رئيسي إلى استخدام * args و ** kwargs في الطريقة الملغية ، والتي تسمح لك بحماية نفسك من التغييرات في توقيع الطرق من مكتبات الطرف الثالث (ولكن في بعض الحالات قد تشير هذه الرسالة إلى أخطاء حقيقية).

  • ungrouped-imports × 4

أنا بالفعل استخدام isort للاستيراد

  • fixme × 4

نعم ، هناك العديد من الأشياء التي يجب إصلاحها ، ولكن الآن لا أريد إصلاحها.

  • duplicate-code × 3

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

  • broad-except × 2
  • abstract-method × 2
  • redefined-builtin × 2
  • too-many-lines × 1

حاولت معرفة الطريقة الطبيعية لكسر هذه الوحدة ، ولكن لم أستطع. هذا مثال واحد حيث يمكنك أن ترى أن اللنتر هو الأداة الخاطئة. إذا كان لدي وحدة نمطية تحتوي على 980 سطرًا من التعليمات البرمجية ، وأضفت 30 سطرًا آخر ، فأنا أتجاوز حد 1000 سطر ، ولن تساعدني الإخطارات من اللنتر هنا. إذا كان 980 خطًا على ما يرام ، فلماذا 1010 سيئ؟ لا أريد إعادة هيكلة هذه الوحدة ، لكني أريد أن لا ينتج اللنتر أخطاء. الحل الوحيد في هذه اللحظة التي أراها هو جعل بطريقة أو بأخرى بحيث يكون اللنت صامتًا ، وهذا يتناقض مع الهدف النهائي.

  • pointless-statement × 1
  • expression-not-assigned × 1
  • cyclic-import × 1

اكتشفنا الدورة بتحريك أجزائها إلى إحدى الوظائف. لم أتمكن من العثور على طريقة أفضل لبناء التعليمات البرمجية بناءً على القيود.

  • unused-import × 1

أضفت بالفعل # NOQAعند استخدام Flake8 بحيث لا ينبثق هذا الخطأ.

  • too-many-public-methods × 1

إذا كان هناك 35 اختبارًا في فصلي الاختباري بدلاً من 20 اختبارًا منظمًا ، فهل هذه مشكلة حقًا؟

  • too-many-arguments × 1

المجموع: 243

غير قادر على الإصلاح


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

  • unused-argument × 21
  • invalid-name × 13
  • protected-access × 3

يشمل الوصول إلى "الأشياء الداخلية الموثقة" ، مثل sys._getframeفي stdlib و Django Model._meta.

  • too-few-public-methods × 3
  • too-many-arguments × 2
  • wrong-import-position × 2
  • attribute-defined-outside-init × 1
  • too-many-ancestors × 1

المجموع: 46

رسائل زائفة


الأشياء التي يكون فيها Pylint خاطئًا بشكل واضح. في هذه الحالة ، هذه ليست أخطاء Pylint: الحقيقة هي أن Python ديناميكية ، و Pylint يحاول اكتشاف أشياء لا يمكن القيام بها بشكل مثالي أو موثوق.

  • no-member × 395

مرتبطة بالعديد من الفئات الأساسية: من Django وتلك التي أنشأتها بنفسي. لم يتمكن Pylint من اكتشاف المتغير بسبب الديناميكية / metaprogramming.

حدثت العديد من الأخطاء بسبب بنية رمز الاختبار (استخدمت نموذجًا من django-functest ، والذي يمكن تصحيحه في بعض الحالات عن طريق إضافة فئات أساسية إضافية باستخدام الأساليب "المجردة" التي تستدعيها NotImplementedError) ، أو ربما إعادة تسمية العديد من فئات الاختبار (I لم أفعل ذلك ، لأنه في بعض الحالات سيكون الأمر مربكًا).

  • invalid-name × 52

نشأت المشكلة بشكل أساسي لأن Pylint طبق قاعدة PEP8 الثابتة ، مع الأخذ في الاعتبار أن كل اسم من المستوى الأعلى المحدد بـ ="ثابت". إن تحديد ما نعنيه بالثابت هو أكثر صعوبة مما يبدو ، ولكن هذا لا ينطبق على بعض الأشياء التي هي ثوابت بطبيعتها ، على سبيل المثال ، الدوال. أيضًا ، لا ينبغي تطبيق القاعدة على طرق أقل شيوعًا لإنشاء الوظائف ، على سبيل المثال:


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


  • no-self-use × 23

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

  • protected-access × 5

تم تقييم Pylint بشكل خاطئ من هو "المالك" (جزء الشفرة الحالي يخلق السمة المحمية للكائن ويستخدمه محليًا ، لكن Pylint لا يرى ذلك).

  • no-name-in-module × 1
  • import-error × 1
  • pointless-statement × 1

هذا البيان أنتج النتيجة في الواقع:


لقد استخدمت هذا لإحداث خطأ غير مقصود من غير المحتمل أن يتم العثور عليه من خلال الاختبارات. أنا لا ألوم Pylint لعدم الاعتراف بها ...

المجموع: 477

المجموع الفرعي


لم نصل إلى خط النهاية بعد ، ولكن حان الوقت لتجميع نتائجنا:

  1. "جيد" - كتل "البق" و "المرافق" - هنا ساعد Pylint بالتأكيد: 17.
  2. "محايد" - "تغييرات تجميلية" - فائدة بسيطة من Pylint ، لن تسبب الأخطاء ضررًا: 252.
  3. "سيء" - "عديم الفائدة" ، "غير قادر على الإصلاح" ، "عدم الدقة" - حيث يريد Pylint إجراء تغييرات في الشفرة ، حيث لا يلزم ذلك. بما في ذلك حيث لا يمكن إجراء تعديلات بسبب التبعيات الخارجية أو حيث قام Pylint ببساطة بتحليل الرمز بشكل غير صحيح: 766.

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

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

  1. تأخذ وقت!
  2. لا أحب تراكم التعليقات الموجودة لإسكات اللتر.

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

الوثائق


كانت المشاكل المتبقية التي اكتشفها Pylint تفتقد إلى خطوط التوثيق. أضعها في فئة منفصلة للأسباب التالية:

  1. إنها مثيرة للجدل للغاية ، وقد يكون لديك سياسة مختلفة تمامًا فيما يتعلق بهذه الأشياء.
  2. لم يكن لدي الوقت لتحليل كل منهم.

في المجموع ، اكتشف Pylint 620 رصيفًا مفقودًا (في الوحدات والوظائف والطرق الصفية). ولكن في كثير من الحالات ، كنت على حق. على سبيل المثال:

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

في حالات أخرى ، لن تضر أسطر الوصف هذه برموزي. في حوالي 15-30 ٪ من الحالات التي اكتشفها Pylint ، كنت أعتقد ، "نعم ، أحتاج إلى إضافة مستند هنا ، شكرًا Pylint للتذكير."

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

  • قراءتها مضيعة للوقت: ليس لديهم معلومات إضافية أو أنها تحتوي على معلومات غير صحيحة ،
  • أنها تساعد في تسليط الضوء دون وعي على المستندات في النص ، لأنها تحتوي على معلومات مفيدة (إذا قمت بكتابتها في الحالة).

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

استنتاج


أعتقد أن افتراضاتي الأولية حول عدم جدوى Pylint تبين أنها صحيحة (في سياق قاعدة الكود التي أستخدمها Flake8). لكي أستخدمه ، يجب أن يكون المؤشر الذي يعكس عدد الإيجابيات الخاطئة أقل.

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

إذا كنت تستخدم Pylint في مشروع من البداية أو في مشروع لا توجد فيه تبعيات خارجية ، أعتقد أنه سيكون لديك رأي مختلف وسيكون عدد الإشعارات الكاذبة أقل. ومع ذلك ، يمكن أن يؤدي هذا إلى تكاليف وقت إضافية.

نهج آخر هو استخدام Pylint لعدد محدود من أنواع الأخطاء. ومع ذلك ، لم يكن هناك سوى عدد قليل ، وتبين أن الردود كانت صحيحة أو نادرا ما تكون خاطئة (من حيث النسبية والمطلقة). من بينها: dangerous-default-value، stop-iteration-return، broad-exception، useless-object-inheritance.

على أي حال ، آمل أن تساعدك هذه المقالة عند التفكير في استخدام Pylint أو في نزاع مع الزملاء.

All Articles