بدیهیترین چیزی که از یک برنامه کامپیوتر میتوان انتظار داشت انجام صحیح عملیات جبری ساده مثل جمع و تفریق است. البته استثناهایی که برای این قضیه وجود دارد را احتمالا خودتان دیدهاید. مثلا در پایتون داریم:
>>>0.1 + 0.2 == 0.3 False >>>print(0.1 + 0.2) 0.30000000000000004
دلیل بروز چنین اشتباه محاسباتی واضحی ذخیره دو-دویی اعداد است. بهطور خلاصه شما هیچ وقت نمیتوانید عدد ۰.۱ را به طور دو-دویی (باینری) با ارقام متناهی نمایش دهید. وقتی نمیتوانید با ارقام متناهی نمایش دهید پس باید با ارقام نامتناهی نمایش داد، و طبیعتا این امکانپذیر نیست. پس برای ذخیره و استفاده از این عدد باید از یک جایی به بعد بیخیال ارقامش شوید (مثلا بعد از ۲۰ رقم) و این یعنی خطا. یعنی عددی که ذخیره شده دقیقا ۰.۱ نیست. توضیحات مفصلتر و لینکهای بیشتر برای توضیحات مفصلتر در اینجا موجود است.
اما این مسئله ما نیست. در حال حاضر صحبت درباره خطاهای محاسباتی بزرگتر است.
متاسفانه یا خوشبختانه بله. اما بهطور طبیعی این اتفاق نمیافتد. ولی بعد از یک انگولک ساده، واقعا میتوانید عبارت زیر را وارد shell پایتون کنید و خروجی زیر را تحویل بگیرید:
>>>2 + 2 2
اما قبل از آن لازم است توضیحی درخصوص پایتون بدهم. البته که این توضیح خیلی ساده شدهایست در حد سواد خودم.
در پیادهسازی CPython، استراکچری تعریف شده به نام PyObject. تمامی چیزهایی که در پایتون میبینید نمونههایی از این Pyobject هستند. منظور از تمام چیزها، واقعا تمام چیزهاست! از اعداد و رشتهها گرفته تا توابع و کلاسها و حتی ماژولها. این آبجکتها هرکدام باید یک عدد داشته باشند به نام id. این id فیالواقع عددی است، ثابت و یکتا. هیچ دو آبجکتی id یکسان نخواهند داشت. بهطور خاص در پیادهسازی CPython، برای id هر آبجکت، شماره حافظه آن آبجکت درنظر گرفته شده.
یعنی مثلا وقتی آبجکتی داریم به نام hello، این آبجکت در خانه ۴۵ام (این عدد فرضیاست، معمولا اعداد خیلی بزرگتر هستند) از حافظه ذخیره شده. پس میدانیم که id آبجکت hello میشود ۴۵.
این مسئله از این جهت مهم است که پس میفهمیم ما آدرس حافظه هر آبجکت را داریم. و این خوشحال کننده است. چون میتوانیم کارهای نسبتا جالبی با استفاده از این آدرس بکنیم.
برای انجام این بازیها، ابتدا باید یکی از کتابخانههای نسبتا ناشناخته پایتون را فراخوانی کرد.
این کتابخانه یکی از API های زبان پایتون با C است. اگر نمیدانید باید بدانید که پیادهسازی CPython با C نوشته شده و به همین دلیل زبان C را میتوان به نوعی زبان مولد پایتون دانست. البته که پیادهسازیهای دیگری هم از پایتون داریم که با زبانهای دیگر هستند. مثل جایتون که با جاوا نوشته شده است.
اگر این کتابخانه برایتان جالب است، نگاه کردن به سورس آن خالی از لطف نیست. اما فعلا برای ما یکی از توابع این کتابخانه مهم است. آنهم تابع memmove است. اگر خواننده دستی در C داشته باشد، علیالقاعده باید از این اسم نسبتا طرز کار این تابع را حدس بزند. این اسم مخفف memory move است. به طور فنی یک بخشی از حافظه را جای بخش دیگری میگذارد. جابجا نمیکند! صرفا یکی را جای دیگری میگذارد.
>>> import ctypes as ct >>> import sys >>> a = "hello" >>> b = "salam" >>> size = sys.getsizeof(a) >>> ct.memmove(id(a),id(b), size) 140678768806768 >>> a 'salam'
همانطور که نظاره کردید، با بردن حافظه آبجکت a بهجای b توانستم مقدار داخل b را به a بدهم. همانطور که
گفتم این اتفاق دو طرفه نیست! یعنی الان مقدار داخل b همان salam است و تغییری نکرده.
همچنین تابع getsizeof هم اندازه آن آبجکت را به ما برمیگرداند. همانطور که میدانید یک رشته ساده در پایتون، خیلیهم ساده نیست. در واقع نوعی آبجکت است. و آبجکت اطلاعات زیادی را نگه میدارد. برای همین اندازه یک رشته ۵ حرفی مثل hello بیشتر از ۵ بایت است. به طور فنی یک رشته خالی، ۴۹ بایت جا میگیرد. و به ازاء هر حرف جدید که به رشته اضافه شود یک بایت بیشتر میشود. این اندازه کل آبجکت در حافظه است. با همه مخلفاتش. این مسئله درمورد اعداد نیز صادق است. یک عدد کوچک مثل ۳، از آنجایی که در واقع یک آبجکت است، ۲۸ بایت فضا اشغال میکند. این فضای اشغال شده را با همان تابع getsizeof میتوان فهمید.
ربطش این است که هر عدد نوعی آبجکت پایتونی است. لذا میتوانیم به همین طریق جای اعداد را هم عوض کنیم! شاید بنظر عجیب برسد. اما واقعا میشود.
>>> ct.memmove(id(2), id(1), 28) 140678993844416 >>> 2+2 2
امیدوارم از این تردستی لذت وافر برده باشید.