منابع ترجمه · فصل ۲

سفر به قلب شی‌گرایی پایتون: از کارت اعتباری غارتگر تا تصاعد فیبوناچی

۸۸ دقیقه مطالعه · به‌روزرسانی ۲ آبان ۱۴۰۵

منبع اصلی: Data Structures and Algorithms in Python — Michael T. Goodrich, Roberto Tamassia, Michael H. Goldwasser

دربارهٔ این ترجمه: این صفحه ترجمهٔ فارسی «فصل ۲: برنامه‌نویسی شی‌گرا» (Object-Oriented Programming) از کتاب Data Structures and Algorithms in Python نوشتهٔ Goodrich، Tamassia و Goldwasser است. شماره‌گذاری بخش‌ها، قطعه‌کدها و تمرین‌ها همان شماره‌گذاری کتاب است. فصل ۱ کتاب («Python Primer») مرور مقدماتی پایتون است و این فصل چند بار به آن ارجاع می‌دهد. بلوک‌هایی که به بلوک‌های قبلی وابسته‌اند فقط برای مطالعه‌اند؛ بقیه را می‌توانید همین‌جا اجرا کنید.

۲.۱ اهداف، اصول و الگوها

همان‌طور که از نام این رویکرد پیداست، «بازیگران» اصلی در پارادایم شی‌گرا اشیاء (objects) نام دارند. هر شیء نمونه‌ای (instance) از یک کلاس (class) است. هر کلاس تصویری موجز و یکدست از اشیائی که نمونه‌های آن هستند به دنیای بیرون ارائه می‌دهد، بی‌آنکه وارد جزئیات غیرضروری شود یا اجازه دهد دیگران به سازوکار درونی اشیاء دسترسی پیدا کنند. تعریف کلاس معمولاً متغیرهای نمونه (instance variables) را که اعضای داده (data members) نیز نامیده می‌شوند و شیء آن‌ها را در خود دارد، و همچنین متدها (methods) را که توابع عضو (member functions) نیز نامیده می‌شوند و شیء می‌تواند آن‌ها را اجرا کند، مشخص می‌کند. این نگاه به محاسبات برای تحقق چند هدف طراحی شده و چند اصل طراحی را در بر می‌گیرد که در این فصل به آن‌ها می‌پردازیم.

۲.۱.۱ اهداف طراحی شی‌گرا

پیاده‌سازی‌های نرم‌افزاری باید به استحکام (robustness)، انطباق‌پذیری (adaptability) و قابلیت استفاده مجدد (reusability) دست یابند (شکل ۲.۱).

شکل ۲.۱: اهداف طراحی شی‌گرا: استحکام، انطباق‌پذیری و قابلیت استفاده مجدد.

استحکام

هر برنامه‌نویس خوبی می‌خواهد نرم‌افزاری بنویسد که درست باشد؛ یعنی برنامه برای همه ورودی‌های پیش‌بینی‌شده در کاربردش خروجی صحیح تولید کند. افزون بر این، می‌خواهیم نرم‌افزار مستحکم باشد؛ یعنی بتواند ورودی‌های غیرمنتظره‌ای را که صراحتاً برای کاربردش تعریف نشده‌اند نیز مدیریت کند. برای مثال، اگر برنامه‌ای انتظار یک عدد صحیح مثبت داشته باشد (مثلاً قیمت یک کالا) و به جای آن عددی منفی دریافت کند، باید بتواند به‌شکلی آبرومندانه از این خطا عبور کند. مهم‌تر از آن، در کاربردهای حیاتی (life-critical applications)، که خطای نرم‌افزاری ممکن است به آسیب یا مرگ انسان بینجامد، نرم‌افزاری که مستحکم نباشد می‌تواند مرگبار باشد. اهمیت این نکته در اواخر دهه ۱۹۸۰ با حوادث مربوط به Therac-25 آشکار شد؛ دستگاه پرتودرمانی‌ای که بین سال‌های ۱۹۸۵ تا ۱۹۸۷ به شش بیمار دوز پرتویی به‌مراتب بیش از حد مجاز داد و برخی از آن‌ها بر اثر عوارض ناشی از این پرتوگیری جان باختند. علت هر شش حادثه خطاهای نرم‌افزاری تشخیص داده شد.

انطباق‌پذیری

کاربردهای نرم‌افزاری امروزی، مانند مرورگرهای وب و موتورهای جستجوی اینترنتی، معمولاً برنامه‌های بزرگی هستند که سال‌ها مورد استفاده قرار می‌گیرند. بنابراین نرم‌افزار باید بتواند با گذشت زمان و در پاسخ به تغییر شرایط محیطش تکامل یابد. از این رو، یکی دیگر از اهداف مهم نرم‌افزار باکیفیت، دستیابی به انطباق‌پذیری (که تکامل‌پذیری یا evolvability نیز نامیده می‌شود) است. مفهوم قابلیت حمل (portability) با این موضوع مرتبط است؛ یعنی توانایی نرم‌افزار برای اجرا روی سخت‌افزارها و سیستم‌عامل‌های مختلف با کمترین تغییر. یکی از مزایای نوشتن نرم‌افزار به زبان پایتون، قابلیت حملی است که خود زبان فراهم می‌کند.

قابلیت استفاده مجدد

انطباق‌پذیری با تمایل به قابلیت استفاده مجدد نرم‌افزار همراه است؛ یعنی یک کد واحد بتواند به‌عنوان مؤلفه‌ای از سیستم‌های مختلف در کاربردهای گوناگون به کار رود. توسعه نرم‌افزار باکیفیت می‌تواند پرهزینه باشد، و اگر نرم‌افزار به‌گونه‌ای طراحی شود که استفاده مجدد از آن در کاربردهای آینده آسان باشد، بخشی از این هزینه جبران می‌شود. با این حال، چنین استفاده مجددی باید با احتیاط انجام شود؛ زیرا یکی از منابع اصلی خطاهای نرم‌افزاری Therac-25 استفاده مجدد نابه‌جا از نرم‌افزار Therac-20 بود (نرم‌افزاری که نه شی‌گرا بود و نه برای سکوی سخت‌افزاری Therac-25 طراحی شده بود).

۲.۱.۲ اصول طراحی شی‌گرا

مهم‌ترین اصول رویکرد شی‌گرا، که برای تسهیل دستیابی به اهداف بالا در نظر گرفته شده‌اند، عبارت‌اند از (شکل ۲.۲):

  • ماژولاریتی (Modularity)
  • انتزاع (Abstraction)
  • کپسوله‌سازی (Encapsulation)

شکل ۲.۲: اصول طراحی شی‌گرا.

ماژولاریتی

سیستم‌های نرم‌افزاری امروزی معمولاً از چندین مؤلفه مختلف تشکیل شده‌اند که برای کارکرد درست کل سیستم باید به‌درستی با یکدیگر تعامل کنند. درست نگه داشتن این تعامل‌ها مستلزم سازمان‌دهی مناسب این مؤلفه‌هاست. ماژولاریتی به اصلی سازمان‌دهنده اشاره دارد که بر اساس آن، مؤلفه‌های مختلف یک سیستم نرم‌افزاری به واحدهای عملکردی جداگانه تقسیم می‌شوند.

به‌عنوان قیاسی از دنیای واقعی، می‌توان یک خانه یا آپارتمان را متشکل از چند واحد در تعامل با یکدیگر دانست: سیستم برق، گرمایش و سرمایش، لوله‌کشی و سازه. معماری که با نظم و ترتیب خانه یا آپارتمانی را طراحی می‌کند، این سیستم‌ها را توده‌ای درهم از سیم، کانال، لوله و تخته نمی‌بیند، بلکه آن‌ها را ماژول‌هایی جداگانه می‌داند که به روش‌هایی کاملاً تعریف‌شده با هم تعامل دارند. او با این کار از ماژولاریتی برای رسیدن به وضوح فکری بهره می‌گیرد؛ وضوحی که راهی طبیعی برای سازمان‌دهی کارکردها در قالب واحدهای مجزا و مدیریت‌پذیر فراهم می‌کند.

به همین ترتیب، به‌کارگیری ماژولاریتی در یک سیستم نرم‌افزاری نیز می‌تواند چارچوب سازمانی قدرتمندی فراهم کند که به پیاده‌سازی وضوح می‌بخشد. در پایتون، ماژول (module) مجموعه‌ای از توابع و کلاس‌های نزدیک به هم است که با هم در یک فایل کد منبع تعریف شده‌اند. برای مثال، کتابخانه‌های استاندارد پایتون شامل ماژول math است که تعاریف ثابت‌ها و توابع کلیدی ریاضی را فراهم می‌کند، و ماژول os که امکان تعامل با سیستم‌عامل را فراهم می‌سازد.

استفاده از ماژولاریتی به تحقق اهداف فهرست‌شده در بخش ۲.۱.۱ کمک می‌کند. استحکام به‌طور چشمگیری افزایش می‌یابد، زیرا تست و اشکال‌زدایی مؤلفه‌های جداگانه پیش از یکپارچه شدن در یک سیستم نرم‌افزاری بزرگ‌تر آسان‌تر است. افزون بر این، اشکالاتی که در سیستم کامل باقی می‌مانند ممکن است تا یک مؤلفه خاص ردیابی شوند و بتوان آن‌ها را به‌طور نسبتاً مجزا برطرف کرد. ساختاری که ماژولاریتی تحمیل می‌کند به قابلیت استفاده مجدد نرم‌افزار نیز کمک می‌کند. اگر ماژول‌های نرم‌افزاری به شکلی کلی نوشته شوند، می‌توان هنگام بروز نیازهای مشابه در زمینه‌های دیگر از آن‌ها دوباره استفاده کرد. این موضوع به‌ویژه در مطالعه ساختارهای داده اهمیت دارد، زیرا معمولاً می‌توان آن‌ها را با انتزاع و کلیت کافی طراحی کرد تا در کاربردهای بسیاری قابل استفاده مجدد باشند.

انتزاع

ایده انتزاع این است که یک سیستم پیچیده را به بنیادی‌ترین اجزایش فروکاهیم. توصیف اجزای یک سیستم معمولاً شامل نام‌گذاری آن‌ها و توضیح کارکردشان است. به‌کارگیری پارادایم انتزاع در طراحی ساختارهای داده به انواع داده انتزاعی (Abstract Data Types یا ADT) می‌انجامد. ADT مدلی ریاضی از یک ساختار داده است که نوع داده‌های ذخیره‌شده، عملیات قابل‌انجام روی آن‌ها و نوع پارامترهای این عملیات را مشخص می‌کند. ADT تعیین می‌کند هر عملیات چه کاری انجام می‌دهد، اما نه اینکه چگونه آن را انجام می‌دهد. معمولاً به مجموعه رفتارهایی که یک ADT پشتیبانی می‌کند، رابط عمومی (public interface) آن می‌گوییم.

پایتون به‌عنوان یک زبان برنامه‌نویسی، در زمینه تعیین رابط‌ها آزادی عمل زیادی می‌دهد. در پایتون سنت بر این است که انتزاع‌ها به‌صورت ضمنی و با سازوکاری به نام تایپ اردکی (duck typing) به کار روند. از آنجا که پایتون زبانی تفسیری با تایپ پویاست، هیچ بررسی نوعی در «زمان کامپایل» انجام نمی‌شود و الزامی رسمی برای اعلان کلاس‌های پایه انتزاعی وجود ندارد. در عوض، برنامه‌نویسان فرض می‌کنند که یک شیء از مجموعه‌ای از رفتارهای شناخته‌شده پشتیبانی می‌کند، و اگر این فرض‌ها نادرست باشند، مفسر خطای زمان اجرا ایجاد می‌کند. اصطلاح «تایپ اردکی» از گفته‌ای منسوب به جیمز ویتکامب رایلی، شاعر آمریکایی، گرفته شده است: «وقتی پرنده‌ای می‌بینم که مثل اردک راه می‌رود، مثل اردک شنا می‌کند و مثل اردک صدا می‌دهد، آن پرنده را اردک می‌نامم.»

به‌طور رسمی‌تر، پایتون از انواع داده انتزاعی با سازوکاری به نام کلاس پایه انتزاعی (Abstract Base Class یا ABC) پشتیبانی می‌کند. از کلاس پایه انتزاعی نمی‌توان نمونه ساخت (یعنی نمی‌توان مستقیماً نمونه‌ای از آن کلاس ایجاد کرد)، اما این کلاس یک یا چند متد مشترک را تعریف می‌کند که همه پیاده‌سازی‌های آن انتزاع باید داشته باشند. یک ABC به‌وسیله یک یا چند کلاس عینی (concrete class) محقق می‌شود که از کلاس پایه انتزاعی ارث می‌برند و برای متدهای اعلام‌شده در ABC پیاده‌سازی ارائه می‌دهند. ماژول abc پایتون پشتیبانی رسمی از ABCها را فراهم می‌کند، هرچند ما برای سادگی از چنین اعلان‌هایی صرف‌نظر می‌کنیم. از چند کلاس پایه انتزاعی موجود در ماژول collections پایتون استفاده خواهیم کرد؛ این ماژول تعاریفی برای چند ADT رایج ساختار داده و پیاده‌سازی‌های عینی برخی از این انتزاع‌ها را در بر دارد.

کپسوله‌سازی

یکی دیگر از اصول مهم طراحی شی‌گرا کپسوله‌سازی است. مؤلفه‌های مختلف یک سیستم نرم‌افزاری نباید جزئیات درونی پیاده‌سازی خود را آشکار کنند. یکی از مزایای اصلی کپسوله‌سازی این است که به برنامه‌نویس آزادی می‌دهد جزئیات یک مؤلفه را پیاده‌سازی کند، بی‌آنکه نگران باشد برنامه‌نویسان دیگر کدی بنویسند که به‌شکلی پیچیده به این تصمیم‌های درونی وابسته باشد. تنها محدودیت برنامه‌نویس یک مؤلفه، حفظ رابط عمومی آن است، چون برنامه‌نویسان دیگر کدی خواهند نوشت که به همین رابط وابسته است. کپسوله‌سازی استحکام و انطباق‌پذیری را به همراه دارد، زیرا اجازه می‌دهد جزئیات پیاده‌سازی بخش‌هایی از برنامه بدون اثر نامطلوب بر بخش‌های دیگر تغییر کند و در نتیجه رفع اشکال یا افزودن قابلیت‌های جدید با تغییراتی نسبتاً محلی در یک مؤلفه آسان‌تر می‌شود.

در سراسر این کتاب به اصل کپسوله‌سازی پایبند خواهیم بود و روشن خواهیم کرد کدام جنبه‌های یک ساختار داده عمومی فرض می‌شوند و کدام جزئیات درونی‌اند. با این حال، پایتون تنها پشتیبانی ضعیفی از کپسوله‌سازی دارد. طبق قرارداد، نام اعضای یک کلاس (چه اعضای داده و چه توابع عضو) که با یک زیرخط شروع می‌شوند (مانند _secret) غیرعمومی فرض می‌شوند و نباید به آن‌ها تکیه کرد. این قراردادها با حذف عمدی چنین اعضایی از مستندات خودکار تقویت می‌شوند.

۲.۱.۳ الگوهای طراحی

طراحی شی‌گرا ساخت نرم‌افزار قابل استفاده مجدد، مستحکم و انطباق‌پذیر را تسهیل می‌کند. با این حال، طراحی کد خوب چیزی فراتر از صرفاً شناخت روش‌شناسی‌های شی‌گراست و به کاربرد مؤثر فنون طراحی شی‌گرا نیاز دارد.

پژوهشگران و متخصصان علوم کامپیوتر مفاهیم و روش‌شناسی‌های سازمانی گوناگونی برای طراحی نرم‌افزار شی‌گرای باکیفیت، یعنی نرم‌افزاری موجز، درست و قابل استفاده مجدد، ارائه کرده‌اند. مفهومی که برای این کتاب اهمیت ویژه دارد الگوی طراحی (design pattern) است که راه‌حلی برای یک مسئله «متداول» در طراحی نرم‌افزار توصیف می‌کند. الگو قالبی کلی برای راه‌حلی فراهم می‌کند که می‌توان آن را در موقعیت‌های بسیار متفاوتی به کار برد. الگو عناصر اصلی راه‌حل را به شکلی انتزاعی توصیف می‌کند که بتوان آن را برای مسئله خاص پیش رو تخصیص داد. هر الگو از این بخش‌ها تشکیل شده است: یک نام که الگو را مشخص می‌کند؛ یک زمینه (context) که موقعیت‌هایی را که الگو در آن‌ها قابل اعمال است توصیف می‌کند؛ یک قالب (template) که چگونگی اعمال الگو را توصیف می‌کند؛ و یک نتیجه (result) که آنچه الگو تولید می‌کند را توصیف و تحلیل می‌کند.

در این کتاب چند الگوی طراحی معرفی می‌کنیم و نشان می‌دهیم که چگونه می‌توان آن‌ها را به‌طور یکپارچه در پیاده‌سازی ساختارهای داده و الگوریتم‌ها به کار برد. این الگوها در دو گروه قرار می‌گیرند: الگوهایی برای حل مسائل طراحی الگوریتم و الگوهایی برای حل مسائل مهندسی نرم‌افزار. الگوهای طراحی الگوریتم که بررسی می‌کنیم عبارت‌اند از:

  • بازگشت (Recursion)
  • تحلیل سرشکنی (Amortization)
  • تقسیم و حل (Divide-and-conquer)
  • هرس و جستجو (Prune-and-search) که «کاهش و حل» (decrease-and-conquer) نیز نامیده می‌شود
  • جستجوی جامع یا روش نیروی خام (Brute force)
  • برنامه‌نویسی پویا (Dynamic programming)
  • روش حریصانه (Greedy method)

الگوهای طراحی مهندسی نرم‌افزار که بررسی می‌کنیم نیز عبارت‌اند از:

  • تکرارگر (Iterator)
  • آداپتور (Adapter)
  • موقعیت (Position)
  • ترکیب (Composition)
  • متد قالب (Template method)
  • مکان‌یاب (Locator)
  • متد کارخانه (Factory method)

با این حال، به جای توضیح همه این مفاهیم در اینجا، آن‌ها را به‌تدریج و در جای مناسب معرفی می‌کنیم؛ الگوهای تکرارگر و متد قالب در همین فصل (به‌ترتیب در بخش‌های ۲.۳.۴ و ۲.۴.۳) و بقیه در فصل‌های بعدی. برای هر الگو، چه در مهندسی الگوریتم و چه در مهندسی نرم‌افزار، کاربرد کلی آن را توضیح می‌دهیم و آن را دست‌کم با یک مثال عینی نشان می‌دهیم.

۲.۲ توسعه نرم‌افزار

توسعه نرم‌افزار به شیوه سنتی شامل چند مرحله است. سه گام اصلی عبارت‌اند از:

  1. طراحی
  2. پیاده‌سازی
  3. تست و اشکال‌زدایی

در این بخش به‌اختصار نقش این مراحل را بررسی می‌کنیم و چند شیوه خوب برنامه‌نویسی در پایتون را معرفی می‌کنیم، از جمله سبک کدنویسی، قراردادهای نام‌گذاری، مستندسازی رسمی و تست واحد.

۲.۲.۱ طراحی

