تقرير البايثون
الأخطاء العشرة الأكثر شيوعًا التي يصنعها مطورو البايثون
حول البايثون:
البايثون هي لغة برمجة قابلة للبرمجة للغاية للكائنات الحية النشطة بيولوجيا.
تعد هياكل البيانات المهيكلة عالية المستوى ، البرمجة النصية الديناميكية والربط الديناميكي ، جذابة للغاية لتطوير التطبيقات السريعة ، وكذلك للاستخدام كنص برمجي أو لغة غراء لتقديم المكونات أو الخدمات الحالية.
يدعم البايثون الوحدات النمطية والحزم ، والتي تشجع على إعادة استخدام البرنامج والشفرة.
حول هذا المقال:
يمكن أن يؤدي بناء جملة Python البسيط وسهل التعلم إلى تضليل مطوري Python - وخاصة أولئك الذين هم جدد على اللغة - إلى فقدان بعض التفاصيل الدقيقة له والتقليل من قوة لغة Python المختلفة.
مع وضع ذلك في الاعتبار ، تقدم هذه المقالة "أفضل 10" قائمة بالأخطاء الدقيقة إلى حد ما التي يصعب اللحاق بها والتي يمكن أن تعض حتى بعض مطوري بايثون الأكثر تقدماً في العمق.
(ملاحظة: هذه المقالة مخصصة لجمهور أكثر تقدماً من الأخطاء الشائعة لمبرمجي Python ، والتي يتم توجيهها بشكل أكبر نحو الأشخاص الأحدث في اللغة.)
خطأ شائع رقم 1: إساءة استخدام التعبيرات كإعدادات افتراضية للوسائط الوظيفية
يسمح لك البايثون بتحديد أن وسيطة الوظيفة اختيارية من خلال توفير قيمة افتراضية لها.
في حين أن هذه ميزة رائعة للغة ، إلا أنها قد تؤدي إلى بعض الالتباس عندما تكون القيمة الافتراضية قابلة للتغيير.
على سبيل المثال ، ضع في الاعتبار تعريف دالة البايثون:
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() مرارًا وتكرارًا (أي دون تحديد وسيطة شريطية) سيعود دائمًا إلى "baz" ، لأن الافتراض سيكون في كل مرة يتم استدعاء foo () (بدون شريط يتم تعيين شريط الوسيطة) إلى [] (أي ، قائمة فارغة جديدة).
ولكن دعونا نلقي نظرة على ما يحدث بالفعل عند القيام بذلك:
foo()
["baz"]
foo()
["baz", "baz"]
foo()
["baz", "baz", "baz"]
هاه؟ لماذا استمرت في إلحاق القيمة الافتراضية لـ "baz" بقائمة حالية في كل مرة تم استدعاء foo () بدلاً من إنشاء قائمة جديدة في كل مرة؟
إجابة برمجة Python الأكثر تقدمًا هي أن القيمة الافتراضية لوسيطة دالة يتم تقييمها مرة واحدة فقط ، في الوقت الذي يتم فيه تعريف الوظيفة.
وبالتالي ، تتم تهيئة وسيطة الشريط إلى وضعها الافتراضي (أي ، قائمة فارغة) فقط عندما يتم تعريف foo () لأول مرة ، ولكن بعد ذلك ستستمر المكالمات إلى foo () (أي بدون وسيطة شريطية محددة) في استخدام نفس القائمة ل أي شريط تم تهيئته في الأصل.
لمعلوماتك ، للمشكلة الشائعة لهذا كالتالي:
def foo(bar=None):
... if bar is None: # or if not bar: ... bar = [] ... bar.append("baz") ... return bar ...
foo() ["baz"] foo() ["baz"] foo() ["baz"]
خطأ شائع # 2: استخدام متغيرات الفئة بشكل غير صحيح النظر في المثال التالي:
class A(object): ... x = 1 ... class B(A): ... pass ... class C(A): ... pass ... print A.x, B.x, C.x 1 1 1 من المنطقي. B.x = 2 print A.x, B.x, C.x 1 2 1 نعم ، مرة أخرى كما هو متوقع. A.x = 3 print A.x, B.x, C.x 3 2 3 ما هي $٪ #! & ؟؟ لقد غيرنا فقط لماذا تغيرت C.x أيضا؟
في Python ، تتم معالجة متغيرات الفصل داخليًا كقواميس وتتبع ما يشار إليه عادةً طريقة قرار القرار (MRO). لذلك في التعليمة البرمجية أعلاه ، نظرًا لعدم العثور على السمة x في الفئة C ، سيتم البحث عنها في فئاتها الأساسية (فقط A في المثال أعلاه ، على الرغم من أن Python تدعم الوراثة المتعددة). بمعنى آخر ، ليس لدى C خاصية x خاصة بها ، مستقلة عن A. وبالتالي ، فإن الإشارات إلى C.x هي في الواقع إشارات إلى A.x. هذا يسبب مشكلة بايثون ما لم يتم التعامل معها بشكل صحيح. تعرف على المزيد حول سمات الفصل في بيثون.
خطأ شائع # 3: تحديد المعلمات بشكل غير صحيح لكتلة استثناء افترض أن لديك الكود التالي:
try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError: # To catch both exceptions, right? ... pass ... Traceback (most recent call last): File "", line 3, in IndexError: list index out of range
المشكلة هنا هي أن العبارة باستثناء لا تأخذ قائمة من الاستثناءات المحددة بهذه الطريقة. بدلاً من ذلك ، في Python 2.x ، يستخدم بناء الجملة باستثناء Exception ، e لربط الاستثناء بالمعلمة الثانية الاختيارية المحددة (في هذه الحالة هـ) ، من أجل إتاحتها لمزيد من الفحص. نتيجة لذلك ، في التعليمة البرمجية أعلاه ، لا يتم اكتشاف استثناء IndexError بالعبارة باستثناء ؛ بدلاً من ذلك ، ينتهي الاستثناء بدلاً من ذلك إلى ربط معلمة باسم IndexError.
تتمثل الطريقة المناسبة لالتقاط استثناءات متعددة في عبارة ما عدا في تحديد المعلمة الأولى كلعبة تحتوي على جميع الاستثناءات التي سيتم اكتشافها. أيضًا ، لتحقيق أقصى قدر من قابلية النقل ، استخدم الكلمة الأساسية ، نظرًا لأن بناء الجملة مدعوم من كل من Python 2 و Python 3:
try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e:
... pass ...خطأ شائع رقم 4: سوء فهم قواعد نطاق بايثون
تعتمد دقة نطاق Python على ما يعرف باسم قاعدة LEGB ، وهو اختصار لـ Local ، Enclosure ، Global ، Built-in. يبدو واضحا بما فيه الكفاية ، أليس كذلك؟ حسنًا ، في الواقع ، هناك بعض التفاصيل الدقيقة للطريقة التي يعمل بها هذا في بيثون ، وهو ما يقودنا إلى مشكلة بيثون الأكثر شيوعًا في البرمجة أدناه. النظر في ما يلي:
x = 10 def foo(): ... x += 1 ... print x ... foo() Traceback (most recent call last): File "", line 1, in File "", 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 "", line 1, in File "", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment
هاه؟ لماذا قنبلة foo2 بينما foo1 بخير؟
الجواب هو نفسه كما في مشكلة المثال السابق ولكن من المسلم به أنه أكثر دقة. foo1 لا يقوم بواجب إلى lst ، بينما foo2 هو. تذكر أن lst + = [5] هي في الحقيقة اختصار لـ lst = lst + [5] ، نرى أننا نحاول تعيين قيمة إلى lst (وبالتالي يفترض أن بايثون تكون في النطاق المحلي). ومع ذلك ، فإن القيمة التي نتطلع إلى تعيينها إلى lst تستند إلى lst نفسها (مرة أخرى ، يُفترض الآن أنها في النطاق المحلي) ، والتي لم يتم تحديدها بعد.
خطأ شائع رقم 5: تعديل قائمة أثناء التكرار يجب أن تكون المشكلة في التعليمات البرمجية التالية واضحة إلى حد ما:
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 "", line 2, in IndexError: list index out of range يعد حذف عنصر من قائمة أو صفيف أثناء التكرار فوقه مشكلة بيثون معروفة لأي مطور برامج ذي خبرة. ولكن على الرغم من أن المثال أعلاه قد يكون واضحًا إلى حد ما ، إلا أنه حتى المطورين المتقدمين يمكن أن يلدغوا عن غير قصد في هذا الكود الأكثر تعقيدًا.
لحسن الحظ ، تضم 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] خطأ شائع رقم 6: الخلط بين كيفية ربط بيثون المتغيرات في عمليات الإغلاق
النظر في المثال التالي:
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 مفاجأة!
يحدث هذا بسبب سلوك الربط المتأخر لبيثون الذي يقول إن قيم المتغيرات المستخدمة في عمليات الإغلاق يتم بحثها في الوقت الذي يتم فيه استدعاء الوظيفة الداخلية. لذلك في التعليمة البرمجية أعلاه ، كلما تم استدعاء أي من الوظائف التي تم إرجاعها ، يتم البحث عن قيمة 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
نحن نستفيد من الوسائط الافتراضية هنا لإنشاء وظائف مجهولة من أجل تحقيق السلوك المطلوب. قد يسميه البعض هذا الأنيق. البعض يسميها خفية. يكره البعض ذلك. ولكن إذا كنت مطورًا بيثون ، فمن المهم أن تفهمه على أي حال.
خطأ شائع رقم 7: إنشاء تبعيات وحدة دائرية
دعنا نقول أن لديك ملفين ، a.py و b.py ، يستورد كل منهما الآخر ، كما يلي:
في a.py: import b
def f(): return b.x
print f() وفي b.py: import a
x = 1
def g(): print a.f() أولاً ، دعونا نحاول استيراد a.py:
import a 1 عملت على ما يرام. ربما هذا يفاجئك. بعد كل شيء ، لدينا استيراد دائري هنا يفترض أنه ينبغي أن يكون مشكلة ، أليس كذلك؟
الجواب هو أن مجرد وجود استيراد دائري ليس بحد ذاته مشكلة في بيثون. إذا تم استيراد وحدة نمطية بالفعل ، فإن بايثون ذكي بما يكفي لعدم محاولة إعادة استيراده. ومع ذلك ، بناءً على النقطة التي تحاول كل وحدة من خلالها الوصول إلى الوظائف أو المتغيرات المحددة في الأخرى ، قد تواجه بالفعل مشكلات.
لذا ، بالعودة إلى مثالنا ، عندما قمنا باستيراد a.py ، لم يكن لديها مشكلة في استيراد b.py ، لأن b.py لا يتطلب أي شيء من a.py ليتم تعريفه في وقت استيراده. المرجع الوحيد في b.py إلى a هو استدعاء a.f (). ولكن هذه الدعوة في g () ولا شيء في a.py أو b.py يستدعي g (). لذا الحياة جيدة.
ولكن ماذا يحدث إذا حاولنا استيراد b.py (دون استيراد a.py مسبقًا ، أي):
import b Traceback (most recent call last): File "", line 1, in File "b.py", line 1, in import a File "a.py", line 6, in print f() File "a.py", line 4, in f return b.x AttributeError: 'module' object has no attribute 'x' هذا ليس جيدا! المشكلة هنا هي أنه في عملية استيراد b.py ، يحاول استيراد a.py ، والذي بدوره يستدعي f () ، والذي يحاول الوصول إلى b.x. لكن b.x لم يتم تعريفها بعد. ومن هنا استثناء AttributeError.
حل واحد على الأقل لهذا هو تافهة جدا. ما عليك سوى تعديل b.py لاستيراد a.py خلال g (): x = 1
def g(): import a # This will be evaluated only when g() is called print a.f() لا ، عندما نستوردها ، كل شيء على ما يرام:
import b b.g() 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'
خطأ شائع رقم 8: اشتباك الاسم مع وحدات مكتبة بيثون القياسية
واحدة من محاسن بيثون هي ثروة من وحدات المكتبة التي تأتي مع "خارج الصندوق". لكن كنتيجة لذلك ، إذا لم تكن تتجنب ذلك عن وعي ، فليس من الصعب أن تصطدم بصدام اسم بين اسم إحدى الوحدات النمطية ووحدة نمطية بنفس الاسم في المكتبة القياسية التي تأتي مع Python (على سبيل المثال ، قد يكون لديك وحدة نمطية تحمل عنوان البريد الإلكتروني. py في الكود ، والتي تتعارض مع وحدة المكتبة القياسية التي تحمل الاسم نفسه).
قد يؤدي هذا إلى مشاكل نادرة ، مثل استيراد مكتبة أخرى والتي بدورها تحاول استيراد إصدار مكتبة Python القياسية من وحدة نمطية ، ولكن نظرًا لأن لديك وحدة نمطية بنفس الاسم ، فإن الحزمة الأخرى تقوم باستيراد نسختك عن طريق الخطأ بدلاً من تلك الموجودة داخل مكتبة بيثون القياسية. هذا هو المكان الذي تحدث فيه أخطاء بايثون السيئة.
لذلك يجب توخي الحذر لتجنب استخدام نفس الأسماء الموجودة في وحدات مكتبة بيثون القياسية. من الأسهل بالنسبة لك تغيير اسم وحدة نمطية ضمن الحزمة الخاصة بك مما هو عليه الحال في تقديم اقتراح تحسين بيثون (PEP) لطلب تغيير الاسم في المرحلة الأولى ومحاولة الحصول على الموافقة عليه.
خطأ شائع رقم 9: الفشل في معالجة الاختلافات بين بيثون 2 وبيثون 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()
في بيثون 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 bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment
ما حدث للتو هنا؟ "المشكلة" هي أنه في Python 3 ، لا يمكن الوصول إلى كائن الاستثناء خارج نطاق الكتلة باستثناء. (والسبب في ذلك هو ، وإلا ، فسيحتفظ بدورة مرجعية مع إطار مكدس الذاكرة حتى يتم تشغيل أداة تجميع مجمعي البيانات المهملة وتطهير المراجع من الذاكرة.
إحدى الطرق لتجنب هذه المشكلة هي الاحتفاظ بمرجع إلى كائن الاستثناء خارج نطاق الكتلة باستثناء بحيث يمكن الوصول إليها. إليك نسخة من المثال السابق الذي يستخدم هذه التقنية ، مما يؤدي إلى الحصول على كود سهل الاستخدام لكل من 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 ييب!
(بالمناسبة ، يناقش دليل توظيف بيثون لدينا عددًا من الاختلافات المهمة الأخرى التي يجب معرفتها عند ترحيل الكود من بيثون 2 إلى بيثون 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 بشع.
لماذا ا؟ عندما يتم إيقاف تشغيل المترجم الفوري ، يتم تعيين جميع المتغيرات العامة للوحدة إلى بلا. نتيجة لذلك ، في المثال أعلاه ، عند استدعاء __del__ ، تم بالفعل تعيين الاسم foo على بلا.
قد يكون الحل لمشكلة برمجة 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 ، لكنك تحصل على الفكرة.
يتم إحتوائه
البايثون هي لغة قوية ومرنة مع العديد من الآليات والنماذج التي يمكن أن تحسن كثيرا من الإنتاجية.
كما هو الحال مع أي أداة أو لغة برمجية ، فإن امتلاك قدر محدود من الفهم أو التقدير لقدراتها قد يكون في بعض الأحيان عائقًا أكثر من كونه منفعة ، تاركًا واحدة في حالة مثل "معرفة ما يكفي لتكون خطيرًا".
إن التعرّف على الفروق الدقيقة في بيثون ، مثل (على سبيل المثال لا الحصر) مشاكل البرمجة المعتدلة التي أثيرت في هذه المقالة ، سوف يساعد على تحسين استخدام اللغة مع تجنب بعض الأخطاء الشائعة.
قد ترغب أيضًا في الاطلاع على دليل المطلعين على بيثون لإجراء المقابلات للحصول على اقتراحات بشأن أسئلة المقابلة التي يمكن أن تساعد في تحديد خبراء بيثون.
نأمل أن تكون المؤشرات الموجودة في هذه المقالة مفيدة ونرحب بتعليقاتك.
نبذة عن الكاتب:
عمل مارتن كمطور محترف لبيثون منذ عام 2007 ، على الرغم من أن مسيرته المهنية في تكنولوجيا المعلومات قد بدأت في عام 2001. وهو مهندس مكدس ، حيث يدير أنظمة التشغيل والشبكات لسنوات عديدة. اهتماماته الأخيرة هي تطوير الويب وبيثون (وبالتحديد مع Django).
رابط المقالة باللغة الانجليزية:
التعليقات