عملگر is در پایتون و خطرهای استفاده از آن – راهنمای کاربردی


در این مقاله از مجله فرادرس به بررسی یک باگ عجیب integer در پایتون میپردازیم. این باگ به استفاده از عملگر is در پایتون مربوط است. در ادامه به بررسی شیوه کشف این باگ، دلیل باگ بودن این پدیده و این که چرا تنها در اعداد صحیح بزرگ رخ میدهد میپردازیم. بدین ترتیب درک خواهیم کرد که چرا باگها بهترین معلم ما در مسیر برنامهنویسی هستند و چگونه میتوانیم از آنها برای جلوگیری از مشکلات دیگر استفاده کنیم.
باگ is در پایتون
تصویر فوق متعلق به یک وبسایت کرایه دوچرخه است و همه چیز به خوبی کار میکرد تا این که یک روز بدون این که تغییری در کد داده شود، پیامی از یکی از تستکنندگان دریافت میشود که دو سفارش برای تاریخ 7 صبح تاریخ 25/9 رزرو کرده است؛ اما یک آرایه خالی برای وی بازگشت یافته است.
این سفارش در بخش مدیریتی سیستم مشاهده نمیشود و بنابراین توسعهدهندگان سیستم سردرگم بودند که آیا بدون ایجاد هیچ تغییری در کد هم ممکن است یک باگ ظاهر شود؟ کد زیر حلقهای را نشان میدهد که همه قرارها را بر اساس time_slot گروهبندی میکند و این قرارها را به صورتی لیستی بازگشت میدهد که از سوی API قابل استفاده است:
دو نوع مقایسه در کد فوق وجود دارد که به توضیح آنها خواهیم پرداخت. کلیدواژه is بر اساس ارجاع مقایسه میشود. منظور از ارجاع چیزی مانند یک آدرس یا اشارهگر به یک شیء است. مقایسه is دلیل اصلی بروز این مشکل بوده است. عملگر == مقایسه را بر اساس مقدار انجام میدهد. در ادامه از اسکناسهای با شکل سگ برای توضیح تفاوتهای مقایسه با is و == استفاده کردهایم.
مثال
فرض کنید دو نفر به نامهای Paul و Brad یک اسکناس یک دلاری puppy دارند. زمانی که آنها پول خود را مقایسه میکنند در واقع یک ارجاع به یک مقدار وجود دارد و چه آنها را با is و چه با == مقایسه کنیم، نتیجه کار یکسان خواهد بود.
به مثال زیر نیز توجه کنید که در آن دلارهای متفاوتی داریم؛ اما مقدار آنها یکسان است:
در این حالت، زمانی که پول خود را مقایسه میکنیم، ارجاعهای مختلفی داریم؛ اما مقادیر یکسان هستند. بنابراین اگر با is مقایسه کنیم، پول ما متفاوت خواهد بود؛ اما اگر با == مقایسه کنیم پول همان خواهد بود.
برای درک علت این که چرا این باگ در همان ابتدا ظاهر نشده و پس از مدت زمان نسبتاً زیادی، خود را نشان داده است باید یک گام به عقب بازگردیم و به بررسی طرز کار مفسر پایتون و شیوه مدیریت حافظه بپردازیم.
مفسر پایتون به همراه «پشته فراخوانی» (Call Stack) و «هیپ خصوصی» (Private Heap) با «اشیای پایتون» (PyObjects). فریمهای روی پشته فراخوانی به اشیای پایتون روی هیپ اشاره میکنند.
مدیریت حافظه در پایتون
پایتون یک ماشین مجازی مبتنی بر پشته است که همه اشیا را روی هیپ خصوصی نگهداری میکند و هیپ روش دیگری برای نامیدن بخشهای حافظه یا آرایههای بزرگی از دادهها است. اینک کمی جلوتر میرویم و جزییات اشیای پایتون را که روی هیپ قرار دارند مورد بررسی قرار میدهیم.
هر مقدار که در پایتون به یک متغیر انتساب یافته باشد روی یک شیء در هیپ قرار دارد. تخصیص این اشیا به زمان نیاز دارد، زیرا بخش مدیریت حافظه باید این کار را اجرا کند. زمانی که یک عدد صحیح به یک متغیر در پایتون انتساب مییابد، یک PyObject متناظر باید روی هیپ ایجاد شود.
یک شیء PyLong نوعی PyObject در پایتون است که مقدار صحیح عددی دارد. PyLongObject-های بین 5- تا 256 از قبل، روی هیپ در CPython تخصیص یافتهاند و میتوان در C با کمک آرایه small_ints به آنها دسترسی یافت. small_ints یک بهینهسازی است، زیرا ماژول مدیریت حافظه برای کار با اعداد صحیح کوچک به کار کمتری نیاز دارد.
مقایسه با ارجاع
در ادامه مثالی از مقایسه با ارجاع ارائه شده است. این همان نوع مقایسهای است که موجب بروز باگ فوق شده است. این نوع مقایسه با استفاده از کلیدواژه is صورت میگیرد. در ابتدا متغیر v خود را با مقدار 5- مقداردهی میکنیم. سپس متغیر دیگری به نام w را با مقدار 5- مقدار میکنیم و سپس v و w را با استفاده از is مقایسه میکنیم که نتیجه کار True است.
در ادامه کد منبع CPython را میبینید که نشان میدهد چرا وقتی اعداد صحیح کوچک را مقایسه میکنیم، نتیجه کار True است. در کد زیر وهلهای از یک عدد صحیح کوچک دریافت میشود:
اگر عدد ما همچون سگ روی اسکناس اول (puppy) کوچک باشد، از آرایه small_ints بازگشت مییابد. در این حالت دیگر لازم نیست که یک PyObject جدید روی هیپ تخصیص بدهیم، زیرا قبلاً در آن جا است. همچنین باید دانست که وقتی متغیر w مقداردهی میشود، همچنان میتوان از آرایه small_ints استفاده کرد و لازم نیست PyObject دیگری را مقداردهی کرد. در ادامه به مقایسه v و w میپردازیم.
کد منبع CPython زیر به مقایسه دو اشارهگر به PyObject-ها میپردازد. به بیان دیگر این کد به مقایسه دو آدرس یا نشانی میپردازد. بدین ترتیب یک اشارهگر به متغیر، در واقع آدرس متغیر دیگر است. ما روش مقایسه را میشناسیم. PyObject_RichCompareBool به مقایسه اشارهگرها یا آدرسها میپردازد، زیرا پارامترهای v و w در جلوی خود کاراکتر ستاره (*) دارند.
مقایسه با مقدار
توجه کنید که CPython برای مقایسه اشیا از ارجاع استفاده میکند و با == به مقایسه آدرسها میپردازد. به بیان دیگر کد منبع مقایسه با ارجاع از روش مقایسه با مقدار استفاده میکند. اگر v و w در آدرس یکسانی باشند، این مقایسه مقدار true بازگشت میدهد. بنابراین اگر v و w اعداد صحیح کوچک باشند؛ از آنجا که در آرایه small_ints قرار دارند در واقع آدرس یکسانی هم دارند و PyObject_RichCompareBool باعث خروجی True میشود. در سطحی بالاتر، مقایسه اشیا با استفاده از is میتواند به مقایسه مکان اشیا در هیپ تشبیه شود.
اینک نگاهی به مقایسه مقادیری میپردازیم که در آرایه small_ints قرار ندارند. از آنجا که 257 در محدوده آرایه small_ints نیست، زمانی که متغیرهای خود را مقداردهی میکنیم در واقع دو PyObject جدید میسازیم که مقدار یکسانی دارند و زمانی که ارجاع این شیءها را مقایسه کنیم، نتیجه کار False خواهد بود.
اگر به باگ خود بازگردیم، ما در واقع قرارهای خود را برای بازه زمانی جدید فیلتر میکنیم، زیرا بند if روی اعداد بزرگ نتیجه False میدهد. در حقیقت ما با بررسی عبارت زیر:
if appointment.time_slot_id is time_slot.id
پیش از افزودن یک قرار ، همه چیز را فیلتر میکنیم.
در تصویر فوق یک تصویر از کد دارای باگ را مشاهده میکنید. PyLongObject در سمت چپ نشان دهنده time_slot.id است و PyLongObject در سمت راست به نمایش appointment.time_slot_id میپردازد. همان طور که میبینید هر دوی آنها اشیای متفاوتی هستند. ما به جای مقایسه مقادیر به مقایسه PyLongObjects پرداختهایم و نتیجه False بوده است، زیرا این دو شیءهای متفاوتی هستند. این کد به مقایسه دو PyLongObject متفاوت میپردازد که بر حسب تصادف مقدار یکسانی دارند.
راهحل باگ
اگر مقایسه خود را به سادگی با تغییر دادن کاراکتر (_) پایانی به (.) طوری عوض کنیم که بررسی زیر را اجرا کند:
if time_slot.id is appointment.time_slot.id
در این صورت به بررسی PyLongObject یکسانی پرداختهایم.
از آنجا که کاراکتر زیرخط به صورت نقطه در آمده است، اینک appointment.time_slot و time_slot هر دو به شیء time_slot یکسانی روی هیپ اشاره میکنند. این بدان معنی است که همه فیلدهای دیگر شامل id یکسان هستند. اگر این وضعیت را بخواهیم با اسکناسهای «طرح سگ» خود بیان کنیم، appointment.time_slot و time_slot اینک به دلارهای سگی یکسانی اشاره میکنند.
این تغییر کاراکتری کوچک در مواردی موجب اصلاح باگ میشود؛ اما در مواردی هم که وهلههای متعددی از شیء time_slot وجود داشته باشد، ممکن است پاسخگو نباشد.
یک اصلاحیه بهتر برای این باگ، استفاده از مقایسه == است. اگر ما در همان ابتدا، از مقایسه == به جای is استفاده میکردیم، کل این باگ قابل اجتناب بود. مقایسه با مقدار به بررسی محتوای دو شیء میپردازد. بر حسب مثال اسکناسهای دلاری، مقایسه کردن با استفاده از == در واقع به معنی نگاه کردن به ارزش عددی اسکناس به جای خود اسکناس است.
سخن پایانی
مهمترین نکته این بررسی آن است که موارد استفاده از is برای مقایسه، باید صرفاً به مقایسه اشیا محدود شود. بدین ترتیب متوجه شدیم که چگونه اصلاح یک باگ منجر می شود به درک بهتری از یک زبان برنامهنویسی برسیم. در پایتون همه چیز شیء محسوب میشود و از این رو هنگام استفاده از is برای مقایسه باید با احتیاط بیشتری عمل کرد.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزش های برنامه نویسی پایتون
- آموزش برنامه نویسی پایتون – مقدماتی
- مجموعه آموزشهای برنامه نویسی
- آموزش نحوه نصب و راه اندازی پایتون
- گنجینه آموزش های برنامه نویسی پایتون (Python)
- زبان برنامه نویسی پایتون (Python) — از صفر تا صد
- پنج دلیل برای کاربردی بودن زبان پایتون
==
is returns True if and only if two objects have the same memory address.