در برنامه‌نویسی شی‌گرا، مرحله طراحی احتمالاً مهم‌ترین مرحله فرایند توسعه نرم‌افزار است؛ زیرا در همین مرحله است که تصمیم می‌گیریم کارکرد برنامه را چگونه میان کلاس‌ها تقسیم کنیم، این کلاس‌ها چگونه با هم تعامل داشته باشند، هر کدام چه داده‌ای ذخیره کنند و چه کارهایی انجام دهند. در واقع، یکی از چالش‌های اصلی برنامه‌نویسان مبتدی، تصمیم‌گیری درباره کلاس‌هایی است که باید برای انجام کار برنامه تعریف کنند. هرچند دستورالعمل‌های کلی در این زمینه به‌سختی یافت می‌شوند، چند قاعده سرانگشتی وجود دارد که می‌توان هنگام طراحی کلاس‌ها به کار برد:

  • مسئولیت‌ها: کار را میان بازیگران (actors) مختلف تقسیم کنید، به‌طوری که هر کدام مسئولیت متفاوتی داشته باشد. سعی کنید مسئولیت‌ها را با افعال کنشی توصیف کنید. این بازیگران کلاس‌های برنامه را تشکیل می‌دهند.
  • استقلال: کار هر کلاس را تا حد امکان مستقل از سایر کلاس‌ها تعریف کنید. مسئولیت‌ها را به‌گونه‌ای میان کلاس‌ها تقسیم کنید که هر کلاس بر جنبه‌ای از برنامه اختیار داشته باشد. داده‌ها را (به‌صورت متغیرهای نمونه) به کلاسی بسپارید که اختیار کارهای نیازمند دسترسی به آن داده‌ها با اوست.
  • رفتارها: رفتارهای هر کلاس را با دقت و به‌طور مشخص تعریف کنید تا پیامدهای هر عمل یک کلاس برای کلاس‌هایی که با آن تعامل دارند به‌خوبی روشن باشد. این رفتارها متدهای کلاس را تعریف می‌کنند و مجموعه رفتارهای یک کلاس، رابط (interface) آن را تشکیل می‌دهد، زیرا سایر بخش‌های کد از طریق همین رفتارها با اشیای آن کلاس تعامل می‌کنند.

تعریف کلاس‌ها به همراه متغیرهای نمونه و متدهایشان، کلید طراحی یک برنامه شی‌گراست. برنامه‌نویس خوب به‌طور طبیعی با گذشت زمان در انجام این کارها مهارت بیشتری پیدا می‌کند، زیرا تجربه به او می‌آموزد در نیازمندی‌های یک برنامه الگوهایی را تشخیص دهد که با الگوهایی که پیش‌تر دیده است مطابقت دارند.

یکی از ابزارهای رایج برای تهیه طرح اولیه و سطح‌بالای یک پروژه، کارت‌های CRC است. کارت‌های کلاس-مسئولیت-همکار (Class-Responsibility-Collaborator) کارت‌های یادداشت ساده‌ای هستند که کار مورد نیاز برنامه را تقسیم می‌کنند. ایده اصلی این ابزار آن است که هر کارت نماینده یک مؤلفه باشد که در نهایت به یک کلاس در برنامه تبدیل می‌شود. نام هر مؤلفه را بالای یک کارت می‌نویسیم. در سمت چپ کارت، مسئولیت‌های این مؤلفه را می‌نویسیم و در سمت راست، همکاران آن را فهرست می‌کنیم؛ یعنی مؤلفه‌های دیگری که این مؤلفه برای انجام وظایفش باید با آن‌ها تعامل داشته باشد.

فرایند طراحی در قالب چرخه‌ای از عمل/بازیگر تکرار می‌شود: ابتدا یک عمل (یعنی یک مسئولیت) را شناسایی می‌کنیم و سپس بازیگری (یعنی مؤلفه‌ای) را تعیین می‌کنیم که برای انجام آن عمل از همه مناسب‌تر است. طراحی زمانی کامل می‌شود که همه عمل‌ها را به بازیگران سپرده باشیم. با استفاده از کارت‌های یادداشت کوچک (به جای برگه‌های بزرگ‌تر)، بر این واقعیت تکیه می‌کنیم که هر مؤلفه باید مجموعه کوچکی از مسئولیت‌ها و همکاران داشته باشد. رعایت این قاعده به مدیریت‌پذیر ماندن تک‌تک کلاس‌ها کمک می‌کند.

با شکل گرفتن طرح، رویکرد استاندارد برای توضیح و مستندسازی آن، استفاده از نمودارهای UML (زبان مدل‌سازی یکپارچه) برای بیان سازمان برنامه است. نمودارهای UML نمادگذاری دیداری استانداردی برای بیان طرح‌های نرم‌افزاری شی‌گرا هستند و ابزارهای رایانه‌ای متعددی برای ساخت آن‌ها وجود دارد. یکی از انواع نمودارهای UML نمودار کلاس (class diagram) نام دارد. نمونه‌ای از چنین نموداری در شکل ۲.۳ برای کلاسی آمده است که کارت اعتباری یک مصرف‌کننده را نمایش می‌دهد. این نمودار سه بخش دارد: بخش اول نام کلاس، بخش دوم متغیرهای نمونه پیشنهادی و بخش سوم متدهای پیشنهادی کلاس را مشخص می‌کند. در بخش ۲.۲.۳ قراردادهای نام‌گذاری خود را شرح می‌دهیم و در بخش ۲.۳.۱ پیاده‌سازی کامل کلاس CreditCard در پایتون را بر اساس همین طرح ارائه می‌کنیم.

کلاس: CreditCard
فیلدها: _customer، _bank، _account، _balance، _limit
رفتارها: get_customer()، get_bank()، get_account()، make_payment(amount)، get_balance()، get_limit()، charge(price)

شکل ۲.۳: نمودار کلاس برای کلاس پیشنهادی CreditCard.

۲.۲.۲ شبه‌کد

به‌عنوان گامی میانی پیش از پیاده‌سازی یک طرح، اغلب از برنامه‌نویسان خواسته می‌شود الگوریتم‌ها را به شکلی توصیف کنند که فقط برای خواندن انسان در نظر گرفته شده است. چنین توصیف‌هایی شبه‌کد (pseudo-code) نامیده می‌شوند. شبه‌کد برنامه کامپیوتری نیست، اما ساختارمندتر از نثر معمولی است؛ ترکیبی از زبان طبیعی و ساختارهای برنامه‌نویسی سطح‌بالاست که ایده‌های اصلی پیاده‌سازی عمومی یک ساختار داده یا الگوریتم را توصیف می‌کند. از آنجا که شبه‌کد برای خواننده انسانی طراحی شده است، نه کامپیوتر، می‌توانیم ایده‌های سطح‌بالا را بدون درگیر شدن با جزئیات سطح‌پایین پیاده‌سازی منتقل کنیم. در عین حال، نباید از گام‌های مهم چشم‌پوشی کنیم. همانند بسیاری از شکل‌های ارتباط انسانی، یافتن توازن مناسب مهارتی مهم است که با تمرین پرورش می‌یابد.

در این کتاب از سبکی از شبه‌کد استفاده می‌کنیم که امیدواریم برای برنامه‌نویسان پایتون روشن باشد، اما با آمیزه‌ای از نمادهای ریاضی و نثر انگلیسی. برای مثال، ممکن است به جای دستور رسمی raise از عبارت «اعلام خطا» استفاده کنیم. به پیروی از قراردادهای پایتون، برای نشان دادن محدوده ساختارهای کنترلی از تورفتگی استفاده می‌کنیم و نمادگذاری‌ای برای اندیس‌ها به کار می‌بریم که در آن عناصر دنباله A به طول n از A[0] تا A[n-1] شماره‌گذاری می‌شوند. با این حال، در شبه‌کد خود توضیحات (comments) را به جای نویسه # پایتون، درون آکولاد { مانند این } قرار می‌دهیم.

۲.۲.۳ سبک کدنویسی و مستندسازی

برنامه‌ها باید به‌آسانی خوانده و فهمیده شوند. بنابراین برنامه‌نویسان خوب باید به سبک کدنویسی خود توجه کنند و سبکی را پرورش دهند که جنبه‌های مهم طراحی برنامه را هم به انسان و هم به کامپیوتر منتقل کند. قراردادهای سبک کدنویسی در جوامع برنامه‌نویسی مختلف متفاوت‌اند. راهنمای رسمی سبک کد پایتون (PEP 8) به‌صورت برخط در نشانی زیر در دسترس است:

http://www.python.org/dev/peps/pep-0008/

اصول اصلی‌ای که ما به کار می‌گیریم به شرح زیر است:

  • بلوک‌های کد پایتون معمولاً با ۴ فاصله تورفتگی پیدا می‌کنند. با این حال، برای اینکه قطعه‌کدهای کتاب از حاشیه صفحات بیرون نزنند، در کتاب اصلی برای هر سطح تورفتگی از ۲ فاصله استفاده شده است. اکیداً توصیه می‌شود از نویسه تب (tab) استفاده نکنید، زیرا تب‌ها در سیستم‌های مختلف با عرض‌های متفاوت نمایش داده می‌شوند و مفسر پایتون تب و فاصله را یکسان در نظر نمی‌گیرد. بسیاری از ویرایشگرهای آشنا با پایتون به‌طور خودکار تب‌ها را با تعداد مناسبی فاصله جایگزین می‌کنند.

    یادداشت مترجم: در این نسخه، قطعه‌کدها با تورفتگی استاندارد ۴ فاصله‌ای آمده‌اند.

  • برای شناسه‌ها از نام‌های معنادار استفاده کنید. سعی کنید نام‌هایی برگزینید که بتوان آن‌ها را بلند خواند و بازتاب‌دهنده عمل، مسئولیت یا داده‌ای باشند که هر شناسه نام آن است.

    • نام کلاس‌ها (به جز کلاس‌های داخلی پایتون) باید اسمی مفرد باشد و با حرف بزرگ شروع شود (مثلاً Date و نه date یا Dates). وقتی چند واژه برای ساختن نام کلاس به هم می‌پیوندند، باید از قرارداد موسوم به «CamelCase» پیروی کنند که در آن حرف اول هر واژه بزرگ نوشته می‌شود (مثلاً CreditCard).
    • نام توابع، از جمله توابع عضو یک کلاس، باید با حروف کوچک نوشته شود. اگر از چند واژه تشکیل شده باشد، واژه‌ها باید با زیرخط از هم جدا شوند (مثلاً make_payment). نام تابع معمولاً باید فعلی باشد که اثر آن را توصیف کند. با این حال، اگر تنها هدف تابع بازگرداندن یک مقدار باشد، نام آن می‌تواند اسمی باشد که آن مقدار را توصیف می‌کند (مثلاً sqrt و نه calculate_sqrt).
    • نام‌هایی که یک شیء منفرد را مشخص می‌کنند (مثلاً پارامتر، متغیر نمونه یا متغیر محلی) باید اسمی با حروف کوچک باشند (مثلاً price). گاهی هنگام استفاده از یک حرف بزرگ تنها برای نام‌گذاری یک ساختار داده (مانند درخت T) از این قاعده عدول می‌کنیم.
    • شناسه‌هایی که نماینده مقداری ثابت (constant) هستند، به‌طور سنتی تماماً با حروف بزرگ و با زیرخط میان واژه‌ها نوشته می‌شوند (مثلاً MAX_SIZE).

    از بحث کپسوله‌سازی به یاد دارید که شناسه‌هایی که در هر زمینه‌ای با یک زیرخط آغاز می‌شوند (مثلاً _secret) به این منظورند که نشان دهند فقط برای استفاده «درونی» یک کلاس یا ماژول هستند و جزئی از رابط عمومی به شمار نمی‌روند.

  • از توضیحاتی (comments) استفاده کنید که به برنامه معنا می‌افزایند و ساختارهای مبهم یا گیج‌کننده را روشن می‌کنند. توضیحات درون‌خطی برای توضیح‌های کوتاه مناسب‌اند؛ در پایتون این توضیحات پس از نویسه # می‌آیند، مانند:

    if n % 2 == 1:    # n is odd

    توضیحات چندخطی بلوکی برای توضیح بخش‌های پیچیده‌تر کد مناسب‌اند. در پایتون، این‌ها از نظر فنی رشته‌های لفظی چندخطی هستند که معمولاً با سه علامت نقل‌قول (""") محصور می‌شوند و هنگام اجرا هیچ اثری ندارند. در بخش بعد، کاربرد توضیحات بلوکی برای مستندسازی را بررسی می‌کنیم.

مستندسازی

پایتون پشتیبانی یکپارچه‌ای برای گنجاندن مستندات رسمی مستقیماً در کد منبع، از طریق سازوکاری به نام docstring (رشته مستندات)، فراهم می‌کند. به‌طور رسمی، هر رشته لفظی‌ای که به‌عنوان نخستین دستور در بدنه یک ماژول، کلاس یا تابع (از جمله تابع عضو یک کلاس) ظاهر شود، docstring به شمار می‌آید. طبق قرارداد، این رشته‌ها باید درون سه علامت نقل‌قول (""") قرار گیرند. برای نمونه، تابع scale را که در مباحث مقدماتی پایتون دیده‌ایم می‌توان این‌گونه مستند کرد:

def scale(data, factor):
    """Multiply all entries of numeric data list by the given factor."""
    for j in range(len(data)):
        data[j] *= factor

استفاده از سه علامت نقل‌قول برای docstring حتی وقتی رشته در یک خط جا می‌شود، مانند مثال بالا، رایج است. docstringهای مفصل‌تر باید با یک خط که هدف را خلاصه می‌کند آغاز شوند، سپس یک خط خالی و بعد جزئیات بیشتر بیاید. برای مثال، می‌توانیم تابع scale را روشن‌تر این‌گونه مستند کنیم:

def scale(data, factor):
    """Multiply all entries of numeric data list by the given factor.

    data    an instance of any mutable sequence type (such as a list)
            containing numeric elements

    factor  a number that serves as the multiplicative factor for scaling
    """
    for j in range(len(data)):
        data[j] *= factor

docstring به‌عنوان یک فیلد از ماژول، تابع یا کلاسی که در آن اعلان شده ذخیره می‌شود. این رشته نقش مستندات را دارد و به روش‌های گوناگون قابل بازیابی است. برای مثال، فرمان help(x) در مفسر پایتون مستندات مربوط به شیء x را نمایش می‌دهد. ابزاری خارجی به نام pydoc همراه پایتون توزیع می‌شود که می‌توان از آن برای تولید مستندات رسمی به‌صورت متن یا صفحه وب استفاده کرد. رهنمودهایی برای نوشتن docstringهای مفید در نشانی زیر در دسترس است:

http://www.python.org/dev/peps/pep-0257/

در این کتاب سعی می‌کنیم هر جا فضا اجازه دهد docstringها را بیاوریم. docstringهای حذف‌شده را می‌توان در نسخه برخط کد منبع کتاب یافت.

۲.۲.۴ تست و اشکال‌زدایی

تست فرایند بررسی تجربی درستی یک برنامه است و اشکال‌زدایی فرایند ردیابی اجرای برنامه و کشف خطاهای آن. تست و اشکال‌زدایی اغلب زمان‌برترین بخش توسعه یک برنامه‌اند.

تست

داشتن یک برنامه تست دقیق، بخش جدایی‌ناپذیر نوشتن برنامه است. هرچند بررسی درستی برنامه برای همه ورودی‌های ممکن معمولاً امکان‌پذیر نیست، باید بکوشیم برنامه را روی زیرمجموعه‌ای نماینده از ورودی‌ها اجرا کنیم. دست‌کم باید مطمئن شویم که هر متد یک کلاس حداقل یک بار تست می‌شود (پوشش متد). بهتر از آن، هر دستور برنامه باید حداقل یک بار اجرا شود (پوشش دستور).

برنامه‌ها اغلب در حالت‌های خاص ورودی دچار خطا می‌شوند. چنین حالت‌هایی باید با دقت شناسایی و تست شوند. برای مثال، هنگام تست متدی که دنباله‌ای از اعداد صحیح را مرتب می‌کند (یعنی به ترتیب درمی‌آورد)، باید ورودی‌های زیر را در نظر بگیریم:

  • دنباله طول صفر دارد (هیچ عنصری ندارد).
  • دنباله فقط یک عنصر دارد.
  • همه عناصر دنباله یکسان‌اند.
  • دنباله از قبل مرتب است.
  • دنباله به ترتیب معکوس مرتب است.

علاوه بر ورودی‌های خاص برنامه، باید شرایط خاص ساختارهایی را که برنامه به کار می‌برد نیز در نظر بگیریم. برای مثال، اگر برای ذخیره داده‌ها از لیست پایتون استفاده می‌کنیم، باید مطمئن شویم که حالت‌های مرزی، مانند درج یا حذف در ابتدا یا انتهای لیست، به‌درستی مدیریت می‌شوند.

هرچند استفاده از مجموعه‌تست‌های دست‌ساز ضروری است، اجرای برنامه روی مجموعه بزرگی از ورودی‌های تصادفی نیز سودمند است. ماژول random پایتون ابزارهای متعددی برای تولید اعداد تصادفی یا به‌هم‌ریختن تصادفی ترتیب مجموعه‌ها فراهم می‌کند.

وابستگی‌های میان کلاس‌ها و توابع یک برنامه، سلسله‌مراتبی پدید می‌آورد. به این معنا که مؤلفه A در این سلسله‌مراتب بالاتر از مؤلفه B قرار دارد اگر A به B وابسته باشد؛ مثلاً وقتی تابع A تابع B را فراخوانی می‌کند، یا تابع A به پارامتری نیاز دارد که نمونه‌ای از کلاس B است. دو راهبرد اصلی تست وجود دارد: از بالا به پایین (top-down) و از پایین به بالا (bottom-up) که در ترتیب تست مؤلفه‌ها با هم تفاوت دارند.

تست از بالا به پایین از بالای سلسله‌مراتب برنامه به سمت پایین پیش می‌رود. این روش معمولاً همراه با استاب‌سازی (stubbing) به کار می‌رود؛ فنی راه‌اندازانه که در آن یک مؤلفه سطح پایین‌تر با یک استاب (stub) جایگزین می‌شود، یعنی جانشینی که کارکرد مؤلفه اصلی را شبیه‌سازی می‌کند. برای مثال، اگر تابع A برای خواندن نخستین خط یک فایل تابع B را فراخوانی کند، هنگام تست A می‌توانیم B را با استابی جایگزین کنیم که یک رشته ثابت برمی‌گرداند.

تست از پایین به بالا از مؤلفه‌های سطح پایین‌تر به سمت مؤلفه‌های سطح بالاتر پیش می‌رود. برای مثال، ابتدا توابع پایین‌ترین سطح که هیچ تابع دیگری را فراخوانی نمی‌کنند تست می‌شوند، سپس توابعی که فقط این توابع را فراخوانی می‌کنند، و به همین ترتیب. به همین شکل، کلاسی که به هیچ کلاس دیگری وابسته نیست می‌تواند پیش از کلاسی که به آن وابسته است تست شود. این شکل از تست معمولاً تست واحد (unit testing) نامیده می‌شود، زیرا کارکرد یک مؤلفه خاص جدا از پروژه نرم‌افزاری بزرگ‌تر تست می‌شود. اگر این راهبرد به‌درستی به کار رود، علت خطاها را بهتر به مؤلفه در حال تست محدود می‌کند، زیرا مؤلفه‌های سطح پایین‌تری که این مؤلفه به آن‌ها وابسته است باید پیش‌تر به‌طور کامل تست شده باشند.

