پرسشهای متداول دربارهی طراحی و تاریخچه¶
چرا پایتون از تورفتگی برای گروهبندی دستورها استفاده میکند؟¶
گیدو ون روسوم معتقد است که استفاده از تورفتگی برای گروهبندی بسیار ظریف است و سهم زیادی در وضوح برنامههای معمول پایتون دارد. بسیاری از افراد پس از مدتی به این ویژگی علاقهمند میشوند.
از آنجا که هیچ براکت آغاز/پایانی وجود ندارد، هیچ ناهماهنگیای میان گروهبندی از دید پارسر و خوانندهی انسانی وجود نخواهد داشت. گاهی برنامهنویسان C با قطعهکدی مانند این مواجه میشوند:
if (x <= y)
x++;
y--;
z++;
در صورت برقرار بودن شرط، تنها دستور x++ اجرا میشود، اما تورفتگی باعث میشود بسیاری برداشت دیگری داشته باشند. حتی برنامهنویسان باتجربهی C نیز گاهی مدتها به آن خیره میشوند و از خود میپرسند که چرا y حتی برای x > y کاهش مییابد.
چون براکتهای آغاز/پایان وجود ندارند، پایتون بسیار کمتر در معرض تعارضهای سبک کدنویسی است. در C روشهای مختلفی برای قرار دادن آکولادها وجود دارد. پس از آنکه به خواندن و نوشتن کد با یک سبک خاص عادت کردید، طبیعی است که هنگام خواندن (یا ملزم بودن به نوشتن) با سبکی متفاوت، تا حدی احساس ناراحتی کنید.
سبکهای کدنویسی بسیاری، براکتهای begin/end را در یک خط جداگانه قرار میدهند. این کار برنامهها را بهطور قابلتوجهی طولانیتر میکند و فضای ارزشمند صفحه را هدر میدهد و در نتیجه داشتن دید کلی خوب از برنامه را دشوارتر میسازد. در حالت ایدهآل، یک تابع باید در یک صفحه جا شود (مثلاً ۲۰ تا ۳۰ خط). ۲۰ خط پایتون میتواند کار بسیار بیشتری نسبت به ۲۰ خط C انجام دهد. این موضوع تنها به دلیل نبود براکتهای begin/end نیست — نبود اعلانها و انواع داده سطح بالا نیز در آن نقش دارند — اما سینتکس مبتنی بر تورفتگی قطعاً کمک میکند.
چرا هنگام انجام عملیات حسابی ساده، نتایج عجیبی دریافت میکنم؟¶
پرسش بعدی را ببینید.
چرا محاسبات ممیز شناور اینقدر نادقیق هستند؟¶
کاربران اغلب از نتایجی مانند این شگفتزده میشوند:
>>> 1.2 - 1.0
0.19999999999999996
و فکر میکنند که این یک باگ در پایتون است. اینطور نیست. این موضوع ارتباط کمی با پایتون دارد و ارتباط بسیار بیشتری با نحوهی مدیریت اعداد ممیز شناور توسط پلتفرم زیرین دارد.
نوع float در CPython برای ذخیرهسازی از یک double در C استفاده میکند. مقدار یک شیء float بهصورت ممیز شناور دودویی با دقت ثابت (معمولاً ۵۳ بیت) ذخیره میشود و پایتون برای انجام عملیات ممیز شناور از عملیات C استفاده میکند، که خود به پیادهسازی سختافزاری در پردازنده متکی هستند. این بدان معناست که تا جایی که به عملیات ممیز شناور مربوط میشود، پایتون مانند بسیاری از زبانهای محبوب از جمله C و Java رفتار میکند.
بسیاری از اعدادی که بهراحتی میتوان آنها را در نماد دهدهی نوشت، نمیتوانند دقیقاً در قالب ممیز شناور دودویی بیان شوند. برای مثال، پس از:
>>> x = 1.2
مقدار ذخیرهشده برای x تقریبی (بسیار خوب) از مقدار دهدهی 1.2 است، اما دقیقاً برابر با آن نیست. در یک ماشین معمول، مقدار ذخیرهشده واقعی عبارت است از:
1.0011001100110011001100110011001100110011001100110011 (دودویی)
که دقیقاً بهصورت زیر است:
1.1999999999999999555910790149937383830547332763671875 (دهدهی)
دقت معمول ۵۳ بیتی، برای اعداد اعشاری پایتون ۱۵ تا ۱۶ رقم اعشاری دقت فراهم میکند.
برای توضیح کاملتر، لطفاً فصل حساب ممیز شناور در آموزش پایتون را ببینید.
چرا رشتههای پایتون تغییرناپذیر هستند؟¶
چندین مزیت وجود دارد.
یکی از آنها کارایی است: دانستن اینکه یک رشته تغییرناپذیر است به این معناست که میتوانیم فضای آن را در زمان ایجاد تخصیص دهیم، و نیازهای ذخیرهسازی ثابت و بدون تغییر هستند. این نیز یکی از دلایل تمایز بین تاپلها و فهرستها است.
مزیت دیگر این است که رشتهها در پایتون به همان اندازهی اعداد «بنیادی» محسوب میشوند. هیچ میزان فعالیتی مقدار ۸ را به چیز دیگری تغییر نخواهد داد، و در پایتون، هیچ میزان فعالیتی رشتهی "eight" را به چیز دیگری تغییر نخواهد داد.
چرا باید از 'self' بهصراحت در تعاریف و فراخوانیهای متد استفاده شود؟¶
این ایده از Modula-3 اقتباس شده است. به دلایل گوناگونی، بسیار مفید است.
اول، آشکارتر است که شما بهجای یک متغیر محلی از یک متد یا ویژگی نمونه استفاده میکنید. خواندن self.x یا self.meth() کاملاً روشن میسازد که از یک متغیر نمونه یا متد استفاده شده است، حتی اگر تعریف کلاس را از حفظ ندانید. در C++، تا حدی میتوان از نبود اعلان متغیر محلی این موضوع را تشخیص داد (با فرض اینکه متغیرهای سراسری نادر یا بهراحتی قابلتشخیص باشند) — اما در پایتون، هیچ اعلان متغیر محلی وجود ندارد، بنابراین برای اطمینان باید به تعریف کلاس مراجعه کنید. برخی استانداردهای کدنویسی C++ و Java مقرر میدارند که ویژگیهای نمونه دارای پیشوند m_ باشند، بنابراین این صراحت در آن زبانها نیز همچنان مفید است.
دوم، این بدان معناست که اگر بخواهید بهصراحت به متدی از یک کلاس خاص ارجاع دهید یا آن را فراخوانی کنید، نیازی به سینتکس خاصی نیست. در C++، اگر بخواهید از متدی از کلاس پایه استفاده کنید که در یک کلاس مشتقشده بازنویسی شده است، باید از عملگر :: استفاده کنید — در پایتون میتوانید baseclass.methodname(self, <argument list>) را بنویسید. این موضوع بهویژه برای متدهای __init__() مفید است، و بهطور کلی در مواردی که متد کلاس مشتقشده بخواهد متد کلاس پایه با همین نام را گسترش دهد و از این رو لازم باشد متد کلاس پایه را بهنحوی فراخوانی کند.
در نهایت، برای متغیرهای نمونه، این موضوع یک مشکل نحوی در انتساب را حل میکند: از آنجا که متغیرهای محلی در پایتون (طبق تعریف!) آن متغیرهایی هستند که مقداری در بدنه یک تابع به آنها انتساب داده میشود (و بهصراحت سراسری اعلام نشدهاند)، باید راهی وجود داشته باشد تا به مفسر بگوییم که منظور از یک انتساب، انتساب به یک متغیر نمونه است نه به یک متغیر محلی، و ترجیحاً باید نحوی باشد (به دلایل کارایی). C++ این کار را از طریق اعلانها انجام میدهد، اما پایتون اعلان ندارد و حیف است که فقط برای این هدف مجبور شویم آنها را معرفی کنیم. استفاده صریح از self.var این مشکل را بهخوبی حل میکند. بهطور مشابه، برای استفاده از متغیرهای نمونه، این که باید self.var نوشته شود به این معناست که ارجاعها به نامهای ناکامل در داخل یک متد نیازی به جستجوی پوشههای آن نمونه ندارند. به بیان دیگر، متغیرهای محلی و متغیرهای نمونه در دو فضای نام متفاوت قرار دارند و شما باید به پایتون بگویید که از کدام فضای نام استفاده کند.
چرا نمیتوانم در یک عبارت از انتساب استفاده کنم؟¶
از پایتون 3.8 به بعد، میتوانید!
عبارات انتسابی که از عملگر والروس (walrus operator) := استفاده میکنند، متغیری را در یک عبارت انتساب میدهند:
while chunk := fp.read(200):
print(chunk)
برای اطلاعات بیشتر، PEP 572 را ببینید.
چرا پایتون برای برخی قابلیتها از متدها استفاده میکند (مثلاً list.index()) اما برای برخی دیگر از توابع (مثلاً len(list))؟¶
همانطور که گایدو گفت:
(a) For some operations, prefix notation just reads better than postfix -- prefix (and infix!) operations have a long tradition in mathematics which likes notations where the visuals help the mathematician thinking about a problem. Compare the easy with which we rewrite a formula like x*(a+b) into x*a + x*b to the clumsiness of doing the same thing using a raw OO notation.
(ب) هنگامی که کدی را میخوانم که len(x) دارد، میدانم که درخواست طول چیزی را دارد. این موضوع دو چیز را به من میگوید: نتیجه یک عدد صحیح است و آرگومان نوعی ظرف است. در مقابل، هنگامی که x.len() را میخوانم، باید از قبل بدانم که x نوعی ظرف است که یک رابط را پیادهسازی میکند یا از کلاسی ارث میبرد که یک len() استاندارد دارد. به سردرگمیای که گاهی با آن مواجه میشویم توجه کنید، هنگامی که کلاسی که یک نگاشت را پیادهسازی نمیکند دارای متد get() یا keys() است، یا چیزی که پرونده نیست متد write() دارد.
—https://mail.python.org/pipermail/python-3000/2006-November/004643.html
چرا join() یک متد رشته است، نه یک متد فهرست یا تاپل؟¶
رشتهها از پایتون 1.6 بسیار شبیهتر به سایر انواع استاندارد شدند، زمانی که متدهایی افزوده شدند که همان عملکردی را ارائه میدهند که همواره از طریق توابع ماژول string در دسترس بوده است. بیشتر این متدهای جدید بهطور گسترده پذیرفته شدهاند، اما آن متدی که به نظر میرسد باعث ناراحتی برخی برنامهنویسان میشود، این است:
", ".join(['1', '2', '4', '8', '16'])
که نتیجه زیر را میدهد:
1, 2, 4, 8, 16
دو آرگومان رایج در مخالفت با این کاربرد وجود دارد.
مورد اول اینگونه است: «استفاده از یک متدِ رشتهی لفظی (ثابت رشتهای) واقعاً زشت به نظر میرسد»، پاسخ این است که شاید چنین باشد، اما یک رشتهی لفظی صرفاً یک مقدار ثابت است. اگر متدها روی نامهای مقید به رشتهها مجاز باشند، هیچ دلیل منطقی برای در دسترس نبودن آنها روی رشتههای لفظی وجود ندارد.
اعتراض دوم معمولاً به این صورت بیان میشود: «من در واقع به یک دنباله میگویم که اعضای خود را با یک ثابت رشتهای به هم بپیوندد». متأسفانه، شما این کار را نمیکنید. به دلایلی، داشتن split() بهعنوان یک متد رشتهای بسیار کمتر دشوار به نظر میرسد، زیرا در آن حالت بهراحتی میتوان دید که
"1, 2, 4, 8, 16".split(", ")
دستوری به یک رشتهی لفظی برای بازگرداندن زیررشتههایی است که با جداکنندهی دادهشده جدا شدهاند (یا بهطور پیشفرض، دنبالههای دلخواه فضای خالی).
join() یک متد رشته است، زیرا هنگام استفاده از آن، شما به رشتهی جداکننده میگویید که دنبالهای از رشتهها را پیمایش کند و خود را بین عناصر مجاور درج کند. این متد را میتوان با هر آرگومانی که از قوانین اشیاء دنبالهای پیروی میکند، استفاده کرد؛ از جمله هر کلاس جدیدی که ممکن است خودتان تعریف کنید. متدهای مشابهی برای اشیاء bytes و bytearray وجود دارند.
استثناها چقدر سریع هستند؟¶
یک بلوک try/except اگر هیچ استثنایی پرتاب نشود، بسیار کارآمد است. در واقع، گرفتن یک استثنا پرهزینه است. در نسخههای پیش از 2.0 پایتون، استفاده از این اصطلاح رایج بود:
try:
value = mydict[key]
except KeyError:
mydict[key] = getvalue(key)
value = mydict[key]
این تنها زمانی منطقی بود که انتظار داشتید دیکشنری تقریباً همیشه کلید را داشته باشد. اگر اینطور نبود، آن را اینگونه کدنویسی میکردید:
if key in mydict:
value = mydict[key]
else:
value = mydict[key] = getvalue(key)
برای این مورد خاص، میتوانید از value = dict.setdefault(key, getvalue(key)) نیز استفاده کنید، اما تنها در صورتی که فراخوانی getvalue() بهاندازه کافی کمهزینه باشد، زیرا در همه موارد ارزیابی میشود.
چرا در پایتون دستور switch یا case وجود ندارد؟¶
بهطور کلی، دستورهای switch ساختاریافته یک بلوک کد را هنگامی اجرا میکنند که یک عبارت دارای یک مقدار خاص یا مجموعهای از مقادیر باشد. از پایتون 3.10، میتوانید بهراحتی مقادیر لفظی یا ثابتهای درون یک فضای نام را با دستور match ... case تطبیق دهید. برای اطلاعات بیشتر درباره دستورهای match، مشخصات و آموزش را ببینید. یک جایگزین قدیمیتر، دنبالهای از if... elif... elif... else است.
برای مواردی که نیاز دارید از میان تعداد بسیار زیادی از حالتهای ممکن انتخاب کنید، میتوانید یک دیکشنری برای نگاشت مقادیر حالت به توابعی که باید فراخوانی شوند ایجاد کنید. برای مثال:
functions = {'a': function_1,
'b': function_2,
'c': self.method_1}
func = functions[value]
func()
برای فراخوانی متدها روی اشیاء، میتوانید این کار را با استفاده از تابع توکار getattr() برای واکشی متدهایی با نامی خاص، باز هم سادهتر کنید:
class MyVisitor:
def visit_a(self):
...
def dispatch(self, value):
method_name = 'visit_' + str(value)
method = getattr(self, method_name)
method()
پیشنهاد میشود از یک پیشوند برای نام متدها استفاده کنید، مانند visit_ در این مثال. بدون چنین پیشوندی، اگر مقادیر از منبعی غیرقابلاعتماد دریافت شوند، یک مهاجم میتواند هر متدی را روی شیء شما فراخوانی کند.
تقلید switch با fallthrough، مانند switch-case-default در C، امکانپذیر است، بسیار دشوارتر است و کمتر مورد نیاز است.
آیا نمیتوانید به جای تکیه بر یک پیادهسازی نخ مختص سیستمعامل، نخها را در مفسر شبیهسازی کنید؟¶
پاسخ ۱: متأسفانه، مفسر برای هر فریم پشتهی پایتون، حداقل یک فریم پشتهی C را روی پشته قرار میدهد. همچنین، افزونهها میتوانند در لحظاتی تقریباً تصادفی فراخوانی بازگشتی به پایتون داشته باشند. بنابراین، یک پیادهسازی کامل نخها به پشتیبانی از نخ برای C نیاز دارد.
پاسخ ۲: خوشبختانه، Stackless Python وجود دارد که حلقه مفسر آن بهطور کامل بازطراحی شده است و از پشتهی C اجتناب میکند.
چرا عبارتهای لامبدا نمیتوانند شامل دستورها باشند؟¶
عبارتهای lambda پایتون نمیتوانند شامل دستور باشند، زیرا چارچوب نحوی پایتون نمیتواند دستورهای تودرتو درون عبارتها را مدیریت کند. با این حال، در پایتون، این مشکل جدیای نیست. برخلاف فرمهای lambda در زبانهای دیگر، که قابلیتهایی اضافه میکنند، lambdaهای پایتون تنها یک نمادگذاری خلاصه هستند، اگر حوصلهی تعریف یک تابع را نداشته باشید.
توابع در پایتون از پیش اشیای درجهیک هستند و میتوانند در یک محدوده محلی تعریف شوند. بنابراین تنها مزیت استفاده از لامبدا به جای یک تابع تعریفشده بهصورت محلی این است که نیازی نیست برای تابع نامی ابداع کنید -- اما آن فقط یک متغیر محلی است که شیء تابع (که دقیقاً همان نوع شیئی است که یک عبارت لامبدا برمیگرداند) به آن انتساب داده میشود!
آیا پایتون میتواند به کد ماشین، C یا زبان دیگری کامپایل شود؟¶
Cython نسخهای اصلاحشده از پایتون را با حاشیهنویسیهای اختیاری به افزونههای C کامپایل میکند. Nuitka یک کامپایلر نوظهور برای کامپایل پایتون به کد C++ است که هدف آن پشتیبانی از کل زبان پایتون است.
پایتون چگونه حافظه را مدیریت میکند؟¶
جزئیات مدیریت حافظه پایتون به پیادهسازی بستگی دارد. پیادهسازی استاندارد پایتون، CPython، از شمارش ارجاع برای تشخیص اشیای دسترسناپذیر و از سازوکار دیگری برای جمعآوری چرخههای ارجاع استفاده میکند و بهصورت دورهای یک الگوریتم تشخیص چرخه را اجرا میکند که چرخههای دسترسناپذیر را جستجو میکند و اشیای درگیر را حذف میکند. ماژول gc توابعی برای انجام زبالهروبی، بهدست آوردن آمار اشکالزدایی و تنظیم پارامترهای جمعکننده ارائه میدهد.
با این حال، سایر پیادهسازیها (مانند Jython یا PyPy) میتوانند به مکانیزم متفاوتی مانند یک زبالهرو (garbage collector) کامل متکی باشند. این تفاوت میتواند در صورتی که کد پایتون شما به رفتار پیادهسازی شمارش ارجاع وابسته باشد، باعث برخی مشکلات ظریف در انتقال شود.
در برخی پیادهسازیهای پایتون، کد زیر (که در CPython مشکلی ندارد) احتمالاً با اتمام توصیفگرهای پرونده مواجه میشود:
for file in very_long_list_of_files:
f = open(file)
c = f.read(1)
در واقع، با استفاده از طرحواره شمارش ارجاع و تخریبکنندهی CPython، هر انتساب جدید به f پرونده پیشین را میبندد. با این حال، با یک زبالهروبی سنتی، آن اشیای پرونده تنها در فاصلههای متغیر و احتمالاً طولانی جمعآوری (و بسته) میشوند.
اگر میخواهید کدی بنویسید که با هر پیادهسازی پایتون کار کند، باید پرونده را بهطور صریح ببندید یا از دستور with استفاده کنید؛ این صرفنظر از طرحواره مدیریت حافظه کار خواهد کرد:
for file in very_long_list_of_files:
with open(file) as f:
c = f.read(1)
چرا CPython از یک روش زبالهروبی سنتیتر استفاده نمیکند؟¶
برای نمونه، این یک ویژگی استاندارد C نیست و بنابراین قابلحمل نیست. (بله، ما در مورد کتابخانه Boehm GC میدانیم. این کتابخانه بخشهایی از کد اسمبلر را برای بیشتر سکوهای رایج دارد، نه برای همهی آنها، و اگرچه عمدتاً شفاف است، کاملاً شفاف نیست؛ برای اینکه پایتون بتواند با آن کار کند، به وصلهها نیاز است.)
زبالهروبی سنتی نیز هنگامی که پایتون در برنامههای دیگر تعبیه میشود، به یک مشکل تبدیل میشود. در حالی که در یک پایتون مستقل، جایگزین کردن malloc() و free() استاندارد با نسخههای ارائهشده توسط کتابخانه زبالهروبی مشکلی ندارد، برنامهای که پایتون را تعبیه میکند ممکن است بخواهد جایگزین خود را برای malloc() و free() داشته باشد و ممکن است جایگزین پایتون را نخواهد. در حال حاضر، CPython با هر چیزی که malloc() و free() را بهدرستی پیادهسازی کند، کار میکند.
چرا هنگام خروج CPython، تمام حافظه آزاد نمیشود؟¶
اشیایی که از فضای نام های سراسری ماژولهای پایتون به آنها ارجاع داده شدهاند، همیشه هنگام خروج پایتون آزاد نمیشوند. ممکن است این اتفاق در صورت وجود ارجاعهای چرخهای رخ دهد. همچنین بخشهایی از حافظه وجود دارند که توسط کتابخانه C تخصیص داده شدهاند و آزاد کردن آنها غیرممکن است (برای مثال، ابزاری مانند Purify دربارهی این موارد شکایت خواهد کرد). با این حال، پایتون در پاکسازی حافظه هنگام خروج بسیار جدی است و واقعاً تلاش میکند تکتک اشیاء را نابود کند.
اگر میخواهید پایتون را وادار به حذف موارد مشخصی در زمان آزادسازی حافظه کنید، از ماژول atexit برای اجرای تابعی استفاده کنید که آن حذفها را اجباری خواهد کرد.
چرا انواع دادهی جداگانهای برای تاپل و فهرست وجود دارند؟¶
فهرستها و تاپلها، با وجود شباهتهای زیاد، معمولاً بهطور بنیادین به شیوههای متفاوتی استفاده میشوند. میتوان تاپلها را مشابه records در پاسکال یا structs در C در نظر گرفت؛ آنها مجموعههای کوچکی از دادههای مرتبط هستند که ممکن است از انواع مختلفی باشند و بهعنوان یک گروه بر روی آنها عملیات انجام میشود. برای مثال، یک مختصات دکارتی بهطور مناسب بهصورت یک تاپل از ۲ یا ۳ عدد نمایش داده میشود.
از سوی دیگر، فهرستها بیشتر به آرایهها در زبانهای دیگر شبیهاند. آنها معمولاً تعداد متغیری از شیءها را نگه میدارند که همگی نوع یکسانی دارند و یکییکی روی آنها عمل میشود. برای مثال، os.listdir('.') فهرستی از رشتهها را برمیگرداند که پروندههای پوشه جاری را نشان میدهند. توابعی که روی این خروجی عمل میکنند، معمولاً اگر یک یا دو پرونده دیگر به پوشه اضافه کنید، از کار نمیافتند.
تاپلها تغییرناپذیر هستند، به این معنا که پس از ایجاد یک تاپل، نمیتوانید هیچیک از عناصر آن را با مقدار جدیدی جایگزین کنید. فهرستها تغییرپذیر هستند، به این معنا که همیشه میتوانید عناصر یک فهرست را تغییر دهید. فقط عناصر تغییرناپذیر میتوانند بهعنوان کلیدهای دیکشنری استفاده شوند، و بنابراین فقط تاپلها و نه فهرستها میتوانند بهعنوان کلید استفاده شوند.
فهرستها در CPython چگونه پیادهسازی شدهاند؟¶
فهرستهای CPython در واقع آرایههایی با طول متغیر هستند، نه فهرستهای پیوندی بهسبک Lisp. پیادهسازی از آرایهای پیوسته از ارجاعها به اشیاء دیگر استفاده میکند و یک اشارهگر به این آرایه و طول آرایه را در یک ساختار سر فهرست نگه میدارد.
این باعث میشود که اندیسدهی به یک فهرست a[i] عملیاتی باشد که هزینهی آن مستقل از اندازهی فهرست یا مقدار اندیس است.
هنگامی که آیتمها افزوده یا درج میشوند، اندازهی آرایهی ارجاعها تغییر میکند. تمهیداتی به کار گرفته شده است تا عملکرد افزودن مکرر آیتمها بهبود یابد؛ هنگامی که آرایه باید بزرگ شود، مقداری فضای اضافی تخصیص داده میشود تا چند بار بعدی نیازی به تغییر اندازهی واقعی نباشد.
See Time complexity of operations on built-in types for the costs of the various list operations.
دیکشنریها در CPython چگونه پیادهسازی شدهاند؟¶
دیکشنریهای CPython بهصورت جدولهای هش با قابلیت تغییر اندازه پیادهسازی شدهاند. در مقایسه با درختهای B، این روش در بیشتر شرایط عملکرد بهتری برای جستجو (رایجترین عملیات با اختلاف زیاد) ارائه میدهد و پیادهسازی سادهتری دارد.
دیکشنریها با محاسبهی کد هش (hash code) برای هر کلید ذخیرهشده در دیکشنری، از طریق تابع توکار hash() کار میکنند. کد هش بسته به کلید و یک بذر بهازای هر فرایند، تفاوت زیادی دارد؛ برای مثال، کد هش 'Python' میتواند -539294296 باشد، در حالی که کد هش 'python'، رشتهای که تنها یک بیت تفاوت دارد، میتواند 1142331976 باشد. سپس از کد هش برای محاسبهی محلی در یک آرایهی داخلی استفاده میشود که مقدار در آن ذخیره خواهد شد. با فرض اینکه شما کلیدهایی را ذخیره میکنید که همگی مقادیر هش متفاوتی دارند، این بدان معناست که دیکشنریها برای بازیابی یک کلید به زمان ثابت -- O(1)، در نمادگذاری Big-O -- نیاز دارند.
See Time complexity of operations on built-in types for the costs of the various dictionary operations.
چرا کلیدهای دیکشنری باید تغییرناپذیر باشند؟¶
پیادهسازی جدول هش دیکشنریها از یک مقدار هش محاسبهشده از مقدار کلید برای پیدا کردن کلید استفاده میکند. اگر کلید یک شیء تغییرپذیر باشد، ممکن است مقدار آن تغییر کند و در نتیجه هش آن نیز ممکن است تغییر کند. اما از آنجا که هرکسی که شیء کلید را تغییر میدهد نمیتواند تشخیص دهد که از آن بهعنوان کلید دیکشنری استفاده میشده است، نمیتواند آیتم را در دیکشنری جابهجا کند. سپس، وقتی سعی کنید همان شیء را در دیکشنری جستجو کنید، پیدا نخواهد شد، زیرا مقدار هش آن متفاوت است. اگر سعی کنید مقدار قدیمی را جستجو کنید، آن هم پیدا نخواهد شد، زیرا مقدار شیء یافتشده در آن سطل هش (hash bin) متفاوت خواهد بود.
اگر دیکشنریای میخواهید که با فهرست اندیسدهی شده باشد، کافی است ابتدا فهرست را به تاپل تبدیل کنید؛ تابع tuple(L) تاپلی با همان آیتمهای فهرست L میسازد. تاپلها تغییرناپذیرند و بنابراین میتوانند بهعنوان کلیدهای دیکشنری استفاده شوند.
برخی راهحلهای غیرقابلقبول که پیشنهاد شدهاند:
هش کردن فهرستها بر اساس نشانیشان (شناسهی شیء). این روش کار نمیکند، زیرا اگر فهرست جدیدی با همان مقدار ایجاد کنید، پیدا نخواهد شد؛ برای مثال:
mydict = {[1, 2]: '12'} print(mydict[[1, 2]])
استثنای
KeyErrorرا پرتاب میکند، زیرا شناسهی[1, 2]استفادهشده در خط دوم با شناسهی آن در خط اول متفاوت است. به عبارت دیگر، کلیدهای دیکشنری باید با استفاده از==مقایسه شوند، نه با استفاده ازis.هنگام استفاده از یک فهرست بهعنوان کلید، یک کپی بسازید. این روش کار نمیکند، زیرا فهرست بهعنوان یک شیء تغییرپذیر میتواند شامل ارجاعی به خودش باشد و در آن صورت کد کپیسازی وارد یک حلقه بیپایان میشود.
اجازه دهید فهرستها بهعنوان کلید استفاده شوند، اما به کاربر بگویید آنها را تغییر ندهد. هنگامی که فراموش کنید یا فهرستی را بهاشتباه تغییر دهید، این کار موجب ایجاد دستهای از باگها در برنامهها میشود که پیگیری آنها دشوار است. همچنین یک ناوردا مهم دیکشنریها را باطل میکند: هر مقدار موجود در
d.keys()را میتوان بهعنوان کلید دیکشنری استفاده کرد.فهرستها را بهمحض استفاده از آنها بهعنوان کلید دیکشنری، فقطخواندنی علامتگذاری کنید. مشکل این است که تنها شیء سطحبالا نیست که ممکن است مقدارش تغییر کند؛ شما میتوانید از یک تاپل حاوی یک فهرست بهعنوان کلید استفاده کنید. وارد کردن هر چیز بهعنوان کلید در یک دیکشنری، مستلزم علامتگذاری تمام اشیای قابلدسترس از آنجا بهعنوان فقطخواندنی است — و باز هم، اشیای خودارجاع میتوانند باعث ایجاد یک حلقه بیپایان شوند.
ترفندی برای دور زدن این موضوع وجود دارد که در صورت نیاز میتوانید از آن استفاده کنید، اما با مسئولیت خودتان: میتوانید یک ساختار تغییرپذیر را درون یک نمونه کلاس بپیچید که هر دو متد __eq__() و __hash__() را دارد. سپس باید اطمینان حاصل کنید که مقدار هش برای همه اشیاء پوششی از این قبیل که در یک دیکشنری (یا ساختار مبتنی بر هش دیگر) قرار دارند، تا زمانی که شیء در دیکشنری (یا ساختار دیگر) است، ثابت بماند.
class ListWrapper:
def __init__(self, the_list):
self.the_list = the_list
def __eq__(self, other):
return self.the_list == other.the_list
def __hash__(self):
l = self.the_list
result = 98767 - len(l)*555
for i, el in enumerate(l):
try:
result = result + (hash(el) % 9999999) * 1001 + i
except Exception:
result = (result % 7777777) + i * 333
return result
توجه داشته باشید که محاسبهی هش به دلیل احتمال هشناپذیر بودن برخی اعضای فهرست و نیز احتمال سرریز محاسباتی، پیچیده است.
علاوه بر این، همیشه باید اگر o1 == o2 (یعنی o1.__eq__(o2) is True)، آنگاه hash(o1) == hash(o2) (یعنی o1.__hash__() == o2.__hash__()) برقرار باشد، صرفنظر از اینکه شیء در یک دیکشنری باشد یا نه. اگر این محدودیتها را رعایت نکنید، دیکشنریها و سایر ساختارهای مبتنی بر هش بهدرستی کار نخواهند کرد.
در مورد ListWrapper، هر گاه شیء پوششی در یک دیکشنری باشد، فهرست پوشیدهشده نباید تغییر کند تا از ناهنجاریها جلوگیری شود. این کار را انجام ندهید مگر آنکه آماده باشید تا درباره الزامات و پیامدهای برآورده نکردن صحیح آنها بهدقت بیندیشید. خود را هشدار دادهشده بدانید.
چرا list.sort() فهرست مرتبشده را برنمیگرداند؟¶
در موقعیتهایی که کارایی اهمیت دارد، ساختن یک کپی از فهرست صرفاً برای مرتبسازی آن، اتلاف منابع خواهد بود. بنابراین، list.sort() فهرست را درجا مرتب میکند. برای یادآوری همین موضوع، فهرست مرتبشده را برنمیگرداند. به این ترتیب، وقتی به یک کپی مرتبشده نیاز دارید اما همزمان باید نسخهی مرتبنشده را نیز نگه دارید، دچار این اشتباه نخواهید شد که بهطور تصادفی یک فهرست را بازنویسی کنید.
اگر میخواهید یک فهرست جدید برگردانید، بهجای آن از تابع توکار sorted() استفاده کنید. این تابع از یک پیمایشپذیر ارائهشده یک فهرست جدید ایجاد میکند، آن را مرتب میکند و برمیگرداند. برای مثال، در اینجا چگونگی پیمایش روی کلیدهای یک دیکشنری با ترتیب مرتبشده آمده است:
for key in sorted(mydict):
... # do whatever with mydict[key]...
چگونه مشخصات یک رابط را در پایتون تعیین و اعمال میکنید؟¶
مشخصات رابط برای یک ماژول، همانطور که توسط زبانهایی مانند C++ و Java ارائه میشود، پیشنمونههای متدها و توابع آن ماژول را توصیف میکند. بسیاری بر این باورند که اعمال مشخصات رابط در زمان کامپایل، به ساخت برنامههای بزرگ کمک میکند.
پایتون 2.6 ماژول abc را اضافه میکند که به شما امکان میدهد کلاسهای پایه انتزاعی (ABCs) را تعریف کنید. سپس میتوانید از isinstance() و issubclass() برای بررسی اینکه آیا یک نمونه یا کلاس یک ABC خاص را پیادهسازی میکند، استفاده کنید. ماژول collections.abc مجموعهای از ABCهای مفید را تعریف میکند، مانند Iterable، Container و MutableMapping.
در پایتون، بسیاری از مزایای مشخصات رابط را میتوان با نظم آزمون مناسب برای کامپوننتها به دست آورد.
یک بدنه آزمون خوب برای یک ماژول میتواند هم یک آزمون رگرسیون فراهم کند و هم بهعنوان مشخصات رابط ماژول و مجموعهای از مثالها عمل کند. بسیاری از ماژولهای پایتون میتوانند بهصورت یک اسکریپت اجرا شوند تا یک «خودآزمایی» ساده فراهم کنند. حتی ماژولهایی که از رابطهای خارجی پیچیده استفاده میکنند، اغلب میتوانند با استفاده از stub ساده برای رابط خارجی، بهصورت مجزا آزمون شوند. میتوان از ماژولهای doctest و unittest یا چارچوبهای آزمون شخص ثالث برای ساخت بدنههای آزمون جامعی استفاده کرد که هر خط از کد یک ماژول را اجرا میکنند.
یک رویه مناسب آزمون، همانطور که داشتن مشخصات رابط کمک میکند، میتواند به ساخت برنامههای بزرگ و پیچیده در پایتون کمک کند. در واقع، میتواند بهتر باشد، زیرا مشخصات رابط نمیتواند برخی از ویژگیهای یک برنامه را آزمون کند. برای مثال، انتظار میرود متد list.append() عناصر جدید را به انتهای یک فهرست داخلی اضافه کند؛ مشخصات رابط نمیتواند آزمون کند که پیادهسازی list.append() شما واقعاً این کار را بهدرستی انجام خواهد داد، اما بررسی این ویژگی در یک بدنه آزمون بسیار ساده است.
نوشتن بدنههای آزمون بسیار مفید است، و شاید بخواهید کد خود را بهگونهای طراحی کنید که بهراحتی آزمون شود. یکی از تکنیکهایی که بهطور فزایندهای محبوب شده است، توسعه آزمونمحور است که ایجاب میکند ابتدا بخشهایی از بدنه آزمون را بنویسید، پیش از آنکه هیچیک از کد واقعی را بنویسید. البته پایتون به شما اجازه میدهد که سهلانگار باشید و اصلاً هیچ مورد آزمونی ننویسید.
چرا goto وجود ندارد؟¶
در دههی ۱۹۷۰، مشخص شد که goto بدون محدودیت میتوانست به کد آشفتهی «اسپاگتی» منجر شود که درک و اصلاح آن دشوار بود. در یک زبان سطح بالا نیز، تا زمانی که راههایی برای انشعاب (در پایتون، با دستورهای if و عبارتهای or، and و if/else) و حلقه (با دستورهای while و for، که ممکن است شامل continue و break باشند) وجود داشته باشد، نیازی به آن نیست.
همچنین میتوان از استثناها برای فراهم کردن یک «goto ساختاریافته» استفاده کرد که حتی میان فراخوانیهای تابع نیز کار میکند. بسیاری بر این باورند که استثناها میتوانند بهراحتی تمام استفادههای معقول از ساختارهای go یا goto در C، Fortran و زبانهای دیگر را شبیهسازی کنند. برای مثال:
class label(Exception): pass # declare a label
try:
...
if condition: raise label() # goto label
...
except label: # where to goto
pass
...
این به شما اجازه نمیدهد به وسط یک حلقه بپرید، اما به هر حال چنین کاری معمولاً سوءاستفاده از goto تلقی میشود. با احتیاط استفاده کنید.
چرا رشتههای خام (r-strings) نمیتوانند با بکاسلش پایان یابند؟¶
دقیقتر، آنها نمیتوانند با تعداد فردی از بکاسلشها پایان یابند: بکاسلش جفتنشده در انتها، نویسهی علامت نقلقول پایانی را خنثی میکند و باعث میشود رشتهای پایاننیافته باقی بماند.
رشتههای خام برای آسانتر کردن ایجاد ورودی برای پردازندههایی (عمدتاً موتورهای عبارت باقاعده) طراحی شدهاند که میخواهند پردازش خنثیسازی بکاسلش خودشان را انجام دهند. این پردازندهها در هر حال یک بکاسلش انتهایی بدون تطابق را خطا میدانند، بنابراین رشتههای خام آن را مجاز نمیشمارند. در عوض، به شما اجازه میدهند که نویسهی نقلقول رشته را با خنثی کردن آن بهوسیلهی یک بکاسلش منتقل کنید. این قوانین زمانی که رشتههای r برای هدف مورد نظرشان استفاده شوند، بهخوبی کار میکنند.
اگر در حال تلاش برای ساخت مسیرهای ویندوزی هستید، توجه داشته باشید که همه فراخوانیهای سیستمی ویندوز نیز اسلشهای رو به جلو را میپذیرند:
f = open("/mydir/file.txt") # بهدرستی کار میکند!
اگر میخواهید یک مسیر برای دستور DOS بسازید، برای مثال یکی از موارد زیر را امتحان کنید
dir = r"\this\is\my\dos\dir" "\\"
dir = r"\this\is\my\dos\dir\ "[:-1]
dir = "\\this\\is\\my\\dos\\dir\\"
چرا پایتون یک دستور "with" برای انتساب ویژگیها ندارد؟¶
پایتون یک دستور with دارد که اجرای یک بلوک را دربر میگیرد و کدی را در هنگام ورود به بلوک و خروج از آن فراخوانی میکند. برخی زبانها ساختاری دارند که به این شکل است:
with obj:
a = 1 # equivalent to obj.a = 1
total = total + 1 # obj.total = obj.total + 1
در پایتون، چنین ساختاری مبهم خواهد بود.
زبانهای دیگر، مانند Object Pascal، Delphi و C++، از انواع ایستا استفاده میکنند، بنابراین میتوان بهشکلی بدون ابهام دانست که به کدام عضو انتساب صورت میگیرد. این نکته اصلی نوعدهی ایستا است — کامپایلر همیشه محدوده هر متغیر را در زمان کامپایل میداند.
پایتون از انواع پویا استفاده میکند. نمیتوان از قبل دانست که در رانتایم به کدام ویژگی ارجاع داده خواهد شد. ویژگیهای عضو ممکن است در لحظه به شیءها اضافه یا از آنها حذف شوند. این موضوع باعث میشود نتوان از یک خواندن ساده فهمید که به کدام ویژگی ارجاع داده میشود: یک ویژگی محلی، یک ویژگی سراسری، یا یک ویژگی عضو؟
برای مثال، قطعهکد ناقص زیر را در نظر بگیرید:
def foo(a):
with a:
print(x)
این قطعه کد فرض میکند که a باید یک ویژگی عضو به نام x داشته باشد. با این حال، هیچ چیزی در پایتون وجود ندارد که این موضوع را به مفسر بگوید. اگر a، مثلاً، یک عدد صحیح باشد، چه اتفاقی باید بیفتد؟ اگر یک متغیر سراسری به نام x وجود داشته باشد، آیا در داخل بلوک with از آن استفاده خواهد شد؟ همانطور که میبینید، ماهیت پویای پایتون چنین انتخابهایی را بسیار دشوارتر میکند.
با این حال، مزیت اصلی with و ویژگیهای زبانی مشابه (کاهش حجم کد) را میتوان بهراحتی در پایتون با انتساب به دست آورد. بهجای:
function(args).mydict[index][index].a = 21
function(args).mydict[index][index].b = 42
function(args).mydict[index][index].c = 63
این را بنویسید:
ref = function(args).mydict[index][index]
ref.a = 21
ref.b = 42
ref.c = 63
این موضوع همچنین اثر جانبی افزایش سرعت اجرا را دارد، زیرا پیوندهای نام (name bindings) در پایتون در رانتایم حل میشوند و نسخه دوم تنها لازم است این حل را یک بار انجام دهد.
پیشنهادهای مشابهی که سینتکسی را برای کاهش بیشتر حجم کد معرفی میکردند، مانند استفاده از «نقطه آغازین»، به نفع صراحت رد شدهاند (به https://mail.python.org/pipermail/python-ideas/2016-May/040070.html مراجعه کنید).
چرا تولیدگرها از دستور with پشتیبانی نمیکنند؟¶
به دلایل فنی، تولیدگری که بهطور مستقیم بهعنوان مدیر زمینه استفاده شود، بهدرستی کار نخواهد کرد. هنگامی که، همانطور که رایجترین حالت است، یک تولیدگر بهعنوان پیمایشگری استفاده میشود که تا پایان اجرا میشود، نیازی به بستن نیست. هرگاه لازم باشد، آن را در دستور with بهصورت contextlib.closing(generator) قرار دهید.
چرا دونقطهها برای دستورات if/while/def/class الزامی هستند؟¶
دونقطه عمدتاً برای افزایش خوانایی لازم است (یکی از نتایج زبان آزمایشی ABC). این را در نظر بگیرید:
if a == b
print(a)
در مقایسه با
if a == b:
print(a)
توجه کنید که چگونه مورد دوم کمی آسانتر خوانده میشود. همچنین توجه کنید که چگونه یک دونقطه مثال را در این پاسخِ پرسشهای متداول جدا میکند؛ این یک کاربرد استاندارد در زبان انگلیسی است.
دلیل فرعی دیگر این است که دونقطه کار را برای ویرایشگرهای دارای برجستهسازی سینتکس آسانتر میکند؛ آنها میتوانند بهدنبال دونقطهها بگردند تا تشخیص دهند چه زمانی تورفتگی باید افزایش یابد، بهجای اینکه مجبور باشند تجزیه پیچیدهتری از متن برنامه انجام دهند.
چرا پایتون اجازه میدهد در انتهای فهرستها و تاپلها کاما وجود داشته باشد؟¶
پایتون به شما امکان میدهد یک کامای پایانی را در انتهای فهرستها، تاپلها و دیکشنریها اضافه کنید:
[1, 2, 3,]
('a', 'b', 'c',)
d = {
"A": [1, 5],
"B": [6, 7], # last trailing comma is optional but good style
}
دلایل متعددی برای مجاز دانستن این وجود دارد.
هنگامی که یک مقدار لفظی برای فهرست، تاپل یا دیکشنری دارید که در چند خط پخش شده است، افزودن عناصر بیشتر آسانتر است، زیرا لازم نیست به خاطر بسپارید که باید یک ویرگول به خط قبلی اضافه کنید. همچنین میتوان سطرها را بدون ایجاد خطای سینتکسی جابهجا کرد.
حذف تصادفی کاما میتواند منجر به خطاهایی شود که اشکالزدایی آنها دشوار است. برای مثال:
x = [
"fee",
"fie"
"foo",
"fum"
]
این فهرست به نظر میرسد چهار عنصر دارد، اما در واقع شامل سه عنصر است: "fee"، "fiefoo" و "fum". افزودن همیشگی ویرگول از این منبع خطا جلوگیری میکند.
مجاز بودن کامای پایانی همچنین ممکن است تولید کد بهصورت برنامهای را آسانتر کند.