اذهب إلى المحتوى

ثلاثة أخطاء عليك تفاديها عند تعلم البرمجة بلغة بايثون


عبد اللطيف ايمش

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

الخطأ الأول: استخدام أنواع البيانات القابلة للتغير كوسائط افتراضية عند تعريف الدالة

لنقل أنَّ لديك دالة صغيرة التي تبحث عن روابط في الصفحة الحالية وتستطيع إضافتها إلى قائمة (list) معيّنة.

def search_for_links(page, add_to=[]):
    new_links = page.search_for_links()
    add_to.extend(new_links)
    return add_to

لا يبدو أنَّ هنالك أيّ خطأ في الدالة السابقة، وهذا صحيح، فهي تعمل لكن هنالك مشاكل فيها؛ فلو مررنا قائمةً (list) إلى المعامل add_to فستعمل كما يجب، لكن ماذا يحدث لو تركنا القيمة الافتراضية دون تعديل؟
جرِّب تنفيذ الشيفرة الآتية:

def fn(var1, var2=[]):
    var2.append(var1)
    print var2

fn(3)
fn(4)
fn(5)

ستتوقع رؤية الناتج الآتي:

[3]
[4]
[5]

لكنك ستُفاجأ بالناتج الآتي:

[3]
[3, 4]
[3, 4, 5]

لماذا؟! يمكنك أن تستنتج أن القائمة (list) نفسها ستستعمل في كل مرة، فعندما نكتب دالة مثل الدالة السابقة في بايثون فسيتم تهيئة القائمة (list) كجزءٍ من تعريف الدالة، أي أنها لن تُهيّئ كل مرة تُستدعى فيها الدالة، وهذا يعني أنَّ الدالة ستحتفظ بكائن القائمة نفسه مرارًا وتكرارًا، ما لم تُحدِّد قيمةً أخرى له:

fn(3, [4])

الناتج:

[4, 3]

الناتج يماثل ما قد توقعناه.
الطريقة الصحيحة لفعل ذلك هي:

def fn(var1, var2=None):
    if not var2:
        var2 = []
    var2.append(var1)

لنعد كتابة الدالة السابقة:

def search_for_links(page, add_to=None):
    if not add_to:
        add_to = []
    new_links = page.search_for_links()
    add_to.extend(new_links)
    return add_to

نقلنا عملية التهيئة من مكان تعريف الدالة إلى داخلها مما يعني أنَّ عملية التهيئة ستتم في كل مرة تُشغَّل فيها الدالة.
لاحظ أنَّ ذلك ليس ضروريًا إذا كنتَ تستعمل أنواع بيانات غير قابلة للتعديل مثل tuple أو string أو int. وهذا يعني أنَّك تستطيع تعريف دالة كما في الدالة الآتية دون إمكانية حدوث أخطاء غير متوقعة:

def func(message="my message"):
    print message

الخطأ الثاني: استخدام أنواع البيانات القابلة للتعديل كمتغيرات في الأصناف

هذا الخطأ شبيهٌ كثيرًا بالخطأ السابق. تمعّن في الشيفرة الآتية:

class URLCatcher(object):
    urls = []

    def add_url(self, url):
        self.urls.append(url)

الشيفرة السابقة تبدو طبيعية جدًا، فلدينا كائن لتخزين روابط URL، وعند استدعائنا للدالة add_url فسنمرر إليها رابط URL لتخزِّنه، صحيح؟ لنجرِّبها:

a = URLCatcher()
a.add_url('http://www.google.')
b = URLCatcher()
b.add_url('http://www.bbc.co.')

الناتج:

b.urls
['http://www.google.com', 'http://www.bbc.co.uk']

a.urls
['http://www.google.com', 'http://www.bbc.co.uk']

ما هذا؟! لم نتوقع ذلك. إذ أنشأنا كائنين منفصلين a و b، وأسندنا رابطًا للكائن a مختلفًا عن رابط الكائن b، فكيف امتلك كلا الكائنين الرابطين نفسهما؟
اتضح أنَّ هذه المشكلة شبيهة جدًا بالمشكلة في المثال الأول، فقائمة (list) عناوين URL قد تمت تهيئتها عند تعريف الصنف (class)، وبالتالي أمست جميع الكائنات المُنشَأة من ذاك الصنف تستعمل القائمة نفسها.
هنالك بعض الحالات التي نستفيد فيها من هذه الميزة، لكنها ستضرك في أغلبية الأوقات، فلو أردتَ تخزين بيانات كل كائن على حدة فيمكنك تعديل الشيفرة لتصبح كما يلي:

class URLCatcher(object):
    def __init__(self):
        self.urls = []

    def add_url(self, url):
        self.urls.append(url)

أصبحت قائمة urls تُهيّئ عند إنشاء الكائن، وعندما نُنشِئ كائنين فستُهيّئ قائمتان منفصلتان.

الخطأ الثالث: عملية إسناد قيم إلى نوع بيانات قابل للتعديل

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

a = {'1': "one", '2': 'two'}

لنفترض أننا نريد أخذ قيمة المتغير a واستعمالها في مكانٍ آخر دون تعديل القيمة الأصلية:

b = a

b['3'] = 'three'

أليس هذا بسيطًا؟ لننظر الآن إلى القيمة المخزّنة في المتغير a التي لم نُعدِّلها قط:

{'1': "one", '2': 'two', '3': 'three'}

ماذا؟! كيف ستبدو قيمة المتغير b إذًا؟

{'1': "one", '2': 'two', '3': 'three'}