پایتون چند شکل پشتیبانی از تست خودکار فراهم می‌کند. وقتی توابع یا کلاس‌ها در یک ماژول تعریف می‌شوند، می‌توان تست‌های آن ماژول را در همان فایل گنجاند (سازوکار این کار در مباحث مقدماتی پایتون شرح داده شده است). کدی که درون یک ساختار شرطی به شکل زیر قرار گیرد:

if __name__ == '__main__':
    # perform tests...

فقط وقتی اجرا می‌شود که پایتون مستقیماً روی همان ماژول اجرا شود، نه وقتی که ماژول برای استفاده در یک پروژه بزرگ‌تر وارد (import) می‌شود. رایج است که تست‌ها را در چنین ساختاری قرار دهیم تا کارکرد توابع و کلاس‌هایی را که به‌طور خاص در آن ماژول تعریف شده‌اند بیازماییم.

پشتیبانی قوی‌تری از خودکارسازی تست واحد را ماژول unittest پایتون فراهم می‌کند. این چارچوب امکان می‌دهد موارد تست منفرد در قالب مجموعه‌تست‌های بزرگ‌تر گروه‌بندی شوند و از اجرای این مجموعه‌ها و گزارش یا تحلیل نتایج آن‌ها پشتیبانی می‌کند. در طول نگهداری نرم‌افزار، از تست رگرسیون (regression testing) استفاده می‌شود؛ یعنی همه تست‌های قبلی دوباره اجرا می‌شوند تا اطمینان حاصل شود که تغییرات نرم‌افزار اشکال جدیدی در مؤلفه‌های پیش‌تر تست‌شده ایجاد نکرده است.

اشکال‌زدایی

ساده‌ترین روش اشکال‌زدایی استفاده از دستورهای چاپ (print) برای پیگیری مقدار متغیرها در طول اجرای برنامه است. مشکل این روش آن است که سرانجام باید این دستورها را حذف یا به توضیح تبدیل کرد تا هنگام انتشار نهایی نرم‌افزار اجرا نشوند.

روش بهتر، اجرای برنامه درون یک اشکال‌زدا (debugger) است؛ محیطی تخصصی برای کنترل و پایش اجرای برنامه. کارکرد اصلی اشکال‌زدا، امکان درج نقطه توقف (breakpoint) در کد است. وقتی برنامه درون اشکال‌زدا اجرا می‌شود، در هر نقطه توقف متوقف می‌شود و در این حالت می‌توان مقدار فعلی متغیرها را بررسی کرد.

توزیع استاندارد پایتون ماژولی به نام pdb دارد که پشتیبانی از اشکال‌زدایی را مستقیماً درون مفسر فراهم می‌کند. بیشتر محیط‌های توسعه یکپارچه (IDE) پایتون، مانند IDLE، محیط‌های اشکال‌زدایی با رابط کاربری گرافیکی ارائه می‌کنند.

۲.۳ تعریف کلاس‌ها

کلاس ابزار اصلی انتزاع در برنامه‌نویسی شی‌گراست. در پایتون، هر داده‌ای به‌صورت نمونه‌ای از یک کلاس نمایش داده می‌شود. کلاس مجموعه‌ای از رفتارها را در قالب توابع عضو (که متد نیز نامیده می‌شوند) فراهم می‌کند که پیاده‌سازی آن‌ها میان همه نمونه‌های آن کلاس مشترک است. کلاس همچنین نقش نقشه ساخت (blueprint) نمونه‌هایش را دارد و عملاً تعیین می‌کند اطلاعات وضعیت هر نمونه در قالب صفت‌ها (attributes)، که فیلد، متغیر نمونه یا عضو داده نیز نامیده می‌شوند، چگونه نمایش داده شود.

۲.۳.۱ مثال: کلاس CreditCard

در نخستین مثال، پیاده‌سازی کلاس CreditCard را بر اساس طرحی که در شکل ۲.۳ از بخش ۲.۲.۱ معرفی کردیم ارائه می‌دهیم. نمونه‌های کلاس CreditCard مدلی ساده از کارت‌های اعتباری سنتی ارائه می‌کنند. این نمونه‌ها اطلاعات شناسایی مشتری، بانک، شماره حساب، سقف اعتبار و موجودی فعلی را نگه می‌دارند. این کلاس از ثبت هزینه‌هایی که موجب شوند موجودی کارت از سقف خرج آن فراتر رود جلوگیری می‌کند، اما بهره یا جریمه دیرکرد دریافت نمی‌کند (این موضوعات را در بخش ۲.۴.۱ دوباره بررسی می‌کنیم).

کد ما در قطعه‌کد ۲.۱ آغاز می‌شود و در قطعه‌کد ۲.۲ ادامه می‌یابد. این ساختار با کلیدواژه class آغاز می‌شود و پس از آن نام کلاس، یک دونقطه و سپس بلوکی تورفته از کد می‌آید که بدنه کلاس را تشکیل می‌دهد. بدنه شامل تعریف همه متدهای کلاس است. این متدها با همان فنونی که برای تعریف توابع به کار می‌رود تعریف می‌شوند، اما پارامتر ویژه‌ای به نام self دارند که نمونه خاصی را که عضو روی آن فراخوانی شده مشخص می‌کند.

شناسه self

در پایتون، شناسه self نقش کلیدی دارد. در مورد کلاس CreditCard، طبیعتاً ممکن است نمونه‌های متعددی از این کلاس وجود داشته باشد و هر کدام باید موجودی، سقف اعتبار و دیگر مشخصات خود را نگه دارد. بنابراین هر نمونه متغیرهای نمونه خودش را برای بازتاب وضعیت فعلی‌اش ذخیره می‌کند.

از نظر نحوی، self نمونه‌ای را مشخص می‌کند که متد روی آن فراخوانی شده است. برای مثال، فرض کنید کاربرِ کلاس ما متغیری به نام my_card دارد که به نمونه‌ای از کلاس CreditCard اشاره می‌کند. وقتی کاربر my_card.get_balance() را فراخوانی می‌کند، شناسه self در تعریف متد get_balance به همان کارتی اشاره می‌کند که فراخواننده آن را my_card می‌نامد. عبارت self._balance به متغیر نمونه‌ای به نام _balance اشاره دارد که بخشی از وضعیت همان کارت اعتباری خاص است.

class CreditCard:
    """A consumer credit card."""

    def __init__(self, customer, bank, acnt, limit):
        """Create a new credit card instance.

        The initial balance is zero.

        customer  the name of the customer (e.g., 'John Bowman')
        bank      the name of the bank (e.g., 'California Savings')
        acnt      the account identifier (e.g., '5391 0375 9387 5309')
        limit     credit limit (measured in dollars)
        """
        self._customer = customer
        self._bank = bank
        self._account = acnt
        self._limit = limit
        self._balance = 0

    def get_customer(self):
        """Return name of the customer."""
        return self._customer

    def get_bank(self):
        """Return the bank's name."""
        return self._bank

    def get_account(self):
        """Return the card identifying number (typically stored as a string)."""
        return self._account

    def get_limit(self):
        """Return current credit limit."""
        return self._limit

    def get_balance(self):
        """Return current balance."""
        return self._balance

قطعه‌کد ۲.۱: آغاز تعریف کلاس CreditCard (ادامه در قطعه‌کد ۲.۲).

    def charge(self, price):
        """Charge given price to the card, assuming sufficient credit limit.

        Return True if charge was processed; False if charge was denied.
        """
        if price + self._balance > self._limit:  # if charge would exceed limit,
            return False                          # cannot accept charge
        else:
            self._balance += price
            return True

    def make_payment(self, amount):
        """Process customer payment that reduces balance."""
        self._balance -= amount

قطعه‌کد ۲.۲: پایان تعریف کلاس CreditCard (ادامه قطعه‌کد ۲.۱). این متدها درون تعریف کلاس تورفتگی دارند.

به تفاوت میان امضای متد آن‌گونه که در کلاس اعلان شده و آن‌گونه که فراخواننده به کار می‌برد توجه کنید. برای مثال، از دید کاربر، متد get_balance هیچ پارامتری نمی‌گیرد، اما در تعریف کلاس، self یک پارامتر صریح است. به همین ترتیب، متد charge در کلاس با دو پارامتر (self و price) اعلان شده، هرچند با یک پارامتر فراخوانی می‌شود؛ مثلاً my_card.charge(200). مفسر به‌طور خودکار نمونه‌ای را که متد روی آن فراخوانی شده به پارامتر self مقید می‌کند.

سازنده

کاربر می‌تواند با نحوی مانند زیر نمونه‌ای از کلاس CreditCard بسازد:

cc = CreditCard('John Doe', '1st Bank', '5391 0375 9387 5309', 1000)

در پشت صحنه، این دستور به فراخوانی متدی با نام ویژه __init__ می‌انجامد که سازنده (constructor) کلاس است. مسئولیت اصلی سازنده، برقراری وضعیت شیء کارت اعتباریِ تازه‌ساخته با متغیرهای نمونه مناسب است. در مورد کلاس CreditCard، هر شیء پنج متغیر نمونه دارد که آن‌ها را _customer، _bank، _account، _limit و _balance می‌نامیم. مقادیر اولیه چهار متغیر نخست به‌صورت پارامترهای صریحی که کاربر هنگام ساخت کارت اعتباری می‌فرستد ارائه و در بدنه سازنده مقداردهی می‌شوند. برای مثال، دستور self._customer = customer مقدار پارامتر customer را به متغیر نمونه self._customer نسبت می‌دهد؛ توجه کنید که چون customer در سمت راست بدون پیشوند (unqualified) آمده است، به پارامتر موجود در فضای نام محلی اشاره دارد.

کپسوله‌سازی

طبق قراردادهایی که در بخش ۲.۲.۳ شرح داده شد، یک زیرخط در ابتدای نام عضو داده، مانند _balance، نشان می‌دهد که آن عضو غیرعمومی (nonpublic) در نظر گرفته شده است. کاربران کلاس نباید مستقیماً به چنین اعضایی دسترسی داشته باشند.

به‌عنوان یک قاعده کلی، همه اعضای داده را غیرعمومی در نظر می‌گیریم. این کار به ما امکان می‌دهد وضعیتی سازگار را برای همه نمونه‌ها بهتر تضمین کنیم. می‌توانیم متدهای دسترسی (accessor)، مانند get_balance، فراهم کنیم تا کاربر کلاس به یک ویژگی دسترسی فقط‌خواندنی داشته باشد. اگر بخواهیم کاربر بتواند وضعیت را تغییر دهد، می‌توانیم متدهای به‌روزرسانی مناسبی فراهم کنیم. در زمینه ساختارهای داده، کپسوله‌سازی نمایش درونی به ما انعطاف بیشتری می‌دهد تا شیوه کار یک کلاس را بازطراحی کنیم؛ مثلاً برای بهبود کارایی ساختار.

متدهای دیگر

جالب‌ترین رفتارهای کلاس ما charge و make_payment هستند. تابع charge معمولاً مبلغ داده‌شده را به موجودی کارت اعتباری اضافه می‌کند تا خریدی به آن مبلغ از سوی مشتری را ثبت کند. با این حال، پیاده‌سازی ما پیش از پذیرش هزینه بررسی می‌کند که خرید جدید موجب فراتر رفتن موجودی از سقف اعتبار نشود. متد make_payment نشان‌دهنده پرداخت مبلغ مشخصی از سوی مشتری به بانک است و در نتیجه موجودی کارت را کاهش می‌دهد. توجه کنید که در دستور self._balance -= amount، عبارت self._balance با پیشوند self آمده، زیرا نماینده یک متغیر نمونه کارت است، در حالی که amount بدون پیشوند به پارامتر محلی اشاره می‌کند.

بررسی خطا

پیاده‌سازی ما از کلاس CreditCard چندان مستحکم نیست. نخست اینکه نوع پارامترهای charge و make_payment و همچنین پارامترهای سازنده را صراحتاً بررسی نکرده‌ایم. اگر کاربری فراخوانی‌ای مانند visa.charge('candy') انجام دهد، احتمالاً کد ما هنگام تلاش برای افزودن این پارامتر به موجودی فعلی از کار می‌افتد. اگر قرار بود این کلاس به‌طور گسترده در یک کتابخانه به کار رود، می‌توانستیم با فنون دقیق‌تر مدیریت استثنا، هنگام مواجهه با چنین استفاده نادرستی خطای TypeError ایجاد کنیم.

فراتر از خطاهای آشکار نوع، پیاده‌سازی ما ممکن است در معرض خطاهای منطقی نیز باشد. برای مثال، اگر کاربر بتواند مبلغی منفی ثبت کند، مانند visa.charge(-300)، این کار موجودی مشتری را کاهش می‌دهد. این یک راه گریز برای کاهش موجودی بدون پرداخت فراهم می‌کند. البته اگر هدف، مدل‌سازی اعتباری باشد که مشتری هنگام بازگرداندن کالا به فروشگاه دریافت می‌کند، چنین استفاده‌ای ممکن است معتبر تلقی شود. برخی از این مسائل کلاس CreditCard را در تمرین‌های پایان فصل بررسی خواهیم کرد.

تست کلاس

در قطعه‌کد ۲.۳، کاربرد پایه کلاس CreditCard را نشان می‌دهیم و سه کارت را در لیستی به نام wallet قرار می‌دهیم. با حلقه‌ها چند هزینه و پرداخت انجام می‌دهیم و با متدهای دسترسی مختلف نتایج را در خروجی چاپ می‌کنیم.

این تست‌ها درون شرط if __name__ == '__main__': قرار گرفته‌اند تا بتوان آن‌ها را همراه تعریف کلاس در کد منبع گنجاند. با اصطلاحات بخش ۲.۲.۴، این تست‌ها پوشش متد را فراهم می‌کنند، زیرا هر متد دست‌کم یک بار فراخوانی می‌شود، اما پوشش دستور را فراهم نمی‌کنند، زیرا هیچ موردی وجود ندارد که در آن هزینه‌ای به دلیل سقف اعتبار رد شود. این شکل از تست چندان پیشرفته نیست، زیرا خروجی تست‌ها باید به‌صورت دستی بازبینی شود تا معلوم شود کلاس مطابق انتظار رفتار کرده است یا نه. پایتون ابزارهایی برای تست رسمی‌تر دارد (به بحث ماژول unittest در بخش ۲.۲.۴ مراجعه کنید) تا بتوان مقادیر به‌دست‌آمده را به‌طور خودکار با نتایج پیش‌بینی‌شده مقایسه کرد و فقط هنگام تشخیص خطا خروجی تولید شود.

if __name__ == '__main__':
    wallet = []
    wallet.append(CreditCard('John Bowman', 'California Savings',
                             '5391 0375 9387 5309', 2500))
    wallet.append(CreditCard('John Bowman', 'California Federal',
                             '3485 0399 3395 1954', 3500))
    wallet.append(CreditCard('John Bowman', 'California Finance',
                             '5391 0375 9387 5309', 5000))

    for val in range(1, 17):
        wallet[0].charge(val)
        wallet[1].charge(2*val)
        wallet[2].charge(3*val)

    for c in range(3):
        print('Customer =', wallet[c].get_customer())
        print('Bank =', wallet[c].get_bank())
        print('Account =', wallet[c].get_account())
        print('Limit =', wallet[c].get_limit())
        print('Balance =', wallet[c].get_balance())
        while wallet[c].get_balance() > 100:
            wallet[c].make_payment(100)
            print('New balance =', wallet[c].get_balance())
        print()

قطعه‌کد ۲.۳: تست کلاس CreditCard.

۲.۳.۲ سربارگذاری عملگر و متدهای ویژه پایتون

کلاس‌های داخلی پایتون معنای طبیعی‌ای برای بسیاری از عملگرها فراهم می‌کنند. برای مثال، نحو a + b برای انواع عددی عمل جمع و برای انواع دنباله‌ای عمل الحاق را انجام می‌دهد. هنگام تعریف کلاسی جدید، باید بررسی کنیم که آیا نحوی مانند a + b باید زمانی که a یا b نمونه‌ای از آن کلاس است تعریف شود یا نه.

به‌طور پیش‌فرض، عملگر + برای کلاس جدید تعریف نشده است. با این حال، نویسنده کلاس می‌تواند با فنی به نام سربارگذاری عملگر (operator overloading) تعریفی برای آن ارائه دهد. این کار با پیاده‌سازی متدی با نام ویژه انجام می‌شود. به‌طور مشخص، عملگر + با پیاده‌سازی متدی به نام __add__ سربارگذاری می‌شود که عملوند سمت راست را به‌عنوان پارامتر می‌گیرد و نتیجه عبارت را برمی‌گرداند. یعنی نحو a + b به فراخوانی متدی روی شیء a به شکل a.__add__(b) تبدیل می‌شود. متدهایی با نام‌های ویژه مشابه برای سایر عملگرها نیز وجود دارد. جدول ۲.۱ فهرست جامعی از این متدها ارائه می‌دهد.

وقتی یک عملگر دودویی روی دو نمونه از انواع متفاوت اعمال می‌شود، مانند 3 * 'love me'، پایتون اولویت را به کلاس عملوند سمت چپ می‌دهد. در این مثال، عملاً بررسی می‌کند که آیا کلاس int از طریق متد __mul__ تعریف مناسبی برای ضرب یک نمونه در یک رشته دارد یا نه. اما اگر آن کلاس چنین رفتاری را پیاده‌سازی نکرده باشد، پایتون تعریف کلاس عملوند سمت راست را برای یافتن متد ویژه‌ای به نام __rmul__ (یعنی «ضرب از راست») بررسی می‌کند. این سازوکار راهی فراهم می‌کند تا کلاس جدیدی که کاربر تعریف کرده است از عملیات ترکیبی‌ای پشتیبانی کند که نمونه‌ای از یک کلاس موجود را در بر دارد (با این فرض طبیعی که کلاس موجود رفتاری برای این کلاس جدید تعریف نکرده است). تمایز میان __mul__ و __rmul__ همچنین به کلاس امکان می‌دهد در مواردی مانند ضرب ماتریس‌ها که عملیات در آن‌ها جابه‌جایی‌پذیر نیست (یعنی A * x ممکن است با x * A متفاوت باشد)، معناهای متفاوتی تعریف کند.

سربارگذاری‌های غیرعملگری

علاوه بر سربارگذاری سنتی عملگرها، پایتون برای کنترل رفتار بسیاری از قابلیت‌های دیگر هنگام اعمال روی کلاس‌های تعریف‌شده توسط کاربر نیز به متدهایی با نام ویژه تکیه می‌کند. برای مثال، نحو str(foo) به‌طور رسمی فراخوانی سازنده کلاس رشته است. طبیعتاً اگر پارامتر نمونه‌ای از کلاسی تعریف‌شده توسط کاربر باشد، نویسندگان اصلی کلاس رشته نمی‌توانسته‌اند بدانند آن نمونه چگونه باید نمایش داده شود. از این رو سازنده رشته متدی با نام ویژه، یعنی foo.__str__()، را فراخوانی می‌کند که باید نمایش رشته‌ای مناسبی برگرداند.

