كود Buggy Python: الأخطاء العشرة الأكثر شيوعًا التي يرتكبها مطورو Python
نشرت: 2022-03-11حول بايثون
Python هي لغة برمجة مفسرة وموجهة للكائنات وعالية المستوى مع دلالات ديناميكية. هياكل البيانات عالية المستوى المدمجة ، جنبًا إلى جنب مع الكتابة الديناميكية والربط الديناميكي ، تجعلها جذابة للغاية لتطوير التطبيقات السريعة ، وكذلك لاستخدامها كلغة نصية أو لصق لربط المكونات أو الخدمات الحالية. تدعم Python الوحدات والحزم ، وبالتالي تشجع على إعادة استخدام الكود ونمطية البرنامج.
حول هذه المقالة
يمكن أن يؤدي بناء جملة Python البسيط السهل التعلم إلى تضليل مطوري Python - خاصة أولئك الذين هم حديثو اللغة - إلى فقدان بعض التفاصيل الدقيقة والتقليل من قوة لغة Python المتنوعة.
مع أخذ ذلك في الاعتبار ، تقدم هذه المقالة قائمة "العشرة الأوائل" للأخطاء الدقيقة إلى حد ما ، والتي يصعب اكتشافها والتي يمكن أن تلغى حتى بعض مطوري Python الأكثر تقدمًا في الخلف.
(ملاحظة: هذه المقالة مخصصة لجمهور أكثر تقدمًا من الأخطاء الشائعة لمبرمجي بايثون ، والتي تتجه أكثر نحو أولئك الجدد في اللغة.)
الخطأ الشائع الأول: إساءة استخدام التعبيرات كإعدادات افتراضية لوسائط الوظيفة
يتيح لك Python تحديد أن وسيطة الوظيفة اختيارية من خلال توفير قيمة افتراضية لها. على الرغم من أن هذه ميزة رائعة للغة ، إلا أنها قد تؤدي إلى بعض الالتباس عندما تكون القيمة الافتراضية قابلة للتغيير . على سبيل المثال ، ضع في اعتبارك تعريف دالة Python هذا:
>>> def foo(bar=[]): # bar is optional and defaults to [] if not specified ... bar.append("baz") # but this line could be problematic, as we'll see... ... return bar
الخطأ الشائع هو الاعتقاد بأنه سيتم تعيين الوسيطة الاختيارية على التعبير الافتراضي المحدد في كل مرة يتم فيها استدعاء الوظيفة دون توفير قيمة للوسيطة الاختيارية. في الكود أعلاه ، على سبيل المثال ، قد يتوقع المرء أن استدعاء foo()
بشكل متكرر (على سبيل المثال ، بدون تحديد وسيطة bar
) سيعيد دائمًا 'baz'
، نظرًا لافتراض أنه في كل مرة يتم استدعاء foo()
(بدون bar
) تم تحديد الوسيطة) bar
على []
(أي قائمة فارغة جديدة).
لكن دعنا نلقي نظرة على ما يحدث بالفعل عندما تفعل هذا:
>>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"]
هاه؟ لماذا استمر في إلحاق القيمة الافتراضية لـ "baz"
بقائمة موجودة في كل مرة يتم فيها استدعاء foo()
، بدلاً من إنشاء قائمة جديدة في كل مرة؟
الإجابة الأكثر تقدمًا لبرمجة Python هي أن القيمة الافتراضية لوسيطة دالة يتم تقييمها مرة واحدة فقط ، في الوقت الذي يتم فيه تحديد الوظيفة. وبالتالي ، تتم تهيئة وسيطة bar
إلى الوضع الافتراضي الخاص بها (أي قائمة فارغة) فقط عند تحديد foo()
لأول مرة ، ولكن بعد ذلك ، ستستمر الاستدعاءات لـ foo()
(أي بدون وسيطة bar
محددة) في استخدام نفس القائمة من أجل أي bar
تمت تهيئته في الأصل.
لمعلوماتك ، الحل البديل الشائع لهذا هو كما يلي:
>>> def foo(bar=None): ... if bar is None: # or if not bar: ... bar = [] ... bar.append("baz") ... return bar ... >>> foo() ["baz"] >>> foo() ["baz"] >>> foo() ["baz"]
الخطأ الشائع الثاني: استخدام متغيرات الفئة بشكل غير صحيح
ضع في اعتبارك المثال التالي:
>>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print Ax, Bx, Cx 1 1 1
من المنطقي.
>>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1
نعم ، مرة أخرى كما هو متوقع.
>>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3
ما هو $٪ #! & ؟؟ نحن فقط غيرنا Ax
. لماذا تغير Cx
أيضًا؟
في Python ، يتم التعامل مع متغيرات الفئة داخليًا كقواميس وتتبع ما يُشار إليه غالبًا باسم أمر حل الطريقة (MRO). لذلك في الكود أعلاه ، نظرًا لأن السمة x
غير موجودة في الفئة C
، فسيتم البحث عنها في فئاتها الأساسية (فقط A
في المثال أعلاه ، على الرغم من أن Python تدعم العديد من الوراثة). بعبارة أخرى ، لا تمتلك C
خاصية x
الخاصة بها ، بغض النظر عن A
وبالتالي ، فإن الإشارات إلى Cx
هي في الواقع إشارات إلى Ax
. هذا يسبب مشكلة بايثون ما لم يتم التعامل معها بشكل صحيح. تعرف على المزيد حول سمات الفصل في بايثون.
الخطأ الشائع الثالث: تحديد المعلمات بشكل غير صحيح لكتلة استثناء
افترض أن لديك الكود التالي:
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError: # To catch both exceptions, right? ... pass ... Traceback (most recent call last): File "<stdin>", line 3, in <module> IndexError: list index out of range
المشكلة هنا هي أن بيان except
لا يأخذ قائمة الاستثناءات المحددة بهذه الطريقة. بدلاً من ذلك ، في Python 2.x ، يتم استخدام البنية except Exception, e
لربط الاستثناء بالمعامل الثاني الاختياري المحدد (في هذه الحالة e
) ، من أجل إتاحته لمزيد من الفحص. نتيجة لذلك ، في الكود أعلاه ، لم يتم اكتشاف استثناء IndexError
بواسطة عبارة except
؛ بدلاً من ذلك ، ينتهي الاستثناء بدلاً من ذلك بالالتزام بمعامل يسمى IndexError
.
الطريقة الصحيحة للقبض على الاستثناءات المتعددة في عبارة except
هي تحديد المعامل الأول كمجموعة تحتوي على جميع الاستثناءات التي يجب التقاطها. أيضًا ، لتحقيق أقصى قدر من قابلية النقل ، استخدم الكلمة as
، نظرًا لأن بناء الجملة هذا مدعوم من قبل كل من Python 2 و Python 3:
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>
الخطأ الشائع الرابع: سوء فهم قواعد نطاق بايثون
يعتمد دقة نطاق Python على ما يُعرف بقاعدة LEGB ، وهو اختصار لـ L ocal و E nclosing و G lobal و B uilt-in. يبدو واضحًا بدرجة كافية ، أليس كذلك؟ حسنًا ، في الواقع ، هناك بعض التفاصيل الدقيقة للطريقة التي يعمل بها هذا في Python ، والتي تقودنا إلى مشكلة برمجة Python الأكثر تقدمًا أدناه. ضع في اعتبارك ما يلي:
>>> x = 10 >>> def foo(): ... x += 1 ... print x ... >>> foo() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment
ما هي المشكلة؟
يحدث الخطأ أعلاه لأنه ، عندما تقوم بتعيين تعيين لمتغير في نطاق ، فإن Python تعتبر تلقائيًا أن هذا المتغير محلي لهذا النطاق ويظلل أي متغير مشابه في أي نطاق خارجي.
وبالتالي يفاجأ الكثيرون بالحصول على UnboundLocalError
في رمز عمل سابقًا عندما يتم تعديله عن طريق إضافة عبارة إسناد في مكان ما في جسم الوظيفة. (تستطيع ان تقرأ المزيد عن هذا هنا.)
من الشائع بشكل خاص أن يقوم هذا برحلة إلى المطورين عند استخدام القوائم. ضع في اعتبارك المثال التالي:
>>> lst = [1, 2, 3] >>> def foo1(): ... lst.append(5) # This works ok... ... >>> foo1() >>> lst [1, 2, 3, 5] >>> lst = [1, 2, 3] >>> def foo2(): ... lst += [5] # ... but this bombs! ... >>> foo2() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment
هاه؟ لماذا قنبلة foo2
بينما كان foo1
يعمل بشكل جيد؟
الإجابة هي نفسها كما في مشكلة المثال السابق ولكنها أكثر دقة. لا foo1
إحالة إلى lst
، في حين أن foo2
هي. تذكر أن lst += [5]
هو في الحقيقة مجرد اختصار لـ lst = lst + [5]
، نرى أننا نحاول تعيين قيمة لـ lst
(لذلك تفترض Python أن تكون في النطاق المحلي). ومع ذلك ، فإن القيمة التي نتطلع إلى تخصيصها لـ lst
تستند إلى lst
نفسها (مرة أخرى ، يُفترض الآن أنها في النطاق المحلي) ، والتي لم يتم تحديدها بعد. فقاعة.
الخطأ الشائع الخامس: تعديل قائمة أثناء تكرارها
يجب أن تكون مشكلة الكود التالي واضحة إلى حد ما:
>>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> for i in range(len(numbers)): ... if odd(numbers[i]): ... del numbers[i] # BAD: Deleting item from a list while iterating over it ... Traceback (most recent call last): File "<stdin>", line 2, in <module> IndexError: list index out of range
يعد حذف عنصر من قائمة أو مصفوفة أثناء التكرار فوقه مشكلة في Python معروفة جيدًا لأي مطور برامج متمرس. ولكن في حين أن المثال أعلاه قد يكون واضحًا إلى حد ما ، يمكن حتى للمطورين المتقدمين أن يعضوا عن غير قصد بهذا في رمز أكثر تعقيدًا.
لحسن الحظ ، تضم Python عددًا من نماذج البرمجة الأنيقة التي ، عند استخدامها بشكل صحيح ، يمكن أن تؤدي إلى تعليمات برمجية مبسطة ومبسطة بشكل كبير. الميزة الجانبية لهذا هو أن الكود الأبسط من غير المرجح أن يتعرض للعض من خلال الحذف العرضي لعنصر القائمة أثناء تكرار الخطأ. أحد هذه النماذج هو مفهوم القوائم. علاوة على ذلك ، تعد عمليات فهم القوائم مفيدة بشكل خاص لتجنب هذه المشكلة المحددة ، كما يتضح من هذا التنفيذ البديل للكود أعلاه والذي يعمل بشكل مثالي:
>>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> numbers[:] = [n for n in numbers if not odd(n)] # ahh, the beauty of it all >>> numbers [0, 2, 4, 6, 8]
الخطأ الشائع السادس: الخلط بين كيفية ربط بايثون للمتغيرات في الإغلاق
بالنظر إلى المثال التالي:

