9 اشتباه متداول بین برنامه نویسان پایتون - قسمت اول
سینتکس به ظاهر ساده پایتون می تواند برنامه نویسانی که از این زبان استفاده می کنند، علی الخصوص برنامه نویسان تازه کار، را دراستفاده مناسب از دستورات این زبان گیج کند.این گیجی می تواند باعث شود که از بعضی ریزه کاری های این زبان نادانسته رد شوند و از تمام قدرت این زبان استفاده نکنند.
برای آشنایی با این ریزه کاری ها و اشکالات متداول در برنامه های نوشته شده در پایتون، در این نوشته به 9 تا از این اشتباهات که حتی ممکن است توسط افراد خبره تر هم انجام شود می پردازیم.
اشتباه 1 – استفاده اشتباه از ورودی های پیش فرض توابع
پایتون به شما این انتخاب را می دهد که مشخص کنید که ورودی های یک تابع اختیاری هستند. این کار با مشخص کردن یک مقدار پیش فرض برای آن ورودی امکان پذیر است. با اینکه داشتن این انتخاب ویژگی بسیار خوبی است، ولی اگر مقدار این ورودی قابل تغییر باشد، می تواند باعث سردرگمی برنامه نویس شود. برای مثال تعریف تابع زیر را در نظر داشته باشید :
>>> def foo(bar=[]):
bar.append("baz")
Return bar
خط اول ما در حال تعریف تابع و ورودی آن هستیم. bar نامی است که برای متغیر ورودی تابع در نظر گرفته ایم و مقدار پیش فرض آن (مقداری که اگر در هنگام استفاده از تابع به آن ورودی ندهیم به عنوان ورودی پیش فرض در نظر گرفته می شود) را برابر [](لیست خالی، تهی) در نظر گرفته ایم. خط دوم تابع کاری است که می خواهیم بر روی ورودی انجام دهیم و خط سوم برای برگرداندن خروجی تابع است.
اشتباهی که در این حالت صورت می گیرد تصور این است هر دفعه تابع را بدون ورودی استفاده می کنیم، ورودی ما با مقدار پیش فرض برابر خواهد بود. مثلا اگر در ادامه کد بالا چندبار تابع ()foo را بدون دادن ورودی استفاده کنیم، از آنجایی که تصور این است که هردفعه که تابع را بدون ورودی استفاده می کنیم ورودی برابر مقدار پیش فرضش (یک لیست تهی) خواهد بود، پس انتظار می رود که در هر دفعه خروجی ما همواره لیستی شامل عبارت 'baz' باشد( چون ورودی ما با اسم bar یک لیست تهی است و در خط دوم کد به انتهای آن لیست عبارت 'baz' را اضافه می کنیم انتظار می رود خروجی ما لیستی که تنها شامل عبارت'baz' است، باشد).
حال اگر این کار را در واقعیت انجام دهیم (تابع را چندبار بدون مقدار ورودی صدا بزنیم)، نتیجه به شکل زیر خواهد بود :
>>> foo()
["baz"]
>>> foo()
["baz", "baz"]
>>> foo()
["baz", "baz", "baz"]
چی شد؟ چرا به جای اینکه هردفعه فقط یک لیست شامل عبارت 'baz' برگردونه، هردفعه عبارت 'baz' رو به یک لیست قدیمی اضافه می کنه؟
جواب این سوال این هستش که مقدار پیش فرض ورودی یک تابع فقط یک بار در نظر گرفته می شود و این کار در زمانی که تابع تعریف می شود اتفاق می افتد. به همین علت مقدار پیش فرض ورودی ما فقط در هنگامی که تابع در کد تعریف می شود، برابر یک لیست تهی است و در استفاده اول از این لیست خالی استفاده خواهد شد. در دفعات بعدی دیگر یک لیست خالی ساخته نمی شود، بلکه از همان لیست قدیمی که دارای عضو است استفاده می شود. راه حل معمول این مشکل، نوشتن کد به شکل زیر است :
>>> def foo(bar=None):
... if bar is None:
... bar = []
... bar.append("baz")
... return bar
که باعث می شود در هردفعه صدا کردن این تابع، از یک لیست تهی به عنوان ورودی پیش فرض استفاده شود.
اشتباه 2 – استفاده نامناسب از متغیر های کلاس (class variables)
کد زیر رو در نظر بگیرید :
>>> class A(object):
... x = 1
...
>>> class B(A):
... pass
...
>>> class C(A):
... pass
حال اگر دستور زیر رو وارد کنیم :
>>> print A.x, B.x, C.x
خروجی ها به این شکل خواهند بود:
1 1 1
که کاملا مطابق انتظارات است و مشکلی ندارد.
اگر در ادامه متغیر x یکی از دو کلاس B و یا C را تغییر دهیم و دوباره دستور بالا را وارد کنیم :
>>> B.x = 2
>>> print A.x, B.x, C.x
1 2 1
می بینیم که خروجی ما مشکلی ندارد و مطابق انتظار ماست.
ولی اگر مقدار متغیر x برای کلاس A را تغییر دهیم و همین کار را تکرار کنیم می بینیم که نتیجه متفاوت خواهد بود.
>>> A.x = 3
>>> print A.x, B.x, C.x
3 2 3
چی شد؟ ما که تنها متغیر xبرای کلاس A را تغییر داده ایم، چرا مقدار C.xهم تغییر کرد؟
در کد بالا از آنجایی که متغیر xبرای کلاس C تعریف نشده است، برای این ویژگی در کلاس های پایه ای آن جستجو خواهد شد (که در مثال بالا در کلاس A پیدا خواهد شد). به عبارت دیگر، متغیر xکلاس C یک متغیر وابسته به کلاس Aخواهد بود و درواقع فراخوانی متغیر C.x باعث فراخوانی A.x خواهد شد.
این مشکل به این علت برای کلاس B در قسمت دوم کد بوجود نیامد که به آن در قسمت دوم دستورات به طور جداگانه و مستقیم مقداری اختصاص داده شد.
اشتباه 3 – به اشتباه مشخص کردن پارامترهای یک دستور استثنا (exception) در کد
کد زیر را در نظر بگیرید
>>> try:
... l = ["a", "b"]
... int(l[2])
... except ValueError, IndexError:
... pass
قسمت جلوی exceptبرای گرفتن دو ارورValueError وIndex Error نوشته شده است و مشکلی ندارد دیگر، نه؟
وقتی کد را اجرا کنیم با پیام زیر روبه رو خواهیم شد :
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
IndexError: list index out of range
پس مشکل ما چیست؟
مشکل این است که دستور except یک لیست از مشکلات را به این نحو دریافت نمی کند.
برای این کار باید یک tuple معرفی کرده که در آن تمامی مشکلات مورد نظر موجود باشند. برای کمتر کردن مشکلات احتمالی و راحت تر کردن انتقال کد از یک as در انتهای کد نیز می توانید استفاده کنید، با این کار این دستور هم با پایتون 2 و هم پایتون 3 سازگار خواهد بود. بدین شکل :
>>> try:
... l = ["a", "b"]
... int(l[2])
... except (ValueError, IndexError) as e:
... pass
اشتباه 4 – تغییردادن یک لیست در حین خواندن آن
کد زیر را در نظر داشته باشید :
>>> 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]
اگر این کد را اجرا کنیم، با ارورهای زیر روبه رو خواهیم شد:
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
IndexError: list index out of range
مشکل این کد چیست؟
مشکل این کد در خط 4 آن است. دستور آن خط جسمی که ما در حال خواندن از روی آن هستیم را حذف می کند که باعث خطا در اجرای کد می شود. این اشتباه بین افرادی که بیشتر با پایتون آشناتر هستند شناخته شده است، ولی گاهی ممکن است حتی آنان نیز به اشتباه این کار را انجام دهند.
در پایتون راه های زیادی برای جلوگیری از این خطا داریم. یکی از راه هایی که برای جلوگیری از این مشکل داریم نوشتن کد بالا به نحوه زیر است :
>>> odd = lambda x : bool(x % 2)
>>> numbers = [n for n in range(10)]
>>> numbers[:] = [n for n in numbers if not odd(n)]
با نوشتن کد به این روش دیگر یک عضو یک لیست را در حین خواندن حذف نمی کنیم. بلکه در حین خواندن لیست یک لیست ثانویه از اعدادی که شرایط ما را شامل می شوند می سازیم و در نهایت، بعد از اتمام خواندن، لیست اولیه را تماما با لیست ثانویه جایگزین می کنیم (اعضای لیست ثانویه را در لیست اولیه میریزیم).
اشتباه 5 – دقت نکردن به نحوه ای که پایتون با متغیرها برخورد می کند
کد زیر را در نظر داشته باشید :
>>> 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
عجیبه! چرا؟
این اتفاق به دلیل نحوه برخورد پایتون با متغیر هاست. این نحوه برخورد باعث می شود مقدار متغیرهایی که در یک قسمت استفاده می شوند، تازه در هنگامی درخواست شوند که تابع شروع به کار میکند. پس در کد بالا، هردفعه که قسمت دوم نتیجه return را درخواست می کند مقدار متغیر در لحظه درخواست به آن تحویل داده می شود، که در نتیجه کامل شدن حلقه قسمت اول قبل از دومی برابر 4 خواهد بود.
راه حل این مشکل نوشتن کد به صورت زیر خواهد بود :
>>> def create_multipliers():
... return [lambda x, i=i : i * x for i in range(5)]
...
>>> for multiplier in create_multipliers():
... print multiplier(2)
که به ما خروجی موردنظر را خواهد داد.
امیر هستم از ایران پایتونیرز، ممنونم از وقتی که برای خوندن قسمت اول این پست گذاشتید.
در این قسمت به 5 تا از 9 اشتباه متداولی که برنامه نویسان پایتون انجام میدن پرداختیم. در قسمت دوم این پست باقی این اشتباهات رو برای شما می نویسیم.
ما به تازگی استفاده از پلتفرم ویرگول رو شروع کردیم و به زودی مطالب بیشتری هم اینجا قرار داده میشه.
برای دیدن ویدیو های آموزشی دیگری که دوستانم برای شما ساخته اند، می تونید به کانال ما در آپارات ، یوتیوب و یا تلگرام مراجعه کنید.
منبع : www.toptal.com
مطلبی دیگر از این انتشارات
معرفی 5 ابزار برای کمک به دیباگ در برنامه های پایتون
مطلبی دیگر از این انتشارات
9 تابع پیش فرض در پایتون که باید با آنها آشنا باشید - قسمت دوم
مطلبی دیگر از این انتشارات
تغییرات پایتون 3.9