متدهای ویژه مشابهی برای تعیین چگونگی ساخت یک int، float یا bool از پارامتری از یک کلاس تعریف‌شده توسط کاربر به کار می‌روند. تبدیل به مقدار بولی اهمیت ویژه‌ای دارد، زیرا نحو if foo: حتی وقتی foo به‌طور رسمی مقداری بولی نیست نیز قابل استفاده است. برای کلاس تعریف‌شده توسط کاربر، این شرط با متد ویژه foo.__bool__() ارزیابی می‌شود.

نحو رایج شکل متد ویژه
a + b a.__add__(b)؛ در غیر این صورت b.__radd__(a)
a - b a.__sub__(b)؛ در غیر این صورت b.__rsub__(a)
a * b a.__mul__(b)؛ در غیر این صورت b.__rmul__(a)
a / b a.__truediv__(b)؛ در غیر این صورت b.__rtruediv__(a)
a // b a.__floordiv__(b)؛ در غیر این صورت b.__rfloordiv__(a)
a % b a.__mod__(b)؛ در غیر این صورت b.__rmod__(a)
a ** b a.__pow__(b)؛ در غیر این صورت b.__rpow__(a)
a << b a.__lshift__(b)؛ در غیر این صورت b.__rlshift__(a)
a >> b a.__rshift__(b)؛ در غیر این صورت b.__rrshift__(a)
a & b a.__and__(b)؛ در غیر این صورت b.__rand__(a)
a ^ b a.__xor__(b)؛ در غیر این صورت b.__rxor__(a)
a | b a.__or__(b)؛ در غیر این صورت b.__ror__(a)
a += b a.__iadd__(b)
a -= b a.__isub__(b)
a *= b a.__imul__(b)
… …
+a a.__pos__()
-a a.__neg__()
~a a.__invert__()
abs(a) a.__abs__()
a < b a.__lt__(b)
a <= b a.__le__(b)
a > b a.__gt__(b)
a >= b a.__ge__(b)
a == b a.__eq__(b)
a != b a.__ne__(b)
v in a a.__contains__(v)
a[k] a.__getitem__(k)
a[k] = v a.__setitem__(k, v)
del a[k] a.__delitem__(k)
a(arg1, arg2, ...) a.__call__(arg1, arg2, ...)
len(a) a.__len__()
hash(a) a.__hash__()
iter(a) a.__iter__()
next(a) a.__next__()
bool(a) a.__bool__()
float(a) a.__float__()
int(a) a.__int__()
repr(a) a.__repr__()
reversed(a) a.__reversed__()
str(a) a.__str__()

جدول ۲.۱: عملیات سربارگذاری‌شده که با متدهای ویژه پایتون پیاده‌سازی می‌شوند.

چند تابع سطح‌بالای دیگر نیز به فراخوانی متدهایی با نام ویژه متکی‌اند. برای مثال، روش استاندارد تعیین اندازه یک نوع ظرف (container)، فراخوانی تابع سطح‌بالای len است. توجه داشته باشید که نحو فراخوانی len(foo) همان نحو سنتی فراخوانی متد با عملگر نقطه نیست. با این حال، در مورد کلاس‌های تعریف‌شده توسط کاربر، تابع سطح‌بالای len متدی با نام ویژه __len__ از آن کلاس را فراخوانی می‌کند. یعنی فراخوانی len(foo) از طریق فراخوانی متد foo.__len__() ارزیابی می‌شود. هنگام توسعه ساختارهای داده، به‌طور معمول متد __len__ را برای بازگرداندن معیاری از اندازه ساختار تعریف خواهیم کرد.

متدهای ضمنی

به‌عنوان یک قاعده کلی، اگر متد ویژه خاصی در کلاس تعریف‌شده توسط کاربر پیاده‌سازی نشده باشد، نحو استانداردی که به آن متد متکی است استثنا ایجاد می‌کند. برای مثال، ارزیابی عبارت a + b برای نمونه‌های کلاسی که نه __add__ دارد و نه __radd__، خطا ایجاد می‌کند.

با این حال، برخی عملگرها در نبود متدهای ویژه، تعریف پیش‌فرضی دارند که پایتون فراهم می‌کند، و تعریف برخی عملگرها نیز از عملگرهای دیگر به دست می‌آید. برای مثال، متد __bool__ که از نحو if foo: پشتیبانی می‌کند، معنای پیش‌فرضی دارد که بر اساس آن هر شیئی به جز None به True ارزیابی می‌شود. اما برای انواع ظرف، متد __len__ معمولاً برای بازگرداندن اندازه ظرف تعریف می‌شود. اگر چنین متدی وجود داشته باشد، ارزیابی bool(foo) به‌طور پیش‌فرض برای نمونه‌های با طول غیرصفر True و برای نمونه‌های با طول صفر False تفسیر می‌شود؛ بنابراین می‌توان از نحوی مانند if waitlist: برای بررسی وجود یک یا چند مورد در لیست انتظار استفاده کرد.

در بخش ۲.۳.۴ سازوکار پایتون برای فراهم کردن تکرارگر برای مجموعه‌ها از طریق متد ویژه __iter__ را بررسی خواهیم کرد. با این حال، اگر یک کلاس ظرف پیاده‌سازی هر دو متد __len__ و __getitem__ را فراهم کند، پیمایش پیش‌فرض به‌طور خودکار در دسترس قرار می‌گیرد (با روشی که در بخش ۲.۳.۴ شرح می‌دهیم). علاوه بر این، پس از تعریف تکرارگر، کارکرد پیش‌فرض __contains__ نیز فراهم می‌شود.

در مباحث مقدماتی پایتون به تفاوت میان عبارت a is b و عبارت a == b اشاره شد: اولی بررسی می‌کند که آیا شناسه‌های a و b نام‌های مستعار یک شیء واحدند، و دومی بررسی می‌کند که آیا این دو شناسه به مقادیری هم‌ارز اشاره دارند. مفهوم «هم‌ارزی» به زمینه کلاس بستگی دارد و معنای آن با متد __eq__ تعریف می‌شود. با این حال، اگر هیچ پیاده‌سازی‌ای برای __eq__ ارائه نشود، نحو a == b معتبر است و همان معنای a is b را دارد؛ یعنی هر نمونه فقط با خودش هم‌ارز است و با هیچ نمونه دیگری هم‌ارز نیست.

باید هشدار داد که برخی پیامدهای طبیعی به‌طور خودکار توسط پایتون فراهم نمی‌شوند. برای مثال، متد __eq__ از نحو a == b پشتیبانی می‌کند، اما فراهم کردن آن بر ارزیابی نحو a != b اثری ندارد. (باید متد __ne__ را نیز فراهم کرد که معمولاً not (a == b) را به‌عنوان نتیجه برمی‌گرداند.) به همین ترتیب، فراهم کردن متد __lt__ از نحو a < b و به‌طور غیرمستقیم از b > a پشتیبانی می‌کند، اما فراهم کردن هر دو متد __lt__ و __eq__ به معنای تعریف a <= b نیست.

یادداشت مترجم: در پایتون ۳، اگر کلاسی __ne__ را تعریف نکند، پایتون به‌طور پیش‌فرض نتیجه __eq__ را وارونه می‌کند؛ بنابراین نکته بالا درباره != بیشتر به پایتون ۲ مربوط است. با این حال، تعریف صریح __ne__ (مانند کلاس Vector در ادامه) اشکالی ندارد.

۲.۳.۳ مثال: کلاس بردار چندبعدی

برای نشان دادن کاربرد سربارگذاری عملگرها از طریق متدهای ویژه، پیاده‌سازی کلاس Vector را ارائه می‌کنیم که مختصات یک بردار در فضایی چندبعدی را نمایش می‌دهد. برای مثال، در فضای سه‌بعدی ممکن است بخواهیم برداری با مختصات ⟨5, -2, 3⟩ را نمایش دهیم. هرچند ممکن است وسوسه شویم مستقیماً از لیست پایتون برای نمایش این مختصات استفاده کنیم، لیست انتزاع مناسبی برای بردار هندسی نیست. به‌طور مشخص، اگر از لیست استفاده کنیم، عبارت [5, -2, 3] + [1, 4, 2] لیست [5, -2, 3, 1, 4, 2] را نتیجه می‌دهد. در کار با بردارها، اگر u = ⟨5, -2, 3⟩ و v = ⟨1, 4, 2⟩ باشند، انتظار داریم عبارت u + v برداری سه‌بعدی با مختصات ⟨6, 2, 5⟩ برگرداند.

بنابراین در قطعه‌کد ۲.۴ کلاس Vector را تعریف می‌کنیم که انتزاع بهتری از مفهوم بردار هندسی فراهم می‌کند. بردار ما در درون، برای ذخیره‌سازی از نمونه‌ای از لیست به نام _coords استفاده می‌کند. با کپسوله نگه داشتن این لیست درونی، می‌توانیم رابط عمومی مورد نظر را برای نمونه‌های کلاس خود اعمال کنیم. نمونه‌ای از رفتارهای پشتیبانی‌شده در ادامه آمده است:

v = Vector(5)       # construct five-dimensional <0, 0, 0, 0, 0>
v[1] = 23           # <0, 23, 0, 0, 0> (based on use of __setitem__)
v[-1] = 45          # <0, 23, 0, 0, 45> (also via __setitem__)
print(v[4])         # print 45 (via __getitem__)
u = v + v           # <0, 46, 0, 0, 90> (via __add__)
print(u)            # print <0, 46, 0, 0, 90>
total = 0
for entry in v:     # implicit iteration via __len__ and __getitem__
    total += entry

بسیاری از رفتارها را به‌سادگی با فراخوانی رفتار مشابه روی لیست زیربنایی مختصات پیاده‌سازی می‌کنیم. با این حال، پیاده‌سازی __add__ سفارشی است. با فرض اینکه دو عملوند بردارهایی هم‌طول باشند، این متد بردار جدیدی می‌سازد و هر مختصه آن را برابر با مجموع عناصر متناظر دو عملوند قرار می‌دهد.

جالب است بدانید که تعریف کلاس، آن‌گونه که در قطعه‌کد ۲.۴ آمده، به‌طور خودکار از نحو u = v + [5, 3, 10, -2, 1] نیز پشتیبانی می‌کند و برداری جدید حاصل می‌شود که «جمع» عنصربه‌عنصر بردار اول و نمونه لیست است. این پیامد چندریختی (polymorphism) در پایتون است. «چندریختی» در لغت به معنای «چندشکلی» است. هرچند وسوسه‌انگیز است که پارامتر other در متد __add__ را نمونه‌ای دیگر از Vector بدانیم، هرگز آن را چنین اعلان نکرده‌ایم. در بدنه متد، تنها رفتارهایی که از پارامتر other انتظار داریم این است که از len(other) و دسترسی به other[j] پشتیبانی کند. بنابراین کد ما وقتی عملوند سمت راست لیستی از اعداد (با طول برابر) باشد نیز اجرا می‌شود.

class Vector:
    """Represent a vector in a multidimensional space."""

    def __init__(self, d):
        """Create d-dimensional vector of zeros."""
        self._coords = [0] * d

    def __len__(self):
        """Return the dimension of the vector."""
        return len(self._coords)

    def __getitem__(self, j):
        """Return jth coordinate of vector."""
        return self._coords[j]

    def __setitem__(self, j, val):
        """Set jth coordinate of vector to given value."""
        self._coords[j] = val

    def __add__(self, other):
        """Return sum of two vectors."""
        if len(self) != len(other):          # relies on __len__ method
            raise ValueError('dimensions must agree')
        result = Vector(len(self))           # start with vector of zeros
        for j in range(len(self)):
            result[j] = self[j] + other[j]
        return result

    def __eq__(self, other):
        """Return True if vector has same coordinates as other."""
        return self._coords == other._coords

    def __ne__(self, other):
        """Return True if vector differs from other."""
        return not self == other             # rely on existing __eq__ definition

    def __str__(self):
        """Produce string representation of vector."""
        return '<' + str(self._coords)[1:-1] + '>'  # adapt list representation

قطعه‌کد ۲.۴: تعریف یک کلاس Vector ساده.

۲.۳.۴ تکرارگرها

تکرار (iteration) مفهومی مهم در طراحی ساختارهای داده است. سازوکار تکرار در پایتون در مباحث مقدماتی معرفی شده است. به‌طور خلاصه، تکرارگر (iterator) یک مجموعه یک رفتار کلیدی دارد: از متد ویژه‌ای به نام __next__ پشتیبانی می‌کند که عنصر بعدی مجموعه را، در صورت وجود، برمی‌گرداند یا با ایجاد استثنای StopIteration نشان می‌دهد که عنصر دیگری باقی نمانده است.

خوشبختانه به‌ندرت لازم می‌شود کلاس تکرارگر را مستقیماً پیاده‌سازی کنیم. رویکرد ترجیحی ما استفاده از نحو مولد (generator) است که به‌طور خودکار تکرارگری از مقادیر تولیدشده با yield می‌سازد.

پایتون همچنین برای هر کلاسی که هر دو متد __len__ و __getitem__ را تعریف کند، به‌طور خودکار پیاده‌سازی‌ای از تکرارگر فراهم می‌کند. برای ارائه مثالی آموزنده از یک تکرارگر سطح پایین، قطعه‌کد ۲.۵ دقیقاً چنین کلاس تکرارگری را نشان می‌دهد که روی هر مجموعه‌ای که از __len__ و __getitem__ پشتیبانی کند کار می‌کند. این کلاس را می‌توان به شکل SequenceIterator(data) نمونه‌سازی کرد. این کلاس با نگه داشتن ارجاعی درونی به دنباله داده‌ها و همچنین یک اندیس فعلی در دنباله کار می‌کند. هر بار که __next__ فراخوانی می‌شود، اندیس یک واحد افزایش می‌یابد تا به انتهای دنباله برسد.

class SequenceIterator:
    """An iterator for any of Python's sequence types."""

    def __init__(self, sequence):
        """Create an iterator for the given sequence."""
        self._seq = sequence      # keep a reference to the underlying data
        self._k = -1              # will increment to 0 on first call to next

    def __next__(self):
        """Return the next element, or else raise StopIteration error."""
        self._k += 1              # advance to next index
        if self._k < len(self._seq):
            return(self._seq[self._k])  # return the data element
        else:
            raise StopIteration()       # there are no more elements

    def __iter__(self):
        """By convention, an iterator must return itself as an iterator."""
        return self

قطعه‌کد ۲.۵: کلاس تکرارگری برای هر نوع دنباله.

۲.۳.۵ مثال: کلاس Range

در آخرین مثال این بخش، پیاده‌سازی خودمان را از کلاسی توسعه می‌دهیم که کلاس داخلی range پایتون را تقلید می‌کند. پیش از معرفی کلاس خود، به تاریخچه نسخه داخلی می‌پردازیم. پیش از انتشار پایتون ۳، range به‌صورت تابع پیاده‌سازی شده بود و نمونه‌ای از لیست را با عناصر بازه مشخص‌شده برمی‌گرداند. برای مثال، range(2, 10, 2) لیست [2, 4, 6, 8] را برمی‌گرداند. با این حال، یکی از کاربردهای رایج این تابع، پشتیبانی از نحو حلقه for بود، مانند for k in range(10000000). متأسفانه این کار موجب ساخت و مقداردهی اولیه لیستی شامل همه اعداد آن بازه می‌شد؛ گامی که هم از نظر زمان و هم از نظر مصرف حافظه بی‌جهت پرهزینه بود.

سازوکار پشتیبانی از بازه‌ها در پایتون ۳ کاملاً متفاوت است (البته رفتار «جدید» در پایتون ۲ نیز با نام xrange وجود داشت). این سازوکار از راهبردی به نام ارزیابی تنبل (lazy evaluation) استفاده می‌کند. به جای ساختن نمونه‌ای جدید از لیست، range کلاسی است که می‌تواند بازه مورد نظر از عناصر را بدون ذخیره صریح آن‌ها در حافظه نمایش دهد. برای آشنایی بیشتر با کلاس داخلی range، توصیه می‌کنیم نمونه‌ای به شکل r = range(8, 140, 5) بسازید. نتیجه شیئی نسبتاً سبک، یعنی نمونه‌ای از کلاس range، است که فقط چند رفتار دارد. نحو len(r) تعداد عناصر موجود در بازه را گزارش می‌کند (در مثال ما ۲۷). شیء range از متد __getitem__ نیز پشتیبانی می‌کند، به‌طوری که نحو r[15] شانزدهمین عنصر بازه را گزارش می‌دهد (زیرا r[0] نخستین عنصر است). از آنجا که این کلاس از هر دو متد __len__ و __getitem__ پشتیبانی می‌کند، پشتیبانی خودکار از پیمایش را به ارث می‌برد (بخش ۲.۳.۴)؛ به همین دلیل می‌توان روی یک range حلقه for اجرا کرد.