>>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ...
قد تتوقع النتيجة التالية:
0 2 4 6 8
لكنك في الواقع تحصل على:
8 8 8 8 8
مفاجئة!
يحدث هذا بسبب سلوك الربط المتأخر في Python والذي ينص على أن قيم المتغيرات المستخدمة في عمليات الإغلاق يتم البحث عنها في وقت استدعاء الوظيفة الداخلية. لذلك في الكود أعلاه ، عندما يتم استدعاء أي من الوظائف التي تم إرجاعها ، يتم البحث عن قيمة i
في النطاق المحيط في وقت استدعائها (وبحلول ذلك الوقت ، تكون الحلقة قد i
، لذلك تم بالفعل تعيينها نهائيًا قيمة 4).
الحل لمشكلة بايثون الشائعة قليل من الاختراق:
>>> def create_multipliers(): ... return [lambda x, i=i : i * x for i in range(5)] ... >>> for multiplier in create_multipliers(): ... print multiplier(2) ... 0 2 4 6 8
هاهو! نحن نستفيد من الحجج الافتراضية هنا لإنشاء وظائف مجهولة من أجل تحقيق السلوك المطلوب. قد يسميها البعض أنيقًا. قد يسميها البعض خفية. البعض يكرهها. ولكن إذا كنت أحد مطوري Python ، فمن المهم أن تفهم على أي حال.
الخطأ الشائع السابع: إنشاء تبعيات وحدة دائرية
لنفترض أن لديك ملفين ، a.py
و b.py
، يستورد كل منهما الآخر ، على النحو التالي:
في a.py
:
import b def f(): return bx print f()
وفي b.py
:
import a x = 1 def g(): print af()
أولاً ، لنحاول استيراد a.py
:
>>> import a 1
عملت بشكل جيد. ربما يفاجئك ذلك. بعد كل شيء ، لدينا هنا استيراد دائري من المفترض أن يكون مشكلة ، أليس كذلك؟
الإجابة هي أن مجرد وجود استيراد دائري لا يمثل في حد ذاته مشكلة في بايثون. إذا تم استيراد وحدة نمطية بالفعل ، فإن Python ذكية بما يكفي لعدم محاولة إعادة استيرادها. ومع ذلك ، بناءً على النقطة التي تحاول عندها كل وحدة الوصول إلى الوظائف أو المتغيرات المحددة في الوحدة الأخرى ، فقد تواجه بالفعل مشكلات.
لذا بالعودة إلى مثالنا ، عندما قمنا باستيراد a.py
، لم يكن هناك مشكلة في استيراد b.py
، حيث أن b.py
لا يتطلب أي شيء من a.py
ليتم تعريفه في وقت استيراده . المرجع الوحيد في b.py
إلى a
هو استدعاء af()
. لكن هذا الاستدعاء موجود في g()
ولا شيء في a.py
أو b.py
يستدعي g()
. لذا فالحياة جيدة.
ولكن ماذا يحدث إذا حاولنا استيراد b.py
(بدون استيراد a.py
مسبقًا ، أي):
>>> import b Traceback (most recent call last): File "<stdin>", line 1, in <module> File "b.py", line 1, in <module> import a File "a.py", line 6, in <module> print f() File "a.py", line 4, in f return bx AttributeError: 'module' object has no attribute 'x'
عذرًا. هذا ليس جيدا! تكمن المشكلة هنا في أنه أثناء عملية استيراد b.py
، يحاول استيراد a.py
، والذي بدوره يستدعي f()
، والذي يحاول الوصول إلى bx
. لكن bx
لم يتم تحديده بعد. ومن هنا جاء استثناء AttributeError
.
هناك حل واحد على الأقل لهذا الأمر تافه تمامًا. ما عليك سوى تعديل b.py
لاستيراد a.py
داخل g()
:
x = 1 def g(): import a # This will be evaluated only when g() is called print af()
لا عندما نستوردها ، كل شيء على ما يرام:
>>> import b >>> bg() 1 # Printed a first time since module 'a' calls 'print f()' at the end 1 # Printed a second time, this one is our call to 'g'
الخطأ الشائع الثامن: تضارب الأسماء مع وحدات مكتبة Python القياسية
واحدة من روائع بايثون هي ثروة وحدات المكتبات التي تأتي مع "خارج الصندوق". ولكن نتيجة لذلك ، إذا لم تكن تتجنبها عن قصد ، فليس من الصعب أن تصطدم بتضارب في الاسم بين اسم إحدى الوحدات النمطية الخاصة بك ووحدة تحمل الاسم نفسه في المكتبة القياسية التي تأتي مع Python (على سبيل المثال ، قد يكون لديك وحدة باسم email.py
في التعليمات البرمجية الخاصة بك ، والتي قد تتعارض مع وحدة المكتبة القياسية التي تحمل الاسم نفسه).
يمكن أن يؤدي ذلك إلى مشاكل غير متوقعة ، مثل استيراد مكتبة أخرى والتي بدورها تحاول استيراد إصدار Python Standard Library من الوحدة النمطية ، ولكن نظرًا لأن لديك وحدة تحمل نفس الاسم ، فإن الحزمة الأخرى تستورد نسختك عن طريق الخطأ بدلاً من النسخة الموجودة بداخلها مكتبة بايثون القياسية. هذا هو المكان الذي تحدث فيه أخطاء Python السيئة.
لذلك ، يجب توخي الحذر لتجنب استخدام نفس الأسماء الموجودة في وحدات مكتبة Python القياسية. من الأسهل بالنسبة لك تغيير اسم الوحدة داخل الحزمة الخاصة بك بدلاً من تقديم اقتراح تحسين Python (PEP) لطلب تغيير الاسم في البداية ومحاولة الحصول على الموافقة.
الخطأ الشائع رقم 9: الفشل في معالجة الاختلافات بين Python 2 و Python 3
ضع في اعتبارك الملف التالي foo.py
:
import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e) bad()
في Python 2 ، هذا جيد:
$ python foo.py 1 key error 1 $ python foo.py 2 value error 2
لكن الآن لنجربها في بايثون 3:
$ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in <module> bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment
ما الذي حدث للتو هنا؟ تكمن المشكلة في أنه ، في Python 3 ، لا يمكن الوصول إلى كائن الاستثناء خارج نطاق كتلة except
. (السبب في ذلك هو أنه ، بخلاف ذلك ، سيحتفظ بدورة مرجعية مع إطار المكدس في الذاكرة حتى يتم تشغيل أداة تجميع البيانات المهملة وتزيل المراجع من الذاكرة. يتوفر المزيد من التفاصيل الفنية حول هذا هنا).
تتمثل إحدى طرق تجنب هذه المشكلة في الاحتفاظ بمرجع إلى كائن الاستثناء خارج نطاق كتلة except
بحيث يظل قابلاً للوصول. إليك إصدار من المثال السابق يستخدم هذه التقنية ، وبالتالي ينتج عنه رمز متوافق مع Python 2 و Python 3:
import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception) good()
تشغيل هذا على Py3k:
$ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2
يبي!
(بالمناسبة ، يناقش دليل التوظيف في Python عددًا من الاختلافات المهمة الأخرى التي يجب أن تكون على دراية بها عند ترحيل التعليمات البرمجية من Python 2 إلى Python 3.)
الخطأ الشائع رقم 10: إساءة استخدام طريقة __del__
لنفترض أن لديك هذا في ملف يسمى mod.py
:
import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)
ثم حاولت القيام بذلك من another_mod.py
:
import mod mybar = mod.Bar()
ستحصل على استثناء خطأ AttributeError
قبيح.
لماذا ا؟ لأنه ، كما ورد هنا ، عندما يتم إيقاف تشغيل المترجم الفوري ، يتم تعيين جميع المتغيرات العامة للوحدة على None
. نتيجة لذلك ، في المثال أعلاه ، في النقطة التي يتم فيها استدعاء __del__
، تم بالفعل تعيين الاسم foo
على None
.
سيكون الحل لمشكلة برمجة Python الأكثر تقدمًا إلى حد ما هو استخدام atexit.register()
بدلاً من ذلك. بهذه الطريقة ، عندما ينتهي برنامجك من التنفيذ (عند الخروج بشكل طبيعي ، أي) ، يتم تشغيل معالجاتك المسجلة قبل إيقاف تشغيل المترجم الفوري.
مع هذا الفهم ، قد يبدو إصلاح كود mod.py
أعلاه شيئًا كالتالي:
import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle)
يوفر هذا التطبيق طريقة نظيفة وموثوقة لاستدعاء أي وظيفة تنظيف مطلوبة عند إنهاء البرنامج العادي. من الواضح أن الأمر متروك لـ foo.cleanup
لتقرير ما يجب فعله مع الكائن المرتبط بالاسم self.myhandle
، لكنك حصلت على الفكرة.
يتم إحتوائه
Python هي لغة قوية ومرنة مع العديد من الآليات والنماذج التي يمكن أن تحسن الإنتاجية بشكل كبير. كما هو الحال مع أي أداة برمجية أو لغة ، على الرغم من ذلك ، فإن امتلاك فهم أو تقدير محدود لقدراتها يمكن أن يكون في بعض الأحيان عائقًا أكثر من كونه منفعة ، مما يترك المرء في حالة يضرب بها المثل "معرفة ما يكفي ليكون خطيرًا".
سيساعد التعرف على الفروق الدقيقة الرئيسية في Python ، مثل (على سبيل المثال لا الحصر) مشاكل البرمجة المتقدمة بشكل معتدل التي أثيرت في هذه المقالة ، على تحسين استخدام اللغة مع تجنب بعض الأخطاء الأكثر شيوعًا.
قد ترغب أيضًا في مراجعة دليل Insider's Guide to Python للمقابلة للحصول على اقتراحات حول أسئلة المقابلة التي يمكن أن تساعد في تحديد خبراء Python.
نأمل أن تكون قد وجدت المؤشرات الواردة في هذه المقالة مفيدة ونرحب بتعليقاتك.