دعنا نعود خطوةً إلى الوراء وننظر ماذا يحدث لو استعملنا أنواع البيانات غير القابلة للتعديل، مثل tuple:

c = (2, 3)
d = c
d = (4, 5)

قيمة c هي:

(2, 3)

بينما قيمة d هي:

(4, 5)

لقد جرى كل شيءٍ على ما يرام، لذا ماذا حدث في مثالنا؟ عند استخدام أنواع البيانات القابلة للتعديل فسنحصل على شيءٍ شبيهٍ بالمؤشرات (pointers) في لغة C، فعندما قلنا أنَّ b = a في الشيفرة السابقة فهذا يعني أنَّ المتغير b أصبح يُشير إلى a، وكلا المتغيرين يشير إلى نفس الكائن في ذاكرة بايثون؟ هل هذا مألوف لديك؟ ذلك لأن هذه المشكلة شبيهة بالمشاكل السابقة، وكنتُ أنوي تسمية هذا الدرس باسم «المشاكل التي تحدث مع أنواع البيانات القابلة للتعديل».
هل يحدث الأمر نفسه مع القوائم (list)؟ نعم. وكيف سنلتف على المشكلة؟ حسنًا، يجب أن نكتب الشيفرة الآتية التي تنسخ القائمة:

b = a[:]

السطر السابق سيؤدي إلى نسخ مرجعية كل عنصر من عناصر القائمة ووضعه في قائمة جديدة، لكن لنأخذ حِذرنا فإذا كان نوع بيانات أحد الكائنات الموجودة في القائمة قابلًا للتعديل فسيؤدي ذلك إلى الحصول إلى مرجعية لتلك الكائنات بدلًا من نسخها.
تخيل وجود قائمة على قطعة من الورق، ففي المثال الأصلي كان ينظر الشخص A والشخص B إلى الورقة نفسها، فلو عدّل شخصٌ ما القائمةَ فسيرى كلا الشخصين التعديلات التي أجريت على القائمة، وعندما نسخنا المرجعيات فأصبح لكل شخصٍ قائمته الخاصة به، لكن لنفترض أنَّ تلك القائمة تحتوي على أماكن يمكن البحث فيها عن طعام، فلو كانت «الثلاجة» موجودة في القائمة فحتى لو نسخها الشخص A و B فما تزال تشير إلى الثلاجة نفسها؛ فلو أتى الشخص A وعدّل محتويات الثلاثة (لنفترض أنه أكل جميع الحلويات فيها) فسيلاحظ الشخص B أن الحلويات قد اختفت من الثلاثة. ولا توجد طريقة سهلة للالتفاف على هذه المشكلة، وهذا أمرٌ مهمٌ عليك تذكره عندما تبرمج لكي تكتب شيفرتك بطريقة لا تسبِّب أيّة مشاكل.
تعمل أنواع dict بنفس الطريقة، ويمكنك إنشاء نسخة كاملة باستعمال الدالة copy()‎:

b = a.copy()

أكرِّر أنَّ ذلك سيُنشِئ متغيرًا جديدًا من نوع dict يُشير إلى نفس العناصر الموجودة في المتغير الأصلي، وبالتالي لو كان لدينا قائمتان متماثلتين وعدّلنا كائنًا قابلًا للتعديل مُشار إليه عبر مفتاح موجود في المتغير a فيمكن معرفة تلك التعديلات من داخل المتغير b.
الإشكاليات التي تواجهنا مع أنواع البيانات القابلة للتعديل تكون نتيجةً لمرونة تلك الأنواع، حيث لا تُشكِّل أيٌّ مما سبق مشكلةً حقيقة، وإنما هي أمور ضرورية يجب أخذها بالحسبان لتنجب المشاكل. وعمليات النسخ الكاملة التي ذكرناها آنفًا لن تكون ضروريةً في 99% من الحالات، أي يجب تعديل برنامجك لكي لا يحتاج إلى استخدام تلك النسخ من الأساس.
ترجمة –وبتصرّف– للمقال ‎3 mistakes to avoid when learning to code in Python لصاحبه Pete Savage


تفاعل الأعضاء

أفضل التعليقات

بالنسبه للاخطاء كماذكرت لاتعد اخطاء وانما هو الاصل.

قد يكون لبس نظرا لانه يعرف لغه اخرى. 

 

بالنسبه للمشكله الاولى الجواب الغير متوقع هو المفترض لانه يظهر فائده المصفوف في التخزين والا مالفائدة. نجعله متغير وتنتهي المشكله.

واذا اردنا ان تكون مصفوفه تظهر النتيجه الحاليه فقط كما في الحل

def fn(var1, var2=None):
    if not var2:
        var2 = []
    var2.append(var1)

اليس هذا افضل ويؤدي الى نفس النتيجة؟ 

def fn(var1,var2=[]):
	var2=[var1]
	print[var2]

 

والمتغير العادي يؤدي الى نفس النتيجه فلماذا نلف وندور طالما اننا لن نستخدم المصفوفه استخدامها الحقيقي وانما نستخدمها كمتغير؟؟

رابط هذا التعليق
شارك على الشبكات الإجتماعية

شكرا أخي عبد اللطيف

أنا من مبرمجي السي شارب وانتقلت حديثا للبايثون 3

الخطأ الأول منطقي جدا , أما الخطأ الثاني فتعلمته منك في هذه اللحظة , أما الخطأ الثالث فأزعجني كثيرا

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

كيف أصنع تطبيق قليل استهلاك للموارد _(لا أقصد Garbage Collector)

رابط هذا التعليق
شارك على الشبكات الإجتماعية



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...