اکنون آماده‌ایم نسخه خودمان از چنین کلاسی را ارائه کنیم. قطعه‌کد ۲.۶ کلاسی به نام Range ارائه می‌دهد (تا به‌روشنی از range داخلی متمایز باشد). بزرگ‌ترین چالش در این پیاده‌سازی، محاسبه درست تعداد عناصر متعلق به بازه با توجه به پارامترهایی است که فراخواننده هنگام ساخت بازه می‌فرستد. با محاسبه این مقدار در سازنده و ذخیره آن در self._length، بازگرداندن آن از متد __len__ کاری ساده می‌شود. برای پیاده‌سازی درست فراخوانی __getitem__(k)، کافی است مقدار شروع بازه را با k برابر اندازه گام جمع کنیم (یعنی برای k = 0، مقدار شروع را برمی‌گردانیم). چند نکته ظریف در این کد ارزش بررسی دارد:

  • برای پشتیبانی درست از پارامترهای اختیاری، از همان فنی استفاده می‌کنیم که هنگام بررسی نسخه تابعی range در مباحث مقدماتی پایتون شرح داده شد.
  • تعداد عناصر بازه را به شکل max(0, (stop - start + step - 1) // step) محاسبه می‌کنیم. بهتر است این فرمول را برای گام‌های مثبت و منفی آزمایش کنید.
  • متد __getitem__ با تبدیل اندیس -k به len(self) - k پیش از محاسبه نتیجه، به‌درستی از اندیس‌های منفی پشتیبانی می‌کند.
class Range:
    """A class that mimics the built-in range class."""

    def __init__(self, start, stop=None, step=1):
        """Initialize a Range instance.

        Semantics is similar to built-in range class.
        """
        if step == 0:
            raise ValueError('step cannot be 0')

        if stop is None:                  # special case of range(n)
            start, stop = 0, start        # should be treated as if range(0,n)

        # calculate the effective length once
        self._length = max(0, (stop - start + step - 1) // step)

        # need knowledge of start and step (but not stop) to support __getitem__
        self._start = start
        self._step = step

    def __len__(self):
        """Return number of entries in the range."""
        return self._length

    def __getitem__(self, k):
        """Return entry at index k (using standard interpretation if negative)."""
        if k < 0:
            k += len(self)                # attempt to convert negative index

        if not 0 <= k < self._length:
            raise IndexError('index out of range')

        return self._start + k * self._step

قطعه‌کد ۲.۶: پیاده‌سازی خودمان از کلاس Range.

۲.۴ وراثت

یک روش طبیعی برای سازمان‌دهی اجزای ساختاری گوناگون یک بسته نرم‌افزاری، سازمان‌دهی سلسله‌مراتبی است؛ به این صورت که تعاریف انتزاعی مشابه، سطح‌به‌سطح در کنار هم قرار می‌گیرند و با بالا رفتن در سلسله‌مراتب، از خاص به عام‌تر پیش می‌رویم. نمونه‌ای از چنین سلسله‌مراتبی در شکل ۲.۴ نشان داده شده است. با نمادگذاری ریاضی، مجموعه خانه‌ها زیرمجموعه مجموعه ساختمان‌هاست، اما ابرمجموعه مجموعه خانه‌های ویلایی یک‌طبقه است. ارتباط میان سطوح اغلب رابطه «هست یک» (is a) نامیده می‌شود، زیرا خانه یک ساختمان است و خانه ویلایی یک خانه است.

  • ساختمان (Building)
    • آپارتمان (Apartment)
      • آپارتمان کم‌ارتفاع (Low-rise Apartment)
      • آپارتمان بلندمرتبه (High-rise Apartment)
    • خانه (House)
      • خانه دوطبقه (Two-story House)
      • خانه ویلایی یک‌طبقه (Ranch)
    • ساختمان تجاری (Commercial Building)
      • آسمان‌خراش (Skyscraper)

شکل ۲.۴: نمونه‌ای از سلسله‌مراتب «هست یک» در مورد ساختمان‌ها.

طراحی سلسله‌مراتبی در توسعه نرم‌افزار سودمند است، زیرا کارکردهای مشترک را می‌توان در عام‌ترین سطح گرد آورد و در نتیجه استفاده مجدد از کد را ترویج داد، در حالی که رفتارهای متمایز را می‌توان گسترش‌هایی از حالت عام در نظر گرفت. در برنامه‌نویسی شی‌گرا، سازوکار سازمان‌دهی ماژولار و سلسله‌مراتبی، فنی به نام وراثت (inheritance) است. وراثت امکان می‌دهد کلاسی جدید بر پایه یک کلاس موجود به‌عنوان نقطه شروع تعریف شود. در اصطلاحات شی‌گرا، کلاس موجود معمولاً کلاس پایه (base class)، کلاس والد (parent class) یا ابرکلاس (superclass) نامیده می‌شود و کلاس تازه‌تعریف‌شده زیرکلاس (subclass) یا کلاس فرزند (child class).

زیرکلاس به دو شیوه می‌تواند خود را از ابرکلاسش متمایز کند. زیرکلاس می‌تواند با ارائه پیاده‌سازی جدیدی که یک متد موجود را بازنویسی (override) می‌کند، یک رفتار موجود را تخصصی (specialize) کند. همچنین می‌تواند با افزودن متدهای کاملاً جدید، ابرکلاس خود را گسترش (extend) دهد.

سلسله‌مراتب استثناهای پایتون

نمونه دیگری از یک سلسله‌مراتب وراثت غنی، سازمان‌دهی انواع استثناها در پایتون است. بسیاری از این کلاس‌ها را در مباحث مقدماتی پایتون دیده‌ایم، اما به رابطه آن‌ها با یکدیگر نپرداخته‌ایم. شکل ۲.۵ بخش (کوچکی) از این سلسله‌مراتب را نشان می‌دهد. کلاس BaseException ریشه کل سلسله‌مراتب است و کلاس خاص‌تر Exception بیشتر انواع خطاهایی را که تاکنون دیده‌ایم در بر می‌گیرد. برنامه‌نویسان می‌توانند کلاس‌های استثنای ویژه خود را برای نمایش خطاهایی که ممکن است در زمینه کاربردشان رخ دهد تعریف کنند. این انواع استثنای تعریف‌شده توسط کاربر باید زیرکلاسی از Exception اعلان شوند.

  • BaseException
    • SystemExit
    • Exception
      • ValueError
      • LookupError
        • IndexError
        • KeyError
      • ArithmeticError
        • ZeroDivisionError
    • KeyboardInterrupt

شکل ۲.۵: بخشی از سلسله‌مراتب انواع استثنا در پایتون.

۲.۴.۱ گسترش کلاس CreditCard

برای نشان دادن سازوکارهای وراثت در پایتون، کلاس CreditCard از بخش ۲.۳ را دوباره بررسی می‌کنیم و زیرکلاسی از آن پیاده‌سازی می‌کنیم که در نبود نامی بهتر، آن را PredatoryCreditCard (کارت اعتباری غارتگر) می‌نامیم. کلاس جدید از دو جهت با کلاس اصلی تفاوت دارد: (۱) اگر ثبت هزینه‌ای به این دلیل رد شود که از سقف اعتبار فراتر می‌رفت، ۵ دلار جریمه دریافت می‌شود؛ و (۲) سازوکاری برای محاسبه بهره ماهانه بر موجودی پرداخت‌نشده، بر اساس نرخ درصد سالانه (APR) که به‌عنوان پارامتر سازنده مشخص می‌شود، وجود خواهد داشت.

برای رسیدن به این هدف، فنون تخصصی‌سازی و گسترش را نشان می‌دهیم. برای دریافت جریمه در ازای تلاش نامعتبر برای ثبت هزینه، متد موجود charge را بازنویسی می‌کنیم و بدین ترتیب آن را برای ارائه کارکرد جدید تخصصی می‌کنیم (هرچند نسخه جدید از فراخوانی نسخه بازنویسی‌شده بهره می‌گیرد). برای پشتیبانی از دریافت بهره، کلاس را با متد جدیدی به نام process_month گسترش می‌دهیم.

کلاس فیلدها رفتارها
CreditCard (ابرکلاس) _customer، _bank، _account، _balance، _limit get_customer()، get_bank()، get_account()، make_payment(amount)، get_balance()، get_limit()، charge(price)
PredatoryCreditCard (زیرکلاس؛ از CreditCard ارث می‌برد) _apr process_month()، charge(price)

شکل ۲.۶: نمودار یک رابطه وراثت.

شکل ۲.۶ نمایی کلی از کاربرد وراثت در طراحی کلاس جدید PredatoryCreditCard ارائه می‌دهد و قطعه‌کد ۲.۷ پیاده‌سازی کامل این کلاس را در پایتون نشان می‌دهد.

برای نشان دادن اینکه کلاس جدید از کلاس موجود CreditCard ارث می‌برد، تعریف ما با نحو class PredatoryCreditCard(CreditCard) آغاز می‌شود. بدنه کلاس جدید سه تابع عضو دارد: __init__، charge و process_month. سازنده __init__ نقشی بسیار مشابه سازنده اصلی CreditCard دارد، با این تفاوت که در کلاس جدید پارامتری اضافی برای تعیین نرخ درصد سالانه وجود دارد. بدنه سازنده جدید برای انجام بیشتر مقداردهی‌های اولیه (در واقع همه کارها به جز ثبت نرخ درصد) به فراخوانی سازنده به‌ارث‌رسیده متکی است. سازوکار فراخوانی سازنده به‌ارث‌رسیده بر نحو super() استوار است. به‌طور مشخص، دستور زیر در بدنه سازنده:

super().__init__(customer, bank, acnt, limit)

متد __init__ به‌ارث‌رسیده از ابرکلاس CreditCard را فراخوانی می‌کند. توجه داشته باشید که این متد فقط چهار پارامتر می‌پذیرد. مقدار APR را در فیلد جدیدی به نام _apr ثبت می‌کنیم.

به شکل مشابه، کلاس PredatoryCreditCard پیاده‌سازی جدیدی از متد charge ارائه می‌دهد که متد به‌ارث‌رسیده را بازنویسی می‌کند. با این حال، پیاده‌سازی متد جدید با نحو super().charge(price) به فراخوانی متد به‌ارث‌رسیده متکی است. مقدار بازگشتی این فراخوانی نشان می‌دهد که آیا ثبت هزینه موفق بوده است یا نه. این مقدار را بررسی می‌کنیم تا تصمیم بگیریم جریمه‌ای اعمال شود یا نه، و سپس همان مقدار را به فراخواننده برمی‌گردانیم تا نسخه جدید charge رابط بیرونی مشابهی با نسخه اصلی داشته باشد.

class PredatoryCreditCard(CreditCard):
    """An extension to CreditCard that compounds interest and fees."""

    def __init__(self, customer, bank, acnt, limit, apr):
        """Create a new predatory credit card instance.

        The initial balance is zero.

        customer  the name of the customer (e.g., 'John Bowman')
        bank      the name of the bank (e.g., 'California Savings')
        acnt      the account identifier (e.g., '5391 0375 9387 5309')
        limit     credit limit (measured in dollars)
        apr       annual percentage rate (e.g., 0.0825 for 8.25% APR)
        """
        super().__init__(customer, bank, acnt, limit)  # call super constructor
        self._apr = apr

    def charge(self, price):
        """Charge given price to the card, assuming sufficient credit limit.

        Return True if charge was processed.
        Return False and assess $5 fee if charge is denied.
        """
        success = super().charge(price)        # call inherited method
        if not success:
            self._balance += 5                 # assess penalty
        return success                         # caller expects return value

    def process_month(self):
        """Assess monthly interest on outstanding balance."""
        if self._balance > 0:
            # if positive balance, convert APR to monthly multiplicative factor
            monthly_factor = pow(1 + self._apr, 1/12)
            self._balance *= monthly_factor

قطعه‌کد ۲.۷: زیرکلاسی از CreditCard که بهره و جریمه دریافت می‌کند.

متد process_month رفتاری جدید است، بنابراین نسخه به‌ارث‌رسیده‌ای برای تکیه بر آن وجود ندارد. در مدل ما، این متد باید ماهی یک بار از سوی بانک فراخوانی شود تا بهره جدید به موجودی مشتری افزوده شود. چالش‌برانگیزترین جنبه پیاده‌سازی این متد، اطمینان از درک درست چگونگی تبدیل نرخ درصد سالانه به نرخ ماهانه است. نرخ سالانه را صرفاً بر دوازده تقسیم نمی‌کنیم تا نرخ ماهانه به دست آید (چنین کاری بیش از حد غارتگرانه خواهد بود، زیرا به APR بالاتری از نرخ اعلام‌شده می‌انجامد). محاسبه درست آن است که ریشه دوازدهم 1 + self._apr را بگیریم و از آن به‌عنوان ضریب ضرب استفاده کنیم. برای مثال، اگر APR برابر ۰٫۰۸۲۵ باشد (یعنی ۸٫۲۵ درصد)، مقدار ریشهٔ دوازدهم ۱٫۰۸۲۵ ≈ 1.006628 را محاسبه می‌کنیم و در نتیجه ماهانه ۰٫۶۶۲۸ درصد بهره دریافت می‌کنیم. به این ترتیب، هر ۱۰۰ دلار بدهی در یک سال ۸٫۲۵ دلار بهره مرکب به بار می‌آورد.

اعضای محافظت‌شده

زیرکلاس PredatoryCreditCard مستقیماً به عضو داده self._balance دسترسی دارد که در کلاس والد، CreditCard، ایجاد شده است. نام زیرخط‌دار طبق قرارداد نشان می‌دهد که این عضو غیرعمومی است، پس ممکن است بپرسیم آیا دسترسی به آن به این شکل درست است. هرچند کاربران عادی کلاس نباید چنین کاری بکنند، زیرکلاس ما رابطه‌ای تا حدی ممتاز با ابرکلاس دارد. برخی زبان‌های شی‌گرا (مانند Java و C++) میان اعضای غیرعمومی تمایز قائل می‌شوند و امکان اعلان سطح دسترسی محافظت‌شده (protected) یا خصوصی (private) را فراهم می‌کنند. اعضایی که محافظت‌شده اعلان می‌شوند برای زیرکلاس‌ها در دسترس‌اند اما برای عموم نه، در حالی که اعضای خصوصی برای هیچ‌یک در دسترس نیستند. از این نظر، ما از _balance به‌گونه‌ای استفاده می‌کنیم که گویی محافظت‌شده است (اما نه خصوصی).

پایتون از کنترل دسترسی رسمی پشتیبانی نمی‌کند، اما طبق قرارداد، نام‌هایی که با یک زیرخط آغاز می‌شوند هم‌ارز محافظت‌شده و نام‌هایی که با دو زیرخط آغاز می‌شوند (به جز متدهای ویژه) هم‌ارز خصوصی هستند. با انتخاب استفاده از داده محافظت‌شده، وابستگی‌ای ایجاد کرده‌ایم که اگر نویسنده کلاس CreditCard طراحی درونی آن را تغییر دهد، ممکن است درستی کلاس PredatoryCreditCard را به خطر بیندازد. توجه کنید که می‌توانستیم در متد process_month برای دریافت موجودی فعلی به متد عمومی get_balance() تکیه کنیم. اما طراحی فعلی کلاس CreditCard راه مؤثری برای تغییر موجودی در اختیار زیرکلاس قرار نمی‌دهد، جز دست‌کاری مستقیم عضو داده. ممکن است وسوسه شویم برای افزودن جریمه یا بهره به موجودی از charge استفاده کنیم. اما این متد اجازه نمی‌دهد موجودی از سقف اعتبار مشتری فراتر رود، هرچند بانک احتمالاً اجازه می‌دهد بهره در صورت لزوم از سقف اعتبار فراتر رود. اگر قرار بود کلاس اصلی CreditCard را بازطراحی کنیم، می‌توانستیم متد غیرعمومی _set_balance را به آن بیفزاییم تا زیرکلاس‌ها بتوانند بدون دسترسی مستقیم به عضو داده _balance موجودی را تغییر دهند.

۲.۴.۲ سلسله‌مراتب تصاعدهای عددی

در دومین مثال از کاربرد وراثت، سلسله‌مراتبی از کلاس‌ها برای پیمایش تصاعدهای عددی (numeric progressions) توسعه می‌دهیم. تصاعد عددی دنباله‌ای از اعداد است که در آن هر عدد به یک یا چند عدد پیشین وابسته است. برای مثال، تصاعد حسابی (arithmetic progression) عدد بعدی را با افزودن مقداری ثابت به مقدار قبلی تعیین می‌کند و تصاعد هندسی (geometric progression) عدد بعدی را با ضرب مقدار قبلی در مقداری ثابت. به‌طور کلی، هر تصاعد به یک مقدار نخست و قاعده‌ای برای تعیین مقدار جدید بر اساس یک یا چند مقدار پیشین نیاز دارد.

برای بیشینه کردن قابلیت استفاده مجدد کد، سلسله‌مراتبی از کلاس‌ها را توسعه می‌دهیم که از یک کلاس پایه عام به نام Progression منشعب می‌شوند (شکل ۲.۷). از نظر فنی، کلاس Progression تصاعد اعداد صحیح نامنفی، یعنی ۰، ۱، ۲، …، را تولید می‌کند. با این حال، این کلاس طوری طراحی شده است که کلاس پایه سایر انواع تصاعد باشد و تا حد ممکن کارکردهای مشترک را فراهم کند و بدین ترتیب بار زیرکلاس‌ها را به حداقل برساند.

  • Progression
    • ArithmeticProgression
    • GeometricProgression
    • FibonacciProgression

شکل ۲.۷: سلسله‌مراتب کلاس‌های تصاعد ما.

پیاده‌سازی کلاس پایه Progression در قطعه‌کد ۲.۸ آمده است. سازنده این کلاس یک مقدار شروع برای تصاعد (به‌طور پیش‌فرض ۰) می‌پذیرد و عضو داده self._current را با آن مقداردهی اولیه می‌کند.

کلاس Progression قراردادهای یک تکرارگر پایتون (بخش ۲.۳.۴)، یعنی متدهای ویژه __next__ و __iter__، را پیاده‌سازی می‌کند. اگر کاربری تصاعدی به شکل seq = Progression() بسازد، هر فراخوانی next(seq) عنصر بعدی دنباله تصاعد را برمی‌گرداند. همچنین می‌توان از نحو حلقه for به شکل for value in seq: استفاده کرد، هرچند توجه داریم که تصاعد پیش‌فرض ما دنباله‌ای نامتناهی تعریف شده است.

برای جداسازی بهتر سازوکار قرارداد تکرارگر از منطق اصلی پیشروی تصاعد، چارچوب ما به متدی غیرعمومی به نام _advance متکی است که مقدار فیلد self._current را به‌روز می‌کند. در پیاده‌سازی پیش‌فرض، _advance یک واحد به مقدار فعلی می‌افزاید، اما قصد ما این است که زیرکلاس‌ها _advance را بازنویسی کنند تا قاعده متفاوتی برای محاسبه جمله بعدی ارائه دهند.

برای سهولت، کلاس Progression متدی کمکی به نام print_progression نیز فراهم می‌کند که n مقدار بعدی تصاعد را نمایش می‌دهد.

class Progression:
    """Iterator producing a generic progression.

    Default iterator produces the whole numbers 0, 1, 2, ...
    """

    def __init__(self, start=0):
        """Initialize current to the first value of the progression."""
        self._current = start

    def _advance(self):
        """Update self._current to a new value.

        This should be overridden by a subclass to customize progression.

        By convention, if current is set to None, this designates the
        end of a finite progression.
        """
        self._current += 1

    def __next__(self):
        """Return the next element, or else raise StopIteration error."""
        if self._current is None:      # our convention to end a progression
            raise StopIteration()
        else:
            answer = self._current     # record current value to return
            self._advance()            # advance to prepare for next time
            return answer              # return the answer

    def __iter__(self):
        """By convention, an iterator must return itself as an iterator."""
        return self

    def print_progression(self, n):
        """Print next n values of the progression."""
        print(' '.join(str(next(self)) for j in range(n)))

قطعه‌کد ۲.۸: کلاس عام تصاعد عددی.

کلاس تصاعد حسابی

نخستین مثال ما از یک تصاعد تخصصی، تصاعد حسابی است. در حالی که تصاعد پیش‌فرض در هر گام مقدار خود را یک واحد افزایش می‌دهد، تصاعد حسابی برای تولید جمله بعدی، مقداری ثابت را به جمله فعلی می‌افزاید. برای مثال، تصاعد حسابی‌ای که از ۰ شروع شود و گام افزایش آن ۴ باشد، دنباله ۰، ۴، ۸، ۱۲، … را تولید می‌کند.

قطعه‌کد ۲.۹ پیاده‌سازی کلاس ArithmeticProgression را نشان می‌دهد که از Progression به‌عنوان کلاس پایه استفاده می‌کند. سازنده این کلاس جدید هم مقدار افزایش و هم مقدار شروع را به‌عنوان پارامتر می‌پذیرد، هرچند برای هر دو مقدار پیش‌فرض در نظر گرفته شده است. طبق قرارداد ما، ArithmeticProgression(4) دنباله ۰، ۴، ۸، ۱۲، … و ArithmeticProgression(4, 1) دنباله ۱، ۵، ۹، ۱۳، … را تولید می‌کند.

بدنه سازنده ArithmeticProgression سازنده ابرکلاس را فراخوانی می‌کند تا عضو داده _current با مقدار شروع مورد نظر مقداردهی شود. سپس مستقیماً عضو داده جدید _increment را برای تصاعد حسابی ایجاد می‌کند. تنها جزئیات باقی‌مانده در پیاده‌سازی، بازنویسی متد _advance است تا مقدار افزایش را به مقدار فعلی بیفزاید.

class ArithmeticProgression(Progression):  # inherit from Progression
    """Iterator producing an arithmetic progression."""

    def __init__(self, increment=1, start=0):
        """Create a new arithmetic progression.

        increment  the fixed constant to add to each term (default 1)
        start      the first term of the progression (default 0)
        """
        super().__init__(start)            # initialize base class
        self._increment = increment

    def _advance(self):                    # override inherited version
        """Update current value by adding the fixed increment."""
        self._current += self._increment

قطعه‌کد ۲.۹: کلاسی که تصاعد حسابی تولید می‌کند.

کلاس تصاعد هندسی

دومین مثال ما از یک تصاعد تخصصی، تصاعد هندسی است که در آن هر مقدار از ضرب مقدار قبلی در مقداری ثابت، موسوم به قدر نسبت (که در کد base نامیده شده است)، به دست می‌آید. نقطه شروع تصاعد هندسی به‌طور سنتی ۱ است، نه ۰، زیرا ضرب ۰ در هر عددی ۰ می‌شود. برای مثال، تصاعد هندسی با قدر نسبت ۲ به شکل ۱، ۲، ۴، ۸، ۱۶، … پیش می‌رود.

قطعه‌کد ۲.۱۰ پیاده‌سازی کلاس GeometricProgression را نشان می‌دهد. سازنده به‌طور پیش‌فرض ۲ را به‌عنوان قدر نسبت و ۱ را به‌عنوان مقدار شروع در نظر می‌گیرد، اما هر دو را می‌توان با پارامترهای اختیاری تغییر داد.

class GeometricProgression(Progression):  # inherit from Progression
    """Iterator producing a geometric progression."""

    def __init__(self, base=2, start=1):
        """Create a new geometric progression.

        base       the fixed constant multiplied to each term (default 2)
        start      the first term of the progression (default 1)
        """
        super().__init__(start)
        self._base = base

    def _advance(self):                    # override inherited version
        """Update current value by multiplying it by the base value."""
        self._current *= self._base

قطعه‌کد ۲.۱۰: کلاسی که تصاعد هندسی تولید می‌کند.

کلاس تصاعد فیبوناچی

در آخرین مثال، نشان می‌دهیم چگونه از چارچوب تصاعد خود برای تولید تصاعد فیبوناچی (Fibonacci progression) استفاده کنیم. سری فیبوناچی را پیش‌تر در مبحث مولدها دیده‌ایم. هر جمله سری فیبوناچی مجموع دو جمله اخیر است. برای شروع سری، دو مقدار نخست به‌طور قراردادی ۰ و ۱ هستند که سری فیبوناچی ۰، ۱، ۱، ۲، ۳، ۵، ۸، … را نتیجه می‌دهند. به‌طور کلی‌تر، چنین سری‌ای را می‌توان از هر دو مقدار آغازینی تولید کرد. برای مثال، اگر با مقادیر ۴ و ۶ شروع کنیم، سری به شکل ۴، ۶، ۱۰، ۱۶، ۲۶، ۴۲، … پیش می‌رود.

class FibonacciProgression(Progression):
    """Iterator producing a generalized Fibonacci progression."""

    def __init__(self, first=0, second=1):
        """Create a new fibonacci progression.

        first   the first term of the progression (default 0)
        second  the second term of the progression (default 1)
        """
        super().__init__(first)            # start progression at first
        self._prev = second - first        # fictitious value preceding the first

    def _advance(self):
        """Update current value by taking sum of previous two."""
        self._prev, self._current = self._current, self._prev + self._current

قطعه‌کد ۲.۱۱: کلاسی که تصاعد فیبوناچی تولید می‌کند.

با استفاده از چارچوب تصاعد خود، کلاس جدید FibonacciProgression را تعریف می‌کنیم (قطعه‌کد ۲.۱۱). این کلاس تفاوت چشمگیری با کلاس‌های تصاعد حسابی و هندسی دارد، زیرا نمی‌توان مقدار بعدی سری فیبوناچی را فقط از روی مقدار فعلی تعیین کرد؛ باید دو مقدار اخیر را در اختیار داشت. کلاس پایه Progression از پیش، ذخیره آخرین مقدار را در عضو داده _current فراهم می‌کند. کلاس FibonacciProgression عضو جدیدی به نام _prev معرفی می‌کند تا مقداری را که پیش از مقدار فعلی آمده ذخیره کند.

با ذخیره هر دو مقدار قبلی، پیاده‌سازی _advance نسبتاً ساده است (از یک انتساب همزمان استفاده می‌کنیم). با این حال، این پرسش پیش می‌آید که مقدار قبلی را در سازنده چگونه مقداردهی اولیه کنیم. مقادیر نخست و دوم مورد نظر به‌عنوان پارامترهای سازنده داده می‌شوند. مقدار نخست باید در _current ذخیره شود تا نخستین مقدار گزارش‌شده باشد. با نگاه به گام بعد، پس از گزارش مقدار نخست، انتسابی انجام می‌دهیم که مقدار فعلی جدید (که دومین مقدار گزارش‌شده خواهد بود) را برابر مجموع مقدار نخست و مقدار «قبلی» قرار می‌دهد. با مقداردهی اولیه مقدار قبلی به (second - first)، نخستین پیشروی، مقدار فعلی جدید را برابر با first + (second - first) = second قرار می‌دهد که همان نتیجه مطلوب است.

تست تصاعدها

برای تکمیل بحث، قطعه‌کد ۲.۱۲ یک تست واحد برای همه کلاس‌های تصاعد ارائه می‌دهد و قطعه‌کد ۲.۱۳ خروجی آن را نشان می‌دهد.

if __name__ == '__main__':
    print('Default progression:')
    Progression().print_progression(10)

    print('Arithmetic progression with increment 5:')
    ArithmeticProgression(5).print_progression(10)

    print('Arithmetic progression with increment 5 and start 2:')
    ArithmeticProgression(5, 2).print_progression(10)

    print('Geometric progression with default base:')
    GeometricProgression().print_progression(10)

    print('Geometric progression with base 3:')
    GeometricProgression(3).print_progression(10)

    print('Fibonacci progression with default start values:')
    FibonacciProgression().print_progression(10)

    print('Fibonacci progression with start values 4 and 6:')
    FibonacciProgression(4, 6).print_progression(10)

قطعه‌کد ۲.۱۲: تست‌های واحد برای کلاس‌های تصاعد.

Default progression:
0 1 2 3 4 5 6 7 8 9
Arithmetic progression with increment 5:
0 5 10 15 20 25 30 35 40 45
Arithmetic progression with increment 5 and start 2:
2 7 12 17 22 27 32 37 42 47
Geometric progression with default base:
1 2 4 8 16 32 64 128 256 512
Geometric progression with base 3:
1 3 9 27 81 243 729 2187 6561 19683
Fibonacci progression with default start values:
0 1 1 2 3 5 8 13 21 34
Fibonacci progression with start values 4 and 6:
4 6 10 16 26 42 68 110 178 288

قطعه‌کد ۲.۱۳: خروجی تست‌های واحد قطعه‌کد ۲.۱۲.

۲.۴.۳ کلاس‌های پایه انتزاعی

هنگام تعریف گروهی از کلاس‌ها به‌عنوان بخشی از یک سلسله‌مراتب وراثت، یکی از فنون پرهیز از تکرار کد، طراحی کلاس پایه‌ای با کارکردهای مشترک است که کلاس‌های دیگر در صورت نیاز از آن ارث ببرند. برای نمونه، سلسله‌مراتب بخش ۲.۴.۲ شامل کلاس Progression است که کلاس پایه سه زیرکلاس متمایز است: ArithmeticProgression، GeometricProgression و FibonacciProgression. هرچند می‌توان نمونه‌ای از کلاس پایه Progression ساخت، این کار ارزش چندانی ندارد، زیرا رفتار آن صرفاً حالت خاصی از ArithmeticProgression با گام افزایش ۱ است. هدف واقعی کلاس Progression متمرکز کردن پیاده‌سازی رفتارهایی بود که سایر تصاعدها به آن نیاز داشتند و در نتیجه ساده‌تر کردن کدی که به عهده زیرکلاس‌ها گذاشته می‌شود.

در اصطلاحات کلاسیک شی‌گرا، می‌گوییم یک کلاس کلاس پایه انتزاعی است اگر تنها هدفش خدمت به‌عنوان کلاس پایه از طریق وراثت باشد. به بیان رسمی‌تر، کلاس پایه انتزاعی کلاسی است که نمی‌توان مستقیماً از آن نمونه ساخت، در حالی که کلاس عینی (concrete class) کلاسی است که می‌توان از آن نمونه ساخت. طبق این تعریف، کلاس Progression ما از نظر فنی عینی است، هرچند اساساً آن را به‌عنوان یک کلاس پایه انتزاعی طراحی کرده‌ایم.

در زبان‌هایی با تایپ ایستا مانند Java و C++، کلاس پایه انتزاعی نقش یک نوع رسمی را دارد که ممکن است وجود یک یا چند متد انتزاعی را تضمین کند. این ویژگی از چندریختی پشتیبانی می‌کند، زیرا نوع اعلان‌شده یک متغیر می‌تواند یک کلاس پایه انتزاعی باشد، در حالی که آن متغیر به نمونه‌ای از یک زیرکلاس عینی اشاره کند. از آنجا که در پایتون نوع اعلان‌شده وجود ندارد، این نوع چندریختی بدون نیاز به یک کلاس پایه انتزاعی یکپارچه‌کننده قابل دستیابی است. به همین دلیل، سنت تعریف کلاس‌های پایه انتزاعی در پایتون چندان قوی نیست، هرچند ماژول abc پایتون از تعریف رسمی کلاس پایه انتزاعی پشتیبانی می‌کند.

دلیل تمرکز ما بر کلاس‌های پایه انتزاعی در مطالعه ساختارهای داده این است که ماژول collections پایتون چند کلاس پایه انتزاعی فراهم می‌کند که هنگام تعریف ساختارهای داده سفارشی‌ای که رابطی مشترک با برخی ساختارهای داده داخلی پایتون دارند، کمک می‌کنند. این کلاس‌ها بر یک الگوی طراحی نرم‌افزار شی‌گرا به نام الگوی متد قالب (template method pattern) استوارند. در الگوی متد قالب، یک کلاس پایه انتزاعی رفتارهای عینی‌ای فراهم می‌کند که به فراخوانی رفتارهای انتزاعی دیگر متکی‌اند. به این ترتیب، به محض اینکه زیرکلاسی تعریف رفتارهای انتزاعیِ غایب را فراهم کند، رفتارهای عینی به‌ارث‌رسیده به‌خوبی تعریف‌شده خواهند بود.

به‌عنوان مثالی ملموس، کلاس پایه انتزاعی collections.Sequence رفتارهای مشترک کلاس‌های list، str و tuple پایتون را، به‌عنوان دنباله‌هایی که از دسترسی به عناصر از طریق اندیس صحیح پشتیبانی می‌کنند، تعریف می‌کند. افزون بر این، کلاس collections.Sequence پیاده‌سازی‌های عینی متدهای count، index و __contains__ را فراهم می‌کند که هر کلاسی که پیاده‌سازی عینی هر دو متد __len__ و __getitem__ را داشته باشد می‌تواند آن‌ها را به ارث ببرد. برای روشن شدن موضوع، نمونه‌ای از پیاده‌سازی چنین کلاس پایه انتزاعی Sequence را در قطعه‌کد ۲.۱۴ ارائه می‌کنیم.

یادداشت مترجم: از پایتون ۳.۳ به بعد، این کلاس‌های پایه انتزاعی در ماژول collections.abc قرار دارند و از پایتون ۳.۱۰ دسترسی به آن‌ها مستقیماً از طریق collections (مثلاً collections.Sequence) حذف شده است. بنابراین در نسخه‌های جدید پایتون باید از collections.abc.Sequence استفاده کرد.

from abc import ABCMeta, abstractmethod  # need these definitions

class Sequence(metaclass=ABCMeta):
    """Our own version of collections.Sequence abstract base class."""

    @abstractmethod
    def __len__(self):
        """Return the length of the sequence."""

    @abstractmethod
    def __getitem__(self, j):
        """Return the element at index j of the sequence."""

    def __contains__(self, val):
        """Return True if val found in the sequence; False otherwise."""
        for j in range(len(self)):
            if self[j] == val:            # found match
                return True
        return False

    def index(self, val):
        """Return leftmost index at which val is found (or raise ValueError)."""
        for j in range(len(self)):
            if self[j] == val:            # leftmost match
                return j
        raise ValueError('value not in sequence')  # never found a match

    def count(self, val):
        """Return the number of elements equal to given value."""
        k = 0
        for j in range(len(self)):
            if self[j] == val:            # found a match
                k += 1
        return k

قطعه‌کد ۲.۱۴: کلاس پایه انتزاعی مشابه collections.Sequence.

این پیاده‌سازی بر دو فن پیشرفته پایتون متکی است. نخست اینکه کلاس ABCMeta از ماژول abc را به‌عنوان متاکلاس (metaclass) کلاس Sequence اعلان می‌کنیم. متاکلاس با ابرکلاس تفاوت دارد؛ متاکلاس قالبی برای خود تعریف کلاس فراهم می‌کند. به‌طور مشخص، اعلان ABCMeta تضمین می‌کند که اگر کسی بخواهد مستقیماً از این کلاس نمونه بسازد، سازنده آن خطا ایجاد کند.

فن پیشرفته دوم، استفاده از دکوراتور @abstractmethod درست پیش از اعلان متدهای __len__ و __getitem__ است. این دکوراتور این دو متد را انتزاعی اعلام می‌کند؛ یعنی در کلاس پایه Sequence پیاده‌سازی‌ای برای آن‌ها ارائه نمی‌دهیم، اما انتظار داریم هر زیرکلاس عینی از این دو متد پشتیبانی کند. پایتون این انتظار را با جلوگیری از نمونه‌سازی هر زیرکلاسی که متدهای انتزاعی را با پیاده‌سازی عینی بازنویسی نکرده باشد، اعمال می‌کند.

بقیه تعریف کلاس Sequence پیاده‌سازی‌های ملموسی برای سایر رفتارها ارائه می‌دهد، با این فرض که متدهای انتزاعی __len__ و __getitem__ در زیرکلاس عینی وجود خواهند داشت. اگر کد منبع را با دقت بررسی کنید، می‌بینید پیاده‌سازی متدهای __contains__، index و count هیچ فرضی درباره نمونه‌های self ندارند، جز اینکه نحو len(self) و self[j] (به‌ترتیب از طریق متدهای ویژه __len__ و __getitem__) پشتیبانی می‌شوند. پشتیبانی از پیمایش نیز، همان‌طور که در بخش ۲.۳.۴ شرح داده شد، به‌طور خودکار فراهم است.

در ادامه این کتاب، از تشریفات استفاده از ماژول abc صرف‌نظر می‌کنیم. اگر به یک کلاس پایه «انتزاعی» نیاز داشته باشیم، صرفاً در مستندات ذکر می‌کنیم که انتظار می‌رود زیرکلاس‌ها کارکرد مفروض را فراهم کنند، بی‌آنکه متدها را به‌طور فنی انتزاعی اعلان کنیم. اما از کلاس‌های پایه انتزاعی بسیار سودمندی که در ماژول collections تعریف شده‌اند (مانند Sequence) استفاده خواهیم کرد. برای استفاده از چنین کلاسی، کافی است از فنون استاندارد وراثت بهره بگیریم.

برای مثال، کلاس Range ما در قطعه‌کد ۲.۶ از بخش ۲.۳.۵، نمونه‌ای از کلاسی است که از متدهای __len__ و __getitem__ پشتیبانی می‌کند. اما این کلاس از متدهای count یا index پشتیبانی نمی‌کند. اگر از ابتدا آن را با Sequence به‌عنوان ابرکلاس اعلان کرده بودیم، متدهای count و index را نیز به ارث می‌برد. نحو چنین اعلانی این‌گونه آغاز می‌شود:

class Range(collections.Sequence):

سرانجام تأکید می‌کنیم که اگر زیرکلاسی پیاده‌سازی خودش را از رفتاری که از کلاس پایه به ارث برده ارائه کند، تعریف جدید جایگزین تعریف به‌ارث‌رسیده می‌شود. از این فن می‌توان زمانی استفاده کرد که بتوانیم برای یک رفتار پیاده‌سازی کارآمدتری از رویکرد عام ارائه دهیم. برای نمونه، پیاده‌سازی عام __contains__ برای یک دنباله بر حلقه‌ای برای جستجوی مقدار مورد نظر استوار است. برای کلاس Range ما، فرصتی برای تعیین کارآمدتر عضویت وجود دارد. برای مثال، روشن است که عبارت 100000 in Range(0, 2000000, 100) باید حتی بدون بررسی تک‌تک عناصر بازه به True ارزیابی شود؛ زیرا بازه از صفر شروع می‌شود، گام آن ۱۰۰ است و تا ۲ میلیون ادامه می‌یابد، پس باید ۱۰۰۰۰۰ را در بر داشته باشد، چون مضربی از ۱۰۰ است که میان مقادیر شروع و پایان قرار دارد. تمرین C-2.27 هدف ارائه پیاده‌سازی‌ای از Range.__contains__ را دنبال می‌کند که از حلقه (زمان‌بر) استفاده نکند.

۲.۵ فضاهای نام و شی‌گرایی

فضای نام (namespace) انتزاعی است که همه شناسه‌های تعریف‌شده در یک حوزه (scope) خاص را مدیریت می‌کند و هر نام را به مقدار متناظرش نگاشت می‌کند. در پایتون، توابع، کلاس‌ها و ماژول‌ها همگی اشیای درجه‌یک (first-class) هستند؛ بنابراین «مقدار» متناظر با یک شناسه در فضای نام ممکن است در واقع یک تابع، کلاس یا ماژول باشد.

در مباحث مقدماتی پایتون دیدیم که پایتون چگونه از فضاهای نام برای مدیریت شناسه‌های تعریف‌شده در حوزه سراسری (global)، در مقابل شناسه‌های تعریف‌شده در حوزه محلی یک فراخوانی تابع، استفاده می‌کند. در این بخش، نقش مهم فضاهای نام را در مدیریت شی‌گرایی در پایتون بررسی می‌کنیم.

۲.۵.۱ فضای نام نمونه و فضای نام کلاس

بررسی را با آنچه فضای نام نمونه (instance namespace) نامیده می‌شود آغاز می‌کنیم؛ فضایی که صفت‌های مختص یک شیء منفرد را مدیریت می‌کند. برای مثال، هر نمونه از کلاس CreditCard موجودی، شماره حساب، سقف اعتبار و دیگر مشخصات مجزای خود را نگه می‌دارد (هرچند ممکن است برخی نمونه‌ها به‌طور تصادفی موجودی یا سقف اعتبار یکسانی داشته باشند). هر کارت اعتباری فضای نام نمونه اختصاصی‌ای برای مدیریت چنین مقادیری خواهد داشت.

برای هر کلاسی که تعریف می‌شود، یک فضای نام کلاس (class namespace) جداگانه وجود دارد. این فضای نام برای مدیریت اعضایی به کار می‌رود که قرار است میان همه نمونه‌های یک کلاس مشترک باشند یا بدون ارجاع به نمونه‌ای خاص به کار روند. برای مثال، متد make_payment از کلاس CreditCard (بخش ۲.۳) به‌طور جداگانه در هر نمونه از آن کلاس ذخیره نمی‌شود، بلکه در فضای نام کلاس CreditCard ذخیره می‌شود. بر اساس تعریف ما در قطعه‌کدهای ۲.۱ و ۲.۲، فضای نام کلاس CreditCard شامل این توابع است: __init__، get_customer، get_bank، get_account، get_balance، get_limit، charge و make_payment. کلاس PredatoryCreditCard فضای نام خاص خود را دارد که شامل سه متدی است که برای این زیرکلاس تعریف کردیم: __init__، charge و process_month.

شکل ۲.۸ تصویری از سه فضای نام ارائه می‌دهد: فضای نام کلاسی که متدهای کلاس CreditCard را در بر دارد، فضای نام کلاس دیگری با متدهای کلاس PredatoryCreditCard، و در نهایت فضای نام نمونه برای یک نمونه از کلاس PredatoryCreditCard. توجه کنید که دو تعریف متفاوت از تابعی به نام charge وجود دارد: یکی در کلاس CreditCard و دیگری متد بازنویسی‌شده در کلاس PredatoryCreditCard. به همین ترتیب، دو پیاده‌سازی متمایز از __init__ وجود دارد. اما process_month نامی است که فقط در حوزه کلاس PredatoryCreditCard تعریف شده است. فضای نام نمونه همه اعضای داده نمونه را در بر دارد (از جمله عضو _apr که سازنده PredatoryCreditCard آن را ایجاد می‌کند).

(الف) فضای نام کلاس CreditCard — هر نام به یک تابع اشاره می‌کند: __init__، get_customer، get_bank، get_account، make_payment، get_balance، get_limit، charge

(ب) فضای نام کلاس PredatoryCreditCard — هر نام به یک تابع اشاره می‌کند: __init__، process_month، charge

(ج) فضای نام نمونه برای یک شیء PredatoryCreditCard:

نام مقدار
_customer 'John Bowman'
_bank 'California Savings'
_account '5391 0375 9387 5309'
_balance 1234.56
_limit 2500
_apr 0.0825

شکل ۲.۸: نمای مفهومی سه فضای نام: (الف) فضای نام کلاس CreditCard؛ (ب) فضای نام کلاس PredatoryCreditCard؛ (ج) فضای نام نمونه برای یک شیء PredatoryCreditCard.

ورودی‌های فضای نام چگونه ایجاد می‌شوند

مهم است بفهمیم چرا عضوی مانند _balance در فضای نام نمونه یک کارت اعتباری قرار دارد، در حالی که عضوی مانند make_payment در فضای نام کلاس قرار دارد. موجودی هنگام ساخت نمونه جدید کارت اعتباری، درون متد __init__ ایجاد می‌شود. انتساب اولیه از نحو self._balance = 0 استفاده می‌کند که در آن self شناسه نمونه تازه‌ساخته است. استفاده از self به‌عنوان پیشوند self._balance در چنین انتسابی موجب می‌شود شناسه _balance مستقیماً به فضای نام نمونه افزوده شود.

وقتی از وراثت استفاده می‌شود، همچنان برای هر شیء تنها یک فضای نام نمونه وجود دارد. برای مثال، وقتی نمونه‌ای از کلاس PredatoryCreditCard ساخته می‌شود، صفت _apr و نیز صفت‌هایی مانند _balance و _limit همگی در فضای نام همان نمونه قرار می‌گیرند، زیرا همه با نحو پیشونددار، مانند self._apr، مقداردهی می‌شوند.

فضای نام کلاس شامل همه اعلان‌هایی است که مستقیماً در بدنه تعریف کلاس انجام می‌شوند. برای مثال، تعریف کلاس CreditCard ما ساختار زیر را داشت:

class CreditCard:
    def make_payment(self, amount):
        ...

از آنجا که تابع make_payment در حوزه کلاس CreditCard اعلان شده است، این تابع با نام make_payment در فضای نام کلاس CreditCard مرتبط می‌شود. هرچند توابع عضو رایج‌ترین ورودی‌های اعلان‌شده در فضای نام کلاس‌اند، در ادامه بررسی می‌کنیم که چگونه می‌توان انواع دیگری از مقادیر داده یا حتی کلاس‌های دیگر را نیز در فضای نام کلاس اعلان کرد.

اعضای داده کلاس

عضو داده سطح کلاس (class-level data member) معمولاً زمانی به کار می‌رود که مقداری، مانند یک ثابت، باید میان همه نمونه‌های کلاس مشترک باشد. در چنین حالتی، ذخیره این مقدار در فضای نام هر نمونه بی‌جهت اسراف‌آمیز خواهد بود. برای مثال، کلاس PredatoryCreditCard معرفی‌شده در بخش ۲.۴.۱ را دوباره در نظر بگیرید. این کلاس در صورت رد شدن ثبت هزینه به دلیل سقف اعتبار، ۵ دلار جریمه دریافت می‌کند. انتخاب ۵ دلار برای جریمه تا حدی دلبخواهی بود و سبک کدنویسی ما بهتر می‌شد اگر به جای گنجاندن مقدار لفظی در کد، از یک متغیر نام‌دار استفاده می‌کردیم. اغلب، مبلغ چنین جریمه‌ای را سیاست بانک تعیین می‌کند و برای هر مشتری متفاوت نیست. در این صورت، می‌توانیم عضو داده کلاسی را به این شکل تعریف و استفاده کنیم:

class PredatoryCreditCard(CreditCard):
    OVERLIMIT_FEE = 5        # this is a class-level member

    def charge(self, price):
        success = super().charge(price)
        if not success:
            self._balance += PredatoryCreditCard.OVERLIMIT_FEE
        return success

عضو داده OVERLIMIT_FEE در فضای نام کلاس PredatoryCreditCard قرار می‌گیرد، زیرا این انتساب مستقیماً در حوزه تعریف کلاس و بدون هیچ شناسه پیشوندی انجام شده است.

کلاس‌های تودرتو

همچنین می‌توان تعریف یک کلاس را درون حوزه کلاسی دیگر قرار داد. این ساختار سودمندی است که در این کتاب چندین بار در پیاده‌سازی ساختارهای داده از آن بهره خواهیم گرفت. این کار با نحوی مانند زیر انجام می‌شود:

class A:          # the outer class
    class B:      # the nested class
        ...

در این حالت، کلاس B کلاس تودرتو است. شناسه B در فضای نام کلاس A ثبت شده و به کلاس تازه‌تعریف‌شده مرتبط می‌شود. توجه کنید که این فن هیچ ارتباطی با مفهوم وراثت ندارد، زیرا کلاس B از کلاس A ارث نمی‌برد.

قرار دادن یک کلاس درون حوزه کلاسی دیگر نشان می‌دهد که کلاس تودرتو برای پشتیبانی از کلاس بیرونی وجود دارد. افزون بر این، می‌تواند به کاهش تداخل احتمالی نام‌ها کمک کند، زیرا اجازه می‌دهد کلاسی با نام مشابه در زمینه‌ای دیگر وجود داشته باشد. برای مثال، در ادامه کتاب ساختار داده‌ای به نام لیست پیوندی (linked list) معرفی خواهیم کرد و یک کلاس گره تودرتو برای ذخیره اجزای منفرد لیست تعریف خواهیم کرد. همچنین ساختار داده‌ای به نام درخت (tree) معرفی خواهیم کرد که به کلاس گره تودرتوی خاص خود وابسته است. این دو ساختار به تعاریف متفاوتی از گره متکی‌اند و با قرار دادن هر یک درون کلاس ظرف مربوط، از ابهام پرهیز می‌کنیم.

مزیت دیگر تودرتو بودن یک کلاس به‌عنوان عضوی از کلاس دیگر این است که شکل پیشرفته‌تری از وراثت را ممکن می‌سازد که در آن زیرکلاسی از کلاس بیرونی، تعریف کلاس تودرتوی آن را بازنویسی می‌کند. از این فن در فصل‌های بعدی، هنگام تخصصی کردن گره‌های یک ساختار درختی، استفاده خواهیم کرد.

دیکشنری‌ها و اعلان __slots__

پایتون به‌طور پیش‌فرض هر فضای نام را با نمونه‌ای از کلاس داخلی dict نمایش می‌دهد که نام‌های شناسه‌های آن حوزه را به اشیای متناظر نگاشت می‌کند. هرچند ساختار دیکشنری جستجوی نسبتاً کارآمد نام‌ها را ممکن می‌سازد، علاوه بر داده‌های خامی که ذخیره می‌کند به حافظه اضافی نیاز دارد (ساختار داده‌ای را که برای پیاده‌سازی دیکشنری‌ها به کار می‌رود در فصل‌های بعدی بررسی خواهیم کرد).

پایتون سازوکار مستقیم‌تری برای نمایش فضای نام نمونه‌ها فراهم می‌کند که از دیکشنری کمکی استفاده نمی‌کند. برای استفاده از این نمایش فشرده برای همه نمونه‌های یک کلاس، تعریف آن کلاس باید عضوی سطح کلاس به نام __slots__ داشته باشد که به دنباله‌ای ثابت از رشته‌ها، به‌عنوان نام متغیرهای نمونه، مقداردهی شده باشد. برای مثال، در کلاس CreditCard چنین اعلانی خواهیم داشت:

class CreditCard:
    __slots__ = '_customer', '_bank', '_account', '_balance', '_limit'

در این مثال، سمت راست انتساب از نظر فنی یک تاپل است (به دلیل بسته‌بندی خودکار تاپل‌ها در پایتون).

وقتی از وراثت استفاده می‌شود، اگر کلاس پایه __slots__ را اعلان کند، زیرکلاس نیز باید __slots__ را اعلان کند تا از ایجاد دیکشنری برای نمونه‌ها جلوگیری شود. اعلان زیرکلاس باید فقط نام متغیرهای نمونه تکمیلی‌ای را که به‌تازگی معرفی شده‌اند در بر داشته باشد. برای مثال، تعریف PredatoryCreditCard شامل اعلان زیر خواهد بود:

class PredatoryCreditCard(CreditCard):
    __slots__ = '_apr'        # in addition to the inherited members

می‌توانستیم از اعلان __slots__ برای فشرده‌سازی همه کلاس‌های این کتاب استفاده کنیم. اما چنین نمی‌کنیم، زیرا این سطح از سخت‌گیری در برنامه‌های پایتون رایج نیست. با این حال، چند کلاس در این کتاب هستند که انتظار داریم نمونه‌های بسیار زیادی از آن‌ها ساخته شود و هر نمونه نماینده ساختاری سبک باشد. برای مثال، هنگام بحث درباره کلاس‌های تودرتو، لیست‌های پیوندی و درخت‌ها را به‌عنوان ساختارهای داده‌ای مطرح کردیم که اغلب از تعداد زیادی گره منفرد تشکیل می‌شوند. برای افزایش کارایی در مصرف حافظه، در هر کلاس تودرتویی که انتظار داشته باشیم نمونه‌های زیادی از آن ساخته شود، از اعلان صریح __slots__ استفاده خواهیم کرد.

۲.۵.۲ تحلیل نام و ارسال پویا

در بخش پیش، فضاهای نام مختلف و سازوکار ایجاد ورودی در آن‌ها را بررسی کردیم. در این بخش، فرایندی را بررسی می‌کنیم که هنگام بازیابی یک نام در چارچوب شی‌گرای پایتون به کار می‌رود. وقتی از نحو عملگر نقطه برای دسترسی به عضوی موجود، مانند obj.foo، استفاده می‌شود، مفسر پایتون فرایند تحلیل نام (name resolution) را به این صورت آغاز می‌کند:

  1. فضای نام نمونه جستجو می‌شود؛ اگر نام مورد نظر یافت شود، مقدار متناظر آن به کار می‌رود.
  2. در غیر این صورت، فضای نام کلاسی که نمونه به آن تعلق دارد جستجو می‌شود؛ اگر نام یافت شود، مقدار متناظر آن به کار می‌رود.
  3. اگر نام در فضای نام همان کلاس یافت نشود، جستجو در سلسله‌مراتب وراثت به سمت بالا ادامه می‌یابد و فضای نام کلاس هر نیا بررسی می‌شود (معمولاً ابتدا ابرکلاس، سپس ابرکلاسِ آن، و به همین ترتیب). نخستین باری که نام یافت شود، مقدار متناظر آن به کار می‌رود.
  4. اگر نام همچنان یافت نشود، خطای AttributeError ایجاد می‌شود.

به‌عنوان مثالی ملموس، فرض کنید mycard به نمونه‌ای از کلاس PredatoryCreditCard اشاره می‌کند. الگوهای استفاده احتمالی زیر را در نظر بگیرید:

  • mycard._balance (یا به‌طور هم‌ارز، self._balance درون بدنه یک متد): عضو داده _balance در فضای نام نمونه mycard یافت می‌شود.
  • mycard.process_month(): جستجو از فضای نام نمونه آغاز می‌شود، اما نام process_month در آن یافت نمی‌شود. در نتیجه، فضای نام کلاس PredatoryCreditCard جستجو می‌شود؛ در این حالت نام یافت شده و همان متد فراخوانی می‌شود.
  • mycard.make_payment(200): جستجوی نام make_payment در فضای نام نمونه و در فضای نام PredatoryCreditCard ناموفق است. این نام در فضای نام ابرکلاس CreditCard یافت می‌شود و بنابراین متد به‌ارث‌رسیده فراخوانی می‌شود.
  • mycard.charge(50): جستجوی نام charge در فضای نام نمونه ناموفق است. فضای نام بعدی که بررسی می‌شود فضای نام کلاس PredatoryCreditCard است، زیرا نوع واقعی نمونه همین کلاس است. در این کلاس تعریفی برای تابع charge وجود دارد و همان فراخوانی می‌شود.

در آخرین مورد، توجه کنید که وجود تابع charge در کلاس PredatoryCreditCard موجب بازنویسی (overriding) نسخه‌ای از این تابع می‌شود که در فضای نام CreditCard وجود دارد. در اصطلاحات سنتی شی‌گرا، پایتون از سازوکاری به نام ارسال پویا (dynamic dispatch) یا انقیاد پویا (dynamic binding) استفاده می‌کند تا در زمان اجرا، بر اساس نوع شیئی که تابع روی آن فراخوانی شده، تعیین کند کدام پیاده‌سازی تابع فراخوانی شود. این رویکرد در تقابل با برخی زبان‌هاست که از ارسال ایستا (static dispatching) استفاده می‌کنند و بر اساس نوع اعلان‌شده یک متغیر، در زمان کامپایل تصمیم می‌گیرند کدام نسخه از تابع فراخوانی شود.

۲.۶ کپی سطحی و کپی عمیق

در مباحث مقدماتی پایتون تأکید کردیم که دستور انتساب foo = bar نام foo را به نام مستعار (alias) شیئی تبدیل می‌کند که با bar مشخص شده است. در این بخش، ساختن کپی (copy) از یک شیء، به جای نام مستعار، را بررسی می‌کنیم. این کار در کاربردهایی ضروری است که بخواهیم بعداً اصل یا کپی را مستقل از دیگری تغییر دهیم.

سناریویی را در نظر بگیرید که در آن لیست‌های مختلفی از رنگ‌ها را مدیریت می‌کنیم و هر رنگ با نمونه‌ای از یک کلاس رنگ فرضی نمایش داده می‌شود. فرض کنید شناسه warmtones به لیستی موجود از چنین رنگ‌هایی (مثلاً نارنجی‌ها و قهوه‌ای‌ها) اشاره کند. در این کاربرد، می‌خواهیم لیست جدیدی به نام palette بسازیم که کپی لیست warmtones باشد. اما می‌خواهیم بعداً بتوانیم رنگ‌های بیشتری به palette بیفزاییم یا برخی رنگ‌های موجود را تغییر دهیم یا حذف کنیم، بی‌آنکه محتوای warmtones تغییر کند. اگر دستور زیر را اجرا کنیم:

palette = warmtones

یک نام مستعار ایجاد می‌شود (شکل ۲.۹). هیچ لیست جدیدی ساخته نمی‌شود، بلکه شناسه جدید palette به همان لیست اصلی ارجاع می‌دهد.

warmtones ──►  list  ◄── palette
                ├──► color(red=249, green=124, blue=43)
                └──► color(red=169, green=163, blue=52)

شکل ۲.۹: دو نام مستعار برای یک لیست رنگ.

متأسفانه این کار معیارهای مورد نظر ما را برآورده نمی‌کند، زیرا اگر بعداً رنگی به palette بیفزاییم یا از آن حذف کنیم، در واقع لیستی را که warmtones نامیده می‌شود تغییر داده‌ایم.

در عوض، می‌توانیم با نحو زیر نمونه جدیدی از کلاس list بسازیم:

palette = list(warmtones)

در این حالت، سازنده list را صراحتاً فراخوانی کرده و لیست نخست را به‌عنوان پارامتر به آن می‌فرستیم. این کار موجب ساخته شدن لیستی جدید می‌شود (شکل ۲.۱۰)؛ اما این همان چیزی است که کپی سطحی (shallow copy) نامیده می‌شود. لیست جدید طوری مقداردهی اولیه می‌شود که محتوایش دقیقاً همان محتوای دنباله اصلی باشد. اما لیست‌های پایتون ارجاعی (referential) هستند، بنابراین لیست جدید دنباله‌ای از ارجاع‌ها به همان عناصر لیست نخست را نگه می‌دارد.

warmtones ──► list ─┬─► color(red=249, green=124, blue=43) ◄─┬─ list ◄── palette
                    └─► color(red=169, green=163, blue=52) ◄─┘

شکل ۲.۱۰: کپی سطحی از یک لیست رنگ.

این وضعیت از تلاش نخست ما بهتر است، زیرا اکنون می‌توانیم بدون تأثیر بر warmtones عناصری به palette بیفزاییم یا از آن حذف کنیم. با این حال، اگر نمونه رنگی را از لیست palette ویرایش کنیم، عملاً محتوای warmtones را تغییر داده‌ایم. هرچند palette و warmtones لیست‌های متمایزی هستند، همچنان نام‌گذاری مستعار غیرمستقیم وجود دارد؛ برای مثال، palette[0] و warmtones[0] نام‌های مستعار یک نمونه رنگ واحدند.

ترجیح می‌دهیم palette یک کپی عمیق (deep copy) از warmtones باشد. در کپی عمیق، کپی جدید به کپی‌های خودش از اشیایی که نسخه اصلی به آن‌ها ارجاع می‌دهد ارجاع می‌دهد (شکل ۲.۱۱).

warmtones ──► list ─┬─► color(red=249, green=124, blue=43)
                    └─► color(red=169, green=163, blue=52)

palette   ──► list ─┬─► color(red=249, green=124, blue=43)   # independent copy
                    └─► color(red=169, green=163, blue=52)   # independent copy

شکل ۲.۱۱: کپی عمیق از یک لیست رنگ.

ماژول copy پایتون

برای ساختن کپی عمیق، می‌توانستیم لیست خود را با ساختن صریح کپی از نمونه‌های رنگ اصلی پر کنیم، اما این کار مستلزم آن است که بدانیم چگونه از رنگ‌ها کپی بسازیم (نه نام مستعار). پایتون ماژول بسیار کاربردی‌ای به نام copy فراهم می‌کند که می‌تواند از اشیای دلخواه هم کپی سطحی و هم کپی عمیق تولید کند.

این ماژول دو تابع دارد: تابع copy کپی سطحی آرگومان خود را می‌سازد و تابع deepcopy کپی عمیق آن را. پس از وارد کردن (import) ماژول، می‌توانیم با دستور زیر کپی عمیقی مانند آنچه در شکل ۲.۱۱ نشان داده شده بسازیم:

palette = copy.deepcopy(warmtones)

۲.۷ تمرین‌ها

برای راهنمایی درباره تمرین‌ها، به وبگاه www.wiley.com/college/goodrich مراجعه کنید.

تقویتی (Reinforcement)

R-2.1 سه نمونه از کاربردهای نرم‌افزاری حیاتی (life-critical) ذکر کنید.

R-2.2 نمونه‌ای از یک کاربرد نرم‌افزاری ذکر کنید که در آن انطباق‌پذیری می‌تواند تفاوت میان دوام طولانی فروش و ورشکستگی باشد.

R-2.3 یکی از مؤلفه‌های رابط کاربری گرافیکی یک ویرایشگر متن و متدهایی را که کپسوله می‌کند توصیف کنید.

R-2.4 کلاسی به نام Flower در پایتون بنویسید که سه متغیر نمونه از انواع str، int و float داشته باشد که به‌ترتیب نام گل، تعداد گلبرگ‌ها و قیمت آن را نشان دهند. کلاس شما باید متد سازنده‌ای داشته باشد که هر متغیر را با مقداری مناسب مقداردهی اولیه کند، و همچنین متدهایی برای تنظیم و بازیابی مقدار هر یک از این متغیرها داشته باشد.

R-2.5 با استفاده از فنون مدیریت استثنا در پایتون، متدهای charge و make_payment کلاس CreditCard را طوری اصلاح کنید که اطمینان حاصل شود فراخواننده یک عدد را به‌عنوان پارامتر می‌فرستد.

R-2.6 اگر پارامتر متد make_payment در کلاس CreditCard عددی منفی باشد، موجب افزایش موجودی حساب می‌شود. پیاده‌سازی را طوری اصلاح کنید که در صورت ارسال مقدار منفی، خطای ValueError ایجاد شود.

R-2.7 کلاس CreditCard در بخش ۲.۳ موجودی حساب جدید را صفر مقداردهی می‌کند. این کلاس را طوری تغییر دهید که بتوان با یک پارامتر پنجم اختیاری در سازنده، به حساب جدید موجودی غیرصفر داد. نحو چهارپارامتری سازنده باید همچنان حسابی با موجودی صفر بسازد.

R-2.8 اعلان نخستین حلقه for در تست‌های CreditCard (قطعه‌کد ۲.۳) را طوری تغییر دهید که سرانجام دقیقاً یکی از سه کارت اعتباری از سقف اعتبار خود فراتر برود. کدام کارت اعتباری است؟

R-2.9 متد __sub__ را برای کلاس Vector بخش ۲.۳.۳ پیاده‌سازی کنید تا عبارت u - v نمونه برداری جدید برگرداند که تفاضل دو بردار را نشان دهد.

R-2.10 متد __neg__ را برای کلاس Vector بخش ۲.۳.۳ پیاده‌سازی کنید تا عبارت -v نمونه برداری جدید برگرداند که هر مختصه‌اش قرینه مختصه متناظر v باشد.

R-2.11 در بخش ۲.۳.۳ گفتیم که کلاس Vector از نحوی مانند v = u + [5, 3, 10, -2, 1] پشتیبانی می‌کند که در آن مجموع یک بردار و یک لیست برداری جدید برمی‌گرداند. اما نحو v = [5, 3, 10, -2, 1] + u غیرمجاز است. توضیح دهید که چگونه می‌توان تعریف کلاس Vector را اصلاح کرد تا این نحو نیز برداری جدید تولید کند.

R-2.12 متد __mul__ را برای کلاس Vector بخش ۲.۳.۳ پیاده‌سازی کنید تا عبارت v * 3 برداری جدید برگرداند که هر مختصه‌اش ۳ برابر مختصه متناظر v باشد.

R-2.13 تمرین R-2.12 پیاده‌سازی __mul__ را برای پشتیبانی از نحو v * 3 در کلاس Vector بخش ۲.۳.۳ می‌خواهد. متد __rmul__ را پیاده‌سازی کنید تا از نحو 3 * v نیز پشتیبانی شود.

R-2.14 متد __mul__ را برای کلاس Vector بخش ۲.۳.۳ پیاده‌سازی کنید تا عبارت u * v مقداری عددی (اسکالر) برگرداند که ضرب داخلی دو بردار، یعنی ∑ uᵢ · vᵢ (برای i از ۱ تا d)، را نشان دهد.

R-2.15 کلاس Vector بخش ۲.۳.۳ سازنده‌ای دارد که عدد صحیح d را می‌گیرد و برداری d-بعدی با همه مختصات برابر ۰ می‌سازد. شکل راحت دیگری برای ساختن بردار جدید این است که پارامتری از یک نوع پیمایش‌پذیر (iterable) که دنباله‌ای از اعداد را نمایش می‌دهد به سازنده بفرستیم و برداری بسازیم که بعدش برابر طول آن دنباله و مختصاتش برابر مقادیر آن باشد. برای مثال، Vector([4, 7, 5]) برداری سه‌بعدی با مختصات <4, 7, 5> می‌سازد. سازنده را طوری تغییر دهید که هر دو شکل پذیرفته شوند؛ یعنی اگر یک عدد صحیح فرستاده شود، برداری با همان بعد و همه مختصات صفر بسازد، و اگر دنباله‌ای از اعداد داده شود، برداری با مختصات برگرفته از آن دنباله بسازد.

R-2.16 کلاس Range ما در بخش ۲.۳.۵ برای محاسبه تعداد عناصر بازه از فرمول max(0, (stop - start + step - 1) // step) استفاده می‌کند. اینکه چرا این فرمول، حتی با فرض گام مثبت، محاسبه درستی ارائه می‌دهد بی‌درنگ آشکار نیست. با بیان خودتان درستی این فرمول را توجیه کنید.

R-2.17 نمودار وراثت کلاس‌ها را برای مجموعه کلاس‌های زیر رسم کنید:

  • کلاس Goat از object ارث می‌برد و متغیر نمونه _tail و متدهای milk() و jump() را می‌افزاید.
  • کلاس Pig از object ارث می‌برد و متغیر نمونه _nose و متدهای eat(food) و wallow() را می‌افزاید.
  • کلاس Horse از object ارث می‌برد و متغیرهای نمونه _height و _color و متدهای run() و jump() را می‌افزاید.
  • کلاس Racer از Horse ارث می‌برد و متد race() را می‌افزاید.
  • کلاس Equestrian از Horse ارث می‌برد و متغیر نمونه _weight و متدهای trot() و is_trained() را می‌افزاید.

R-2.18 قطعه‌کد کوتاهی در پایتون بنویسید که با استفاده از کلاس‌های تصاعد بخش ۲.۴.۲، هشتمین مقدار تصاعد فیبوناچی‌ای را بیابد که دو مقدار نخست آن ۲ و ۲ هستند.

R-2.19 هنگام استفاده از کلاس ArithmeticProgression بخش ۲.۴.۲ با گام افزایش ۱۲۸ و مقدار شروع ۰، پیش از رسیدن به عدد صحیحی برابر 2⁶³ یا بزرگ‌تر، چند بار می‌توان next را فراخوانی کرد؟

R-2.20 برخی از معایب احتمالی درخت‌های وراثت بسیار عمیق از نظر کارایی چیست؟ یعنی مجموعه بزرگی از کلاس‌های A، B، C و … به‌طوری که B از A، C از B، D از C و … ارث ببرد.

R-2.21 برخی از معایب احتمالی درخت‌های وراثت بسیار کم‌عمق از نظر کارایی چیست؟ یعنی مجموعه بزرگی از کلاس‌های A، B، C و … به‌طوری که همه آن‌ها از یک کلاس واحد Z ارث ببرند.

R-2.22 کلاس پایه انتزاعی collections.Sequence از مقایسه دو دنباله با یکدیگر پشتیبانی نمی‌کند. کلاس Sequence ما در قطعه‌کد ۲.۱۴ را طوری تغییر دهید که تعریفی برای متد __eq__ داشته باشد، به‌طوری که عبارت seq1 == seq2 دقیقاً زمانی True برگرداند که دو دنباله عنصربه‌عنصر هم‌ارز باشند.

R-2.23 در همین راستا، کلاس Sequence را با متد __lt__ تکمیل کنید تا از مقایسه واژه‌نامه‌ای (lexicographic) seq1 < seq2 پشتیبانی شود.

خلاقیت (Creativity)

C-2.24 فرض کنید عضو تیم طراحی یک کتاب‌خوان الکترونیکی جدید هستید. کلاس‌ها و متدهای اصلی‌ای که نرم‌افزار پایتونی کتاب‌خوان شما به آن‌ها نیاز دارد کدام‌اند؟ باید نمودار وراثتی برای این کد ارائه کنید، اما نیازی به نوشتن کد واقعی نیست. معماری نرم‌افزار شما باید دست‌کم امکان خرید کتاب‌های جدید، مشاهده فهرست کتاب‌های خریداری‌شده و خواندن آن‌ها را برای مشتریان فراهم کند.

C-2.25 تمرین R-2.12 از متد __mul__ برای پشتیبانی از ضرب یک Vector در یک عدد استفاده می‌کند، در حالی که تمرین R-2.14 از متد __mul__ برای محاسبه ضرب داخلی دو بردار استفاده می‌کند. پیاده‌سازی واحدی از Vector.__mul__ ارائه دهید که با بررسی نوع در زمان اجرا، از هر دو نحو u * v و u * k پشتیبانی کند؛ که در آن u و v نمونه‌های بردار و k یک عدد است.

C-2.26 کلاس SequenceIterator بخش ۲.۳.۴ چیزی را فراهم می‌کند که تکرارگر رو به جلو (forward iterator) نامیده می‌شود. کلاسی به نام ReversedSequenceIterator پیاده‌سازی کنید که برای هر نوع دنباله پایتون نقش تکرارگر معکوس را ایفا کند. نخستین فراخوانی next باید آخرین عنصر دنباله، دومین فراخوانی next عنصر ماقبل آخر، و به همین ترتیب را برگرداند.

C-2.27 در بخش ۲.۳.۵ اشاره کردیم که نسخه ما از کلاس Range به دلیل پشتیبانی صریح از __len__ و __getitem__، به‌طور ضمنی از پیمایش پشتیبانی می‌کند. این کلاس همچنین به‌طور ضمنی از آزمون بولی «k in r» برای بازه‌ای مانند r پشتیبانی می‌کند. این آزمون با پیمایش رو به جلوی بازه ارزیابی می‌شود؛ گواه آن، سریع‌تر بودن نسبی آزمون 2 in Range(10000000) در مقایسه با 9999999 in Range(10000000) است. پیاده‌سازی کارآمدتری از متد __contains__ ارائه دهید که تعیین کند آیا مقدار مشخصی در بازه داده‌شده قرار دارد یا نه. زمان اجرای متد شما باید مستقل از طول بازه باشد.

C-2.28 کلاس PredatoryCreditCard بخش ۲.۴.۱ متد process_month را دارد که پایان یک دوره ماهانه را مدل‌سازی می‌کند. کلاس را طوری تغییر دهید که وقتی مشتری در ماه جاری ده بار charge را فراخوانی کرد، هر فراخوانی اضافی این تابع به دریافت ۱ دلار هزینه اضافی بینجامد.

C-2.29 کلاس PredatoryCreditCard بخش ۲.۴.۱ را طوری تغییر دهید که برای مشتری حداقل پرداخت ماهانه‌ای، به‌صورت درصدی از موجودی، تعیین شود و اگر مشتری تا پیش از دوره ماهانه بعد این حداقل مبلغ را نپردازد، جریمه دیرکرد دریافت شود.

C-2.30 در پایان بخش ۲.۴.۱ مدلی را پیشنهاد کردیم که در آن کلاس CreditCard از متد غیرعمومی _set_balance(b) پشتیبانی می‌کند تا زیرکلاس‌ها بتوانند بدون دسترسی مستقیم به عضو داده _balance موجودی را تغییر دهند. چنین مدلی را پیاده‌سازی کنید و هر دو کلاس CreditCard و PredatoryCreditCard را متناسب با آن اصلاح کنید.

C-2.31 کلاسی در پایتون بنویسید که کلاس Progression را گسترش دهد، به‌طوری که هر مقدار در تصاعد برابر قدر مطلق تفاضل دو مقدار قبلی باشد. سازنده‌ای در نظر بگیرید که دو عدد را به‌عنوان دو مقدار نخست بپذیرد و مقادیر پیش‌فرض آن ۲ و ۲۰۰ باشند.

C-2.32 کلاسی در پایتون بنویسید که کلاس Progression را گسترش دهد، به‌طوری که هر مقدار در تصاعد ریشه دوم مقدار قبلی باشد. (توجه کنید که دیگر نمی‌توان هر مقدار را با یک عدد صحیح نمایش داد.) سازنده شما باید پارامتری اختیاری برای تعیین مقدار شروع بپذیرد که مقدار پیش‌فرض آن ۶۵٬۵۳۶ باشد.

پروژه‌ها (Projects)

P-2.33 برنامه‌ای در پایتون بنویسید که یک چندجمله‌ای را با نمادگذاری جبری استاندارد دریافت کند و مشتق اول آن را در خروجی نمایش دهد.

P-2.34 برنامه‌ای در پایتون بنویسید که یک سند را دریافت کند و سپس نمودار میله‌ای فراوانی هر حرف الفبایی موجود در آن سند را رسم کند.

P-2.35 مجموعه‌ای از کلاس‌های پایتون بنویسید که یک برنامه اینترنتی را شبیه‌سازی کنند. در این برنامه، یکی از طرفین، آلیس، به‌طور دوره‌ای مجموعه‌ای از بسته‌ها (packets) ایجاد می‌کند که می‌خواهد برای باب بفرستد. یک فرایند اینترنتی پیوسته بررسی می‌کند که آیا آلیس بسته‌ای برای ارسال دارد و اگر داشته باشد، آن را به کامپیوتر باب تحویل می‌دهد؛ باب نیز به‌طور دوره‌ای بررسی می‌کند که آیا کامپیوترش بسته‌ای از آلیس دارد و اگر داشته باشد، آن را می‌خواند و حذف می‌کند.

P-2.36 برنامه‌ای در پایتون بنویسید که یک بوم‌سازگان (اکوسیستم) شامل دو نوع موجود، خرس و ماهی، را شبیه‌سازی کند. این بوم‌سازگان از یک رودخانه تشکیل شده که با یک لیست نسبتاً بزرگ مدل‌سازی می‌شود. هر عنصر لیست باید یک شیء Bear، یک شیء Fish یا None باشد. در هر گام زمانی، بر اساس یک فرایند تصادفی، هر حیوان یا تلاش می‌کند به خانه مجاور در لیست برود یا در جای خود بماند. اگر دو حیوان هم‌نوع در آستانه برخورد در یک خانه باشند، هر دو در جای خود می‌مانند، اما نمونه جدیدی از همان نوع حیوان ایجاد می‌کنند که در یک خانه خالی تصادفی (یعنی خانه‌ای که پیش‌تر None بوده) در لیست قرار می‌گیرد. اما اگر یک خرس و یک ماهی با هم برخورد کنند، ماهی می‌میرد (یعنی ناپدید می‌شود).

P-2.37 شبیه‌سازی مانند پروژه قبل بنویسید، اما با استفاده از کلاس Animal به‌عنوان کلاس پایه، به هر حیوان یک فیلد بولی جنسیت و یک فیلد ممیز شناور قدرت بیفزایید. اگر دو حیوان هم‌نوع بخواهند با هم برخورد کنند، فقط در صورتی نمونه جدیدی از آن نوع حیوان ایجاد می‌کنند که جنسیت‌شان متفاوت باشد. در غیر این صورت، اگر دو حیوان هم‌نوع و هم‌جنس بخواهند با هم برخورد کنند، فقط حیوان قوی‌تر زنده می‌ماند.

P-2.38 برنامه‌ای در پایتون بنویسید که سیستمی پشتیبان کارکردهای یک کتاب‌خوان الکترونیکی را شبیه‌سازی کند. باید متدهایی در نظر بگیرید که کاربران سیستم بتوانند کتاب‌های جدید «بخرند»، فهرست کتاب‌های خریداری‌شده را ببینند و کتاب‌های خریداری‌شده را بخوانند. سیستم شما باید برای پر کردن مجموعه کتاب‌هایی که کاربران می‌توانند «بخرند» و بخوانند، از کتاب‌های واقعی‌ای استفاده کند که حق نشرشان منقضی شده و در اینترنت در دسترس‌اند.

P-2.39 سلسله‌مراتب وراثتی بر پایه کلاس Polygon با متدهای انتزاعی area() و perimeter() طراحی کنید. کلاس‌های Triangle، Quadrilateral، Pentagon، Hexagon و Octagon را پیاده‌سازی کنید که این کلاس پایه را گسترش می‌دهند و متدهای area() و perimeter() در آن‌ها معنای بدیهی خود را دارند. همچنین کلاس‌های IsoscelesTriangle، EquilateralTriangle، Rectangle و Square را با روابط وراثت مناسب پیاده‌سازی کنید. در پایان، برنامه ساده‌ای بنویسید که به کاربران امکان دهد چندضلعی‌هایی از انواع مختلف بسازند و ابعاد هندسی آن‌ها را وارد کنند و برنامه مساحت و محیط آن‌ها را نمایش دهد. برای تلاش بیشتر، به کاربران امکان دهید چندضلعی‌ها را با تعیین مختصات رأس‌هایشان وارد کنند و بتوانند بررسی کنند که آیا دو چندضلعی متشابه‌اند یا نه.

یادداشت‌های فصل

برای مروری گسترده بر تحولات علوم و مهندسی کامپیوتر، خواننده را به کتاب The Computer Science and Engineering Handbook ارجاع می‌دهیم. برای اطلاعات بیشتر درباره حادثه Therac-25، به مقاله Leveson و Turner مراجعه کنید.

خوانندگانی که به مطالعه بیشتر برنامه‌نویسی شی‌گرا علاقه‌مندند می‌توانند به کتاب‌های Booch، Budd و Liskov و Guttag مراجعه کنند. Liskov و Guttag بحث خوبی نیز درباره انواع داده انتزاعی ارائه می‌دهند؛ مقاله مروری Cardelli و Wegner و فصلی از Demurjian در The Computer Science and Engineering Handbook نیز به همین موضوع پرداخته‌اند. الگوهای طراحی در کتاب Gamma و همکاران شرح داده شده‌اند.

از کتاب‌هایی که به‌طور ویژه بر برنامه‌نویسی شی‌گرا در پایتون تمرکز دارند، می‌توان به کتاب Goldwasser و Letscher در سطح مقدماتی و کتاب Phillips در سطح پیشرفته‌تر اشاره کرد.