این حالت نمایش چند صفحه ای قابل پرینت این قسمت می‌باشد. برای پرینت کلیک کنید..

بازگشت به حالت نمایش عادی این قسمت.

کتاب Python Crash Course به فارسی

1 - بخش یکم - مقدمات

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

در فصل ۱ پایتون را روی رایانه‌ی خود نصب می‌کنید و اولین برنامه‌یتان را می‌نویسید، که پیامِ Hello World! را روی نمایشگر ظاهر می‌کند.

در فصل ۲ خواهید آموخت که چگونه اطلاعات را در متغیرها ذخیره و با مقادیرِ عددی و متنی کار کنید.

در دو فصل ۳ و فصل ۴ لیست‌ها معرفی می‌شوند. لیست‌ها می‌توانند هر اندازه اطلاعات را که بخواهید در یک متغیر ذخیره کنند. این باعث می‌شود بتوانید با داده‌ها به طورِ مؤثرتری کار کنید. قادر خواهید شد با صدها، هزاران و حتی میلیون‌ها مقدار در تنها چند خط برنامه کار کنید.

در فصل ۵ از دستورِ if برای نوشتنِ برنامه‌هایی استفاده خواهید کرد که با برآورده شدنِ شرط‌های خاصی یک جور پاسخ می‌دهند و در صورتِ برآورده نشدن جورِ دیگر.

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

در فصل ۷ دریافت کردنِ ورودی را از کاربر فرامی‌گیرید، به منظورِ تعاملی کردنِ برنامه‌های‌تان.

در فصل ۸ تابع‌ها را ایجاد خواهید کرد. توابع قطعه‌های نام‌گذاری‌شده‌ای از برنامه‌ برای انجام دادنِ وظیفه‌ای مشخص‌اند و هر جا که لازم باشد می‌توانید آن‌ها را اجرا کنید.

در فصل ۹ با کلاس‌ها آشنا می‌شوید، که ساختن مدل از اشیا و موجوداتِ جهانِ واقعی —مانندِ سگ‌ها، گربه‌ها، مردم، خودروها، موشک‌ها، و چیزهای دیگر— را ممکن می‌سازند، تا برنامه‌ی شما بتواند هر چیزِ واقعی یا انتزاعی را نمایش دهد.

فصل ۱۰ به شما می‌آموزد چگونه با فایل‌ها کار کنید و مدیریتِ خطاها را برای جلوگیری از خراب شدنِ ناگهانیِ برنامه به عهده بگیرید. داده‌ها را پیش از بستن برنامه ذخیره و پس از اجرای مجددِ آن بازیابی می‌کنید. همچنین، در موردِ استثناها در پایتون خواهید آموخت. مدیریتِ استثنا به شما امکان می‌دهد که خطاها را پیش‌بینی کنید تا برنامه‌تان بدونِ دردسرْ خطاها را مدیریت کند.

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

1.1 - آغاز به کار

در این فصل، اولین برنامه‌ی پایتونِ خود، hello_wotrld.py، را می‌نویسید. ابتدا، باید بررسی کنید که آیا نسخه‌ی اخیرِ پایتون روی رایانه‌ی شما نصب شده یا نه. اگر این‌طور نباشد، آن را نصب می‌کنید. همچنین، یک ویرایشگرِ متن برای کار کردن با برنامه‌های پایتون نصب خواهید کرد. ویرایشگرهای متنْ کدِ پایتون را تشخیص می‌دهند و همان‌طور که برنامه را می‌نویسید، بخش‌های مختلف را هایلایت و رنگ‌آمیزی می‌کنند. این به شما کمک می‌کند تا ساختارِ برنامه‌تان را راحت‌تر درک کنید.

1.1.1 - برپا کردنِ محیطِ برنامه‌نویسی

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

نسخه‌های پایتون

هر زبانِ برنامه‌نویسی با ظهورِ فکرهای نو و فناوری‌های جدید تحول می‌یابد. توسعه‌دهندگانِ پایتون هم به طورِ مستمر این زبان را پرتوان‌تر کرده‌اند و آن را به سمتِ همه‌منظوره شدنِ هرچه بیشتر سوق داده‌اند. آخرین نسخه‌ی پایتون هنگامِ نوشتنِ این کتاب ۳.۷ بوده، ولی تمامِ برنامه‌های کتاب روی نسخه‌ی ۳.۶ و بعد از آن اجرا خواهند شد. در این بخش متوجه می‌شوید که آیا پایتون روی سیستمِ شما نصب شده و آیا به نصبِ نسخه‌ی جدیدتری نیاز دارید یا نه. پیوستِ آ راهنمای مفصلی است برای نصبِ آخرین نسخه‌ی پایتون بر روی هر کدام از سیستم‌عامل‌های اصلی. برخی پروژه‌های قدیمیِ پایتون هنوز از پایتونِ ۲ استفاده می‌کنند، اما شما از نسخه‌ی ۳ استفاده کنید. اگر پایتونِ ۲ روی سیستم‌تان نصب شده، احتمالاً‌ برای پشتیبانی از برخی برنامه‌های قدیمی است که سیستم‌تان نیاز دارد. ما نسخه‌ی ۲ را همان‌گونه که هست رها می‌کنیم و مطمئن می‌شویم که نسخه‌ی جدیدتری برای کار کردن نصب می‌کنید.

اجرای یک قطعه‌کد پایتون

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

1
2
>>> print("Hello Python interpreter!")
Hello Python interpreter!

نشان »> نشان می‌دهد که شما در حال کار با ترمینال هستید، متن پررنگ‌شده همان کد شماست که باید آن را بنویسید و با فشردن دکمه‌ی ENTER اجرا کنید. بیشتر مثال‌های این کتاب برنامه‌های کوچک و جامعی هستند که آن‌ها را به جای ترمینال، به کمک ویرایشگر متنی خود اجرا خواهید کرد، زیرا بیشتر کدهای خود را در ویرایشگر متنی می‌نویسید. امّا گاهی مفاهیمِ پایه‌ای در قالبِ تعدادی قطعه‌کدنمایش داده می‌شوند که در یک نشست ترمینال پایتون اجرا می‌شوند تا مفاهیمِ خاص بهینه‌تر ارائه شوند. هر گاه مشابه خط ۱در یک لیستِ کد، سه علامت بزرگتری(«<) دیدید بدانید که کدها و خروجی‌ آن در نشست ترمینال اجرا شده‌اند. به زودی کدنویسی را در مفسّر پایتون نصب‌شده بر سیستم شما امتحان می‌کنیم. همچنین از یک ویرایشگر متنی برای ساختن یک برنامه ساده به نام Hello World! بهره خواهیم گرفت. نوشتن چنین برنامه‌ای به یک اصل برای یادگیری برنامه‌نویسی تبدیل شده است. سنّتی قدیمی در دنیای برنامه‌نویسی وجود دارد که می‌گوید اگر در اوّلین برنامه در زبان برنامه‌نویسی جدید، پیامِ Hello World! را روی صفحه چاپ کنید، برای شما خوش‌یمن خواهد بود. چنین برنامه‌ی ساده‌ای هدفی بسیار جدی را دنبال می‌کند: اگر این برنامه بر روی سیستم شما به درستی اجرا شود، هر برنامه‌ی پایتون دیگری که بنویسید نیز به همان ترتیب و به درستی اجرا خواهد شد.

درباره‌ی ویرایشگر Sublime Text

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

Sublime Text ویرایشگری مناسب برای تازه‌کارهاست، با این حال بسیاری از برنامه‌نویسان حرفه‌ای نیز از آن استفاده می‌کنند. اگر همزمان با یادگیری پایتون به کار با این ویرایشگر عادت کنید، می‌توانید هنگامی به سمت پروژه‌های بزرگ‌تر و پیچیده‌تر می‌روید به استفاده از آن ادامه دهید.

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

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

1.1.2 - پایتون بر روی سیستم عامل های مختلف

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

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

در بخش‌هایی که در ادامه می‌آیند شما برنامه Hello World! را اجرا و هر چیزی را که به درستی کار نکرد اصلاح می‌کنید. این فرآیندِ نصب در هر سیستم عامل بررسی خواهد شد تا بتوانید محیط برنامه‌نویسی پایتونِ مناسبی برای مبتدیان داشته باشید.

پایتون در ویندوز

پایتون معمولاً به صورت پیش‌فرض همراه با سیستم عامل ویندوز نصب نمی‌شود، پس احتمالاً باید آن را نصب کنید و پس از آن به نصب Sublime Text بپردازید.

نصب کردن پایتون

ابتدا بررسی کنید که آیا پایتون بر روی سیستم شما نصب شده یا نه. با وارد کردن عبارت command در منوی استارت یا نگه‌داشتن دکمه Shift و همزمان کلیک‌راست کردن روی دسکتاپ و انتخاب Open command window here یک پنجره‌ی فرمان باز کنید. در پنجره ترمینال عبارت python را با حروف کوچک وارد کنید. اگر در پاسخْ اعلانِ پایتون را (با علامت »>) دریافت کردید، پایتون بر روی سیستم شما نصب شده است. اگر پیامی مبنی بر ناشناخته بودن فرمان python دریافت کردید، به این معنی است که پایتون نصب نشده است.

در آن صورت، یا در صورتی که نسخه پایتون فعلی شما قدیمی‌تر از ۳.۶ است، باید نصب‌کننده‌ی پایتون را برای ویندوز دریافت کنید. به آدرس https://python.org/ بروید و روی پیوند Downloads بایستید. در آن‌جا باید دکمه‌ای برای نصب آخرین نسخه‌ی پایتون ببینید. بر روی آن کلیک کنید تا دریافت نسخه‌ی درستِ نصب‌کننده برای سیستم شما به طور خودکار آغاز شود. پس از دریافت فایل آن را اجرا کنید. حتماً گزینه‌ی Add Python to PATH را انتخاب کنید زیرا تنظیم صحیح سیستم شما را ساده‌تر می‌کند. شکل ۱-۱ انتخاب این گزینه را نمایش می‌دهد.

شکل 1: مطمئن شوید که تیک Add Python to PATH را زده‌اید.

شکل 1: مطمئن شوید که تیک Add Python to PATH را زده‌اید.

اجرای پایتون در یک نشست ترمینال

یک پنجره‌ی فرمان باز کنید و عبارت python را با حروف کوچک وارد کنید. باید اعلان پایتون (»>) را مشاهده نمایید. این اعلان نشان می‌دهد ویندوز آخرین نسخه‌ی پایتون را که هم‌اکنون نصب کردید، یافته است.

C:\> python
Python 3.7.2 (v3.7.2:9a3ffc0492, Dec 23 2018, 23:09:28) [MSC v.1916 64 bit
(AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>

نکته اگر چنین خروجی‌ای یا چیزی شبیه به آن را نمی‌بینید، دستورالعمل مفصل‌تر نصب را در پیوست آ ببینید. خط زیر را در نشست پایتون خود وارد کنید و مطمئن شوید که خروجی Hello Python interpreter را می‌بینید:

>>> print("Hello Python interpreter!")
Hello Python interpreter!
>>>

هر زمان که خواستید یک قطعه‌کد پایتون را اجرا کنید، یک پنجره‌ی فرمان و یک نشست ترمینال پایتون باز کنید. برای بستن این نشست دکمه‌های CTRL+Z و سپس ENTER را فشار داده و یا فرمان ()exit را وارد نمایید.

نصب کردن Sublime Text

می‌توانید نصب‌کننده‌ی Sublime Text را از https://sublimetext.com دریافت کنید. روی پیوند download کلیک کنید و به دنبال نصب‌‌کننده‌ی ویندوز بگردید. بعد از دریافت آن را اجرا کنید و تمام گزینه‌ی پیش‌فرض‌ پیشنهادی را بپذیرید.

پایتون بر روی macOS

پایتون به صورت پیش‌فرض بر روی بسیاری از سیستم‌های macOS نصب شده امّا به احتمال زیاد نسخه‌ی آن ازرده‌خارج است و برای یادگیری شما مناسب نیست. در این بخش آخرین نسخه‌ی پایتون و پس از آن Sublime Text را نصب خواهید کرد و از تنظیم شدن صحیح آن مطمئن خواهید شد.

بررسی نصب شدن پایتون ۳

با رفتن به Applications -> Utilities -> Terminal یک پنجره‌ی ترمینال باز کنید. برای این کار همچنین می‌توانید دکمه‌های و فاصله را فشار داده، عبارت terminal را وارد کرده و ENTER را بزنید. برای فهمیدنِ نسخه‌ی پایتونی که روی سیستم شما نصب شده عبارت python را با p کوچک وارد کنید. این کار یک مفسّر پایتون در ترمینال شما باز می‌کند که می‌توانید از طریق آن دستورهای پایتون را اجرا کنید. اکنون باید خروجی‌ای شامل نسخه‌ی پایتون نصب‌شده بر روی سیستم شما و یک اعلان با علامت »> ببینید. حالا می‌توانید مانند زیر وارد کردن دستورهای پایتون را شروع کنید:

$ python
Python 2.7.15 (default, Aug 17 2018, 22:39:05)
[GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.2)] on darwin
Type "help", "copyright", "credits", or "license" for more information.
>>>

این خروجی نشان می‌دهد که در حال حاضر نسخه‌ی پیش‌فرض نصب‌شده بر روی سیستم ۲.۷.۱۵ است. پس از این که این خروجی را دیدید، با فشردن Ctrl+D یا وارد کردن ()exit از اعلان پایتون خارج شوید و به اعلان ترمینال برگردید.

برای بررسی این که آیا پایتون ۳ نصب شده است، فرمان python3 را وارد کنید. احتمالاً به خطا برخواهید خورد که به معنای وجود نداشتن پایتون ۳ در سیستم شما است. اگر در خروجی پیامی مشاهده کردید که بیانگر نصب بودن نسخه‌ی ۳.۶ یا جدیدتر پایتون بود، می‌توانید این بخش را رها کنید و به بخش «اجرای برنامه در نشست ترمینال» در صفحه‌ی *** بروید. اگر پایتون ۳ به صورت پیش‌فرض بر روی سیستم نصب نشده بود باید آن را به صورت دستی نصب کنید. توجه داشته باشید که در سراسر این کتاب هر جا دستور python را دیدید باید به جای آن از python3 استفاده کنید تا مطمئن شوید که از پایتون ۳ استفاده می‌کنید، نه نسخه‌ی ۲ آن. این دو نسخه آن‌قدر تفاوت دارند که باعث شوند شما در اجرای کدهای این کتاب به کمک پایتون ۲ دچار مشکل شوید.

اگر نسخه‌ای قدیمی‌تر از ۳.۶ مشاهده کردید دستورالعمل بخش بعد را برای نصب آخرین نسخه پی بگیرید.

نصب کردن آخرین نسخه پایتون

می‌توانید نصب‌کننده‌ی پایتون را برای سیستم خود در https://python.org بیابید. روی پیوند Download بایستید. باید دکمه‌ای را برای دانلود آخرین نسخه‌ی پایتون ببینید. روی آن کلیک کنید تا دریافت آخرین نسخه‌ی صحیح پایتون به طور خودکار آغاز شود. پس از دریافت فایل نصب‌کننده آن را اجرا کنید.

پس از پایان یافتن نصب فرمان زیر را در ترمینال وارد کنید:

$ python3 --version
Python 3.7.2

باید خروجی‌ای مشابه این ببینید که نشان می‌دهد شما آماده‌ی کار کردن با پایتون هستید. هر جا فرمان python دیدید به جای آن python3 بنویسید.

اجرا کردن پایتون در یک نشست ترمینال

اکنون می‌توانید قطعه‌کدهای پایتون را با باز کردن یک ترمینال و وارد کردن python3 اجرا کنید. خط زیر را در یک نشست ترمینال وارد کنید:

>>> print("Hello Python interpreter!")
Hello Python interpreter!
>>>

پیام شما باید مستقیماً در پنجره‌ی فعلی ترمینال چاپ شود. به خاطر داشته باشید که می‌توانید مفسّر پایتون را با فشار دادن Ctrl+D یا وارد کردن فرمان exit() ببندید.

نصب کردن Sublime Text

برای نصب ویرایشگر Sublime Text باید فایل نصب‌کننده‌ی آن را از https://sublimetext.com/ بگیرید. روی پیوند Download کلیک کنید و نصب‌کننده‌ای برای macOS پیدا کنید. پس از گرفتن، آن را باز کنید و آیکون آن را کشیده و داخل پوشه Applications قرار دهید.

پایتون در لینوکس

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

بررسی نسخه‌ی پایتون

پنجره‌ی ترمینالی را با اجرا کردن برنامه Terminal روی سیستم خود باز کنید. (در اوبونتو می‌توانید دکمه‌های CTRL-ALT-T را فشار دهید). برای دریافتنِ نسخه‌ی نصب‌شده‌ی پایتون، python3 را با p کوچک وارد کنید. درصورتی‌که پایتون نصب شده باشد این فرمان مفسّرِ آن را اجرا می‌کند. باید خروجی‌ای مانند زیر را ببینید که شامل نسخه‌ی نصب‌شده‌ی پایتون و اعلان »> است. این اعلان نقطه شروع ورود فرمان‌های پایتون است:

$ python3
Python 3.7.2 (default, Dec 27 2018, 04:01:51)
[GCC 7.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

این خروجی نشان می‌دهد که در حال حاضر نسخه‌ی ۳.۷.۲ پایتون به عنوان نسخه پیش‌فرض روی سیستم شما نصب شده است. هرگاه این خروجی را دیدید، با فشردن Ctrl+D یا وارد کردن exit() از اعلان پایتون به اعلان ترمینال برگردید. هر جای این کتاب که دستور python دیدید به جای آن python3 وارد کنید.

برای اجرای کدهای این کتاب به نسخه‌ی ۳.۶ یا جدیدتر پایتون نیاز دارید. اگر نسخه‌ای که روی سیستم شما نصب شده قدیمی‌تر از ۳.۶ است با مراجعه به پیوست آ آخرین نسخه را نصب کنید.

اجرا کردن پایتون در یک نشست ترمینال

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

>>> print("Hello Python interpreter!")
Hello Python interpreter!
>>>

پیام خروجی باید مستقیماً در پنجره‌ی فعلی ترمینال چاپ شود. به خاطر داشته باشید که می‌توانید مفسّر پایتون را با فشردن Ctrl+D یا وارد کردن فرمان ()exit ببندید.

نصب کردن Sublime Text

در لینوکس می‌توانید به کمک Ubuntu Software Center این نرم‌افزار را نصب کنید. روی آیکون Ubuntu Software کلیک کنید و Sublime Text را جستجو کنید. با کلیک روی آن، آن را نصب و اجرا کنید.

1.1.3 - اجرای یک برنامه‌ی Hello World

با نصب شدن نسخه‌ی مناسب پایتون و Sublime Text، تقریباً آماده‌اید که اوّلین برنامه‌ی پایتونی را در یک ویرایشگر متنی بنویسید و اجرا کنید. امّا پیش از پرداختن به این کار باید مطمئن شوید که در سیستم شما Sublime Text به درستی برای استفاده از نسخه‌ی درست پایتون تنظیم شده یا نه. پس از آن برنامه‌ی Hello World! را نوشته و اجرا می‌کنید.

تنظیم کردن Sublime Text برای استفاده از نسخه‌ی صحیح پایتون

اگر در سیستم شما فرمان python پایتون ۳ را فرامی‌خواند نیاز به هیچ تنظیمی ندارید و می‌توانید به بخش بعد مراجعه کنید. اما اگر از فرمان python3 استفاده می‌کنید باید Sublime Text را برای اجرای برنامه‌های خود با استفاده از نسخه‌ی درست پایتون، تنظیم کنید.

روی آیکون Sublime Text کلیک کنید یا به کمک نوار جستجوی سیستم خود آن را پیدا و اجرا کنید. به Tools→ Build System → New Build System بروید تا یک فایل تنظیمات جدید برای شما گشوده شود. هر آنچه را می‌بینید پاک کرده و این‌ها را به جای آن وارد کنید:

1
2
3
{
  "cmd": ["python3", "-u", "$file"]
}

این کد Sublime Text را وامی‌دارد تا هنگام اجرای برنامه‌های پایتون از فرمان python3 استفاده کند. در پوشه‌ی پیش‌فرضی که Sublime Text برای ذخیره کردن می‌گشاید، فایل را با عنوان Python3.sublime-build ذخیره کنید.

اجرای hello_world.py

پیش از نوشتن اولین برنامه‌ی خود پوشه‌ای به نام python_work برای پروژه‌های خود بسازید. بهترین کار این است که برای نام‌گذاری فایل‌ها و پوشه‌های خود از حروف کوچک و زیر‌خط استفاده کنید زیرا قراردادهای نامگذاری پایتون چنین پیشنهاد می‌کنند.

Sublime Text را باز کنید.از File -> Save As، یک فایل پایتون خالی به نام hello_world.py در پوشه‌ی python_work ذخیره کنید. پسوند .py به Sublime Text می‌گوید که کد شما به زبان پایتون نوشته شده و باید آن را چگونه اجرا و متن آن را به روشی کارآمد هایلایت کند.

پس از ذخیره کردن فایل خود خط زیر را در ویرایشگر متنی وارد کنید:

1
print(“Hello Python world!”)

اگر فرمان python روی سیستم شما کار کند، می‌توانید برنامه‌ی خود را با انتخاب Tools->Build در منوی برنامه یا فشردن CTRL+B ـــ (+B روی macOS) اجرا کنید. اگر مانند بخش قبل به ناچار تنظیمات پیش‌فرض برنامه را تغییر داده‌اید، گزینه‌ی Python3 را از Tools->Build System انتخاب کنید. پس از آن می‌توانید برنامه‌های خود را با فشردن CTRL+B (ــــ+B) یا از طریق Tools->Build اجرا کنید.

یک صفحه‌ی ترمینال در قسمت پایینی Sublime Text نمایان می‌شود و خروجی زیر را نمایش می‌دهد:

1
2
Hello Python world!
[Finished in 0.1s]

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

1.1.4 - اشکال‌زدایی

اگر نمی‌توانید برنامه hello_world.py را اجرا کنید، اینجا چند راه چاره برای آزمودن وجود دارد که در عین حال راهکارهای عمومی خوبی برای حل هر مشکلِ برنامه‌نویسی است:

  • اگر در برنامه‌ای خطای مشخّصی وجود داشته باشد، پایتون یک trace-back که نوعی گزارش خطاست، نمایش می‌دهد. پایتون فایل را می‌گردد و سعی می‌کند که خطا را تشخیص دهد. Trace-back را بررسی کنید؛ ممکن است سرنخی به دست دهد که چه مشکلی مانع اجرا برنامه شده است.
  • از رایانه‌ی خود دور شوید، نفسی تازه کنید و دوباره تلاش کنید. به یاد داشته باشید که دستور زبان در برنامه‌نویسی بسیار مهم است، پس حتی جا انداختن یک دونقطه، یک علامت نقل قولِ جابه‌جا یا پرانتزهای نابه‌جا می‌توانند جلوی اجرای برنامه را بگیرند. بخش‌های مرتبط این فصل از کتاب را دوباره بخواهید، کد خود را بررسی کنید و سعی کنید مشکل را بیابید.
  • از نو آغاز کنید. احتمالاً نیازی نیست که هیچ نرم‌افزاری را پاک کنید امّا ممکن است پاک کردن فایل hello_world.py و ساختن مجدد آن از اول، فایده داشته باشد.
  • از کس دیگری بخواهید که گام‌های ذکرشده در کتاب را در رایانه‌ی شما یا خود بپیماید و به دقت آن‌چه را انجام می‌دهد زیر نظر بگیرید. شاید شما یک گام کوچک را جا انداخته‌اید و کس دیگری آن را پیدا کند.
  • شخص دیگری را که پایتون می‌داند پیدا کنید و از او بخواهید که شما را راه بیندازد. اگر پرس و جو کنید احتمالاً کسی را خواهید یافت که نحوه‌ی کار با پایتون را می‌داند.
  • دستورالعمل‌های راه‌اندازی این فصل در وب‌سایت همراه این کتاب در نشانی https://nostarch.com/pythoncrashcourse23/ نیز یافت می‌شود. نسخه‌ی آنلاین این دستورالعمل‌ها ممکن است برای شما کارآمدتر باشد زیرا می‌توانید به کمک آن و به سادگی کد را کپی کنید.
  • دنبال کمک آنلاین بگردید. در پیوست پ تعدادی از این منابع مانند انجمن‌ها یا تالارهای گفتگوی آنلاین معرّفی شده‌اند که می‌توانید در آن‌ها از کسانی که قبلاً با مشکل شما سر و کار داشته‌اند کمک بخواهید.

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

پایتون روی رایانه‌های امروزی به خوبی اجرا می‌شود. مشکلات اولیه در راه‌اندازی ممکن است خسته‌کننده به نظر بیایند ولی می‌ارزد که از آن‌ها سردربیاورید. زمانی که hello_world.py را اجرا کردید، می‌توانید آموختن پایتون را آغاز کنید و برنامه‌نویسی شما جذاب‌تر و لذت‌بخش‌تر خواهد شد.

1.1.5 - اجرا کردن برنامه‌های پایتون از ترمینال

بیشتر برنامه‌هایی را که در ویرایشگر متنی می‌نویسید مستقیماً از همان‌جا اجرا خواهید کرد. با این حال گاهی اجرای برنامه‌ها از ترمینال سودمند خواهد بود. برای مثال ممکن است شما بخواهید برنامه‌ای را که قبلاً نوشته شده بدون باز کردن در ویرایشگر متنی اجرا کنید.

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

در ویندوز

در یک پنجره‌ی فرمان می‌توانید از دستور ترمینالی cd (سرواژه‌ی change directory) برای گشت و گذار در فایل‌سیستم خود استفاده کنید. فرمان dir تمام فایل‌هایی را که در پوشه فعلی وجود دارد نمایش می‌دهد.

یک پنجره‌ی ترمینال باز کنید و دستورهای زیر را برای اجرای hello_world.py وارد کنید:

1
2
3
4
5
C:\> cd Desktop\python_work
C:\Desktop\python_work> dir
hello_world.py
C:\Desktop\python_work> python hello_world.py
Hello Python world

در خط ۱از دستور cd برای رفتن به پوشه‌ی python_work که خود در پوشه‌ی Desktop قرار دارد استفاده می‌کنید. بعد از فرمان dir برای اطمینان یافتن از وجود فایل hello_world.py در این پوشه استفاده می‌کنید.(خط ۲) در ادامه با به‌کارگیری فرمان python hello_world.py فایل را اجرا می‌کنید.(خط ۴)

بیشتر برنامه‌های شما مستقیماً از ویرایشگر متنی به خوبی اجرا می‌شوند. امّا همزمان با پیچیده‌تر شدن کار، گاهی به اجرای برنامه از یک ترمینال نیاز پیدا می‌کنید.

در macOS و لینوکس

اجرای یک برنامه پایتون روی لینوکس و macOS یکسان است. می‌توانید از دستور ترمینالی cd برای گشتن در فایل‌سیستم خود در یک پنجره‌ی فرمان استفاده کنید. فرمان ls تمام فایل‌های غیرپنهان را که در پوشه فعلی وجود دارد نمایش می‌دهد.

یک پنجره‌ی ترمینال باز کنید و فرمان‌های زیر را برای اجرای hello_world.py وارد کنید:

1
2
3
4
5
~$ cd Desktop/python_work/
~/Desktop/python_work$ ls
hello_world.py
~/Desktop/python_work$ python hello_world.py
Hello Python world!

در خط ۱ از دستور cd برای رفتن به پوشه‌ی python_work که خود در پوشه‌ی Desktop قرار دارد استفاده می‌کنید. سپس از فرمان ls برای اطمینان یافتن از وجود فایل hello_world.py در این پوشه استفاده می‌کنید.(خط ۲) در ادامه با به‌کارگیری فرمان python hello_world.py فایل را اجرا می‌کنید.(خط ۴)

بسیار ساده است. شما از دستور python (یا python3) برای اجرای برنامه‌های پایتون خود استفاده می‌کنید.

1.1.6 - خودآزمایی

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

python.org

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

Hello World Typos

فایل hello_world.py را که هم‌اینک ساخته‌اید باز کنید. جایی در خط برنامه یک غلط املایی ایجاد کنید و برنامه را دوباره اجرا کنید. آیا می‌توانید غلطی املاییِ مشکل‌زا را اصلاح کنید؟ آیا پیام خطا برای شما قابل درک است؟ می‌توانید غلطی املایی درست کنید که خطایی ایجاد نکند؟ فکر می‌کنید چرا این غلط برنامه را دچار خطا نکرد؟

Infinite Skills

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

1.1.7 - خلاصه

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

در فصل بعد در مورد انواع مختلف داده‌ای خواهید آموخت که می‌توانید در برنامه‌های پایتون با آن‌ها کار کنید و از متغیّرها استفاده خواهید کرد.

1.2 - متغیّرها و انواع ساده‌ی داده‌ها

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

1.2.1 - هنگام اجرای hello_world.py در واقع چه اتّفاقی می‌افتد؟

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

1
print("Hello Python world!")

اجرای این کد چنین خروجی‌ای تولید می‌کند:

1
Hello Python world!

هنگامی که فایل hello_world.py را اجرا می‌کنید، پسوند .py مشخّص می‌کند که این فایل یک برنامه‌ی پایتون است. پس ویرایشگر متنی این فایل را از طریق مفسّر پایتون اجرا می‌کند. این مفسّر برنامه را می‌خواند و مشخّص می‌کند که هر واژه در برنامه چه معنی‌ای می‌دهد. برای مثال هنگامی که مفسّر به واژه‌ی print با دو پرانتزِ پس از آن می‌رسد، هر آن‌چه‌که داخل پرانتزها قرار دارد روی صفحه چاپ می‌کند.

همچنان که به نوشتن برنامه ادامه می‌دهید ویرایشگر شما بخش‌های مختلف برنامه را به روش‌های مختلف هایلایت و رنگ‌آمیزی می‌کند. برای مثال تشخیص می‌دهد که print() نام یک تابع است و آن را با یک رنگ نمایش می‌دهد. همچنین تشخیص می‌دهد که “Hello Python world!” کد پایتون نیست و آن را با رنگِ دیگری نمایش می‌دهد. این ویژگی برجسته‌سازی یا هایلایت نحوی نامیده می‌شود و برای شروعِ برنامه‌نویسی بسیار سودمند خواهد بود.

1.2.2 - متغیّرها

حال بیایید از یک متغیّر در hello_world.py استفاده کنیم. یک خط به ابتدای فایل خود بیفزایید و خط دوم را این‌گونه تغییر دهید:

1
2
message = "Hello Python world!"
print(message)

برنامه را اجرا کنید تا ببینید چه رخ می‌دهد. باید خروجی‌ای مشابه قبل ببینید:

1
Hello Python world!

یک متغیّر به نام message اضافه کرده‌ایم. هر متغیّر با یک مقدار در ارتباط است که اطّلاعات مرتبط با آن متغیّر را در بر دارد. در این مورد مقدارْ همان متنِ “Hello Python world!” است.

اضافه کردن یک متغیّر کارِ مفسّر پایتون را کمی سخت‌تر می‌کند. هنگامی که مفسّر خط اوّل را پردازش می‌کند متغیّر message را به متن “Hello Python world!” ارتباط می‌دهد و زمانی که به خطّ دوم رسید مقداری را که به message اختصاص یافته چاپ می‌کند.

حال با تغییر دادن hello_world.py برای چاپ پیام دیگری، این برنامه را تغییر می‌دهیم. یک خطّ خالی به فایل خود اضافه کنید و این دو خطّ جدید را اضافه کنید:

1
2
3
4
message = "Hello Python world!"
print(message)
message = "Hello Python Crash Course world!"
print(message)

اکنون با اجرای hello_world.py دو خطّ زیر را در خروجی می‌بینید:

1
2
Hello Python world!
Hello Python Crash Course world!

در برنامه‌ی خود می‌توانید مقدار یک متغیّر را هر زمانی تغییر دهید و پایتون همیشه مقدار فعلی و جدید آن را در نظر خواهد گرفت.

نام‌گذاری و به‌کار‌گیری مغیّرها

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

  • نام متغیّرها تنها می‌تواند شامل حروف، اعداد و زیر‌خط باشند. نام‌ها می‌توانند با حرف یا زیر‌خط و نه با عدد آغاز شوند. برای مثال می‌توانید یک متغیّر را message_1 بنامید امّا نام‌گذاری 1_message ممکن نیست.

  • گذاشتن فاصله در نام متغیّرها مجاز نیست امّا برای فاصله‌گذاری بین چند واژه در نام متغیّرها می‌توانید از زیر‌خط استفاده کنید. برای مثال greeting_message مجاز است امّا greeting message موجب خطا می‌شود.

  • از به‌کارگیری کلیدواژگان و نام توابع پایتون به عنوان نام متغیّر پرهیز کنید. به بیان دیگر از واژگانی که پایتون برای اهداف خاص برنامه‌نویسی در نظر گرفته، مانند print، برای نام‌گذاری استفاده نکنید. (ن.ک «کلیدواژگان و توابع داخلی پایتون» ص ***)

  • نام متغیّرها بهتر است کوتاه امّا گویا باشند. برای مثال name از n و student_name از s_n و name_length از length_of_persons_name بهتر است.

  • هنگام استفاده از حرف کوچک l (اِل) و حرف بزرگ O دقّت کنید چون ممکن است با اعداد 1 و 0 اشتباه گرفته شوند.

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

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

پرهیز از نام‌گذاری اشتباه متغیّرها

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

در اینجا عمداً برنامه‌ای می‌نویسیم که خطا ایجاد کند. در کد زیر واژه‌ی mesage که پررنگ نشان داده‌شده غلط املایی دارد:

1
2
message = "Hello Python Crash Course reader!"
print(mesage)

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

1
2
3
4
Traceback (most recent call last):
   File "hello_world.py", line 2, in <module>
print(mesage)
NameError: name 'mesage' is not defined

خروجی خط ۲ وقوع خطایی را در خطِّ دومِ فایلِ hello_world.py گزارش می‌دهد. مفسّرْ خطِ ۳ را برای کمک به یافتن سریع‌تر خطا و خط ۴ را برای بیان نوع خطایی که پیدا کرده تولید می‌کند. در این مورد مفسّر به خطای name error برخورده و اعلام می‌کند که متغیّر mesage که قرار است چاپ شود، قبلاً جایی تعریف نشده است. پایتون نمی‌تواند نامی را که به غلط ارائه شده تشخیص دهد. خطای نام‌گذاری معمولاً به این معنی است که یا مقدار متغیّر را پیش از به‌کارگیری آن تعیین نکرده‌ایم یا در وارد کردن نام متغیّر مرتکب اشتباه املایی شده‌ایم.

می‌دانیم که در این کد و در خطِ دوم حرف s را در نام متغیّر message جاانداخته‌ایم. مفسّر پایتون کد شما را از لحاظ املایی بررسی نمی‌کند امّا مطمئن می‌شود که متغیّرها همواره به درستی نوشته شده‌اند. برای مثال ببینید اگر واژه‌ی message را یک جای دیگر برنامه هم اشتباه بنویسیم چه رخ می‌دهد:

1
2
mesage = "Hello Python Crash Course reader!"
print(mesage)

اینجا برنامه به درستی اجرا می‌شود!

1
Hello Python Crash Course reader!

زبان‌های برنامه‌نویسی گرچه سخت‌گیرند امّا به درست‌ یا غلط نوشتن واژگان اهمّیّتی نمی‌دهند. در نتیجه نیازی نیست که املا و دستور زبان انگلیسی را هنگام نام‌گذاری متغیّرها و نوشتن کد در نظر بگیرید.

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

متغیّرها برچسب هستند

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

این تمایز احتمالاً در برنامه‌های آغازین شما خیلی مهم نیست امّا بهتر است آن را زودتر فرابگیرید. ممکن است جایی به رفتار غیرمنتظره‌ی یک متغیّر بربخورید و درک صحیح و دقیق چگونگی کارکرد متغیّرها به تشخیص آنچه در برنامه رخ می‌دهد کمک می‌کند.

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

خودآزمایی

برای هر تمرین یک برنامه‌ی جداگانه بنویسید. هر برنامه را با نامی ذخیره کنید که مطابق قواعد استاندارد پایتون است، از حروف کوچک و زیرخط استفاده کنید. برای مثال simple_message.py یا simple_messages.py.

Simple Message

پیامی به یک متغیّر نسبت دهید و سپس آن را چاپ کنید.

Simple Messages

پیامی به یک متغیّر نسبت دهید و آن را چاپ کنید. سپس مقدار متغیّر را به پیام جدیدی تغییر دهید و پیام جدید را چاپ کنید.

1.2.3 - رشته‌ها

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

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

1
2
"This is a string."
'This is also a string.'

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

1
2
3
'I told my friend, "Python is my favorite language!"'
"The language 'Python' is named after Monty Python, not the snake."
"One of Python's strengths is its diverse and supportive community."

اکنون چند روش استفاده از رشته‌ها را بررسی می‌کنیم.

تغییر بزرگ و کوچکی حروف در یک رشته به کمک متدها

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

1
2
name = "ada lovelace"
print(name.title())

فایل را با نام name.py ذخیره و آن را اجرا کنید. باید چنین خروجی‌ای را ببینید:

1
Ada Lovelace

در این مثال متغیّر name به رشته‌ای با حروف آغازین کوچک “ada lovelace” اشاره می‌کند. هنگام فراخوانی print، متدِ title() پس از متغیّر آمده است. متد عملی است که پایتون می‌تواند روی قطعه‌ای از داده انجام دهد. نشانه‌ی نقطه (.) پس از name در name.title() به پایتون می‌گوید که متد title() را روی متغیر name اعمال کند. پس از هر متد یک جفت پرانتز می‌آید زیرا متدها گاهی به اطّلاعات بیشتری برای انجام کار خود نیاز دارند. چنین اطّلاعاتی داخل این پرانتزها قرار داده می‌شوند. تابع title() به هیچ اطّلاعات اضافه‌ای نیاز ندارد در نتیجه پرانتزها خالی مانده‌اند.

متد title() هر کلمه را به حالت عنوان درمی‌آورد که در آن هر واژه با حروف بزرگ آغاز می‌شود. زمانی که به نام به عنوان نوعی اطّلاعات نگاه می‌کنیم، کاربرد چنین متدی آشکار می‌شود. برای مثال ممکن است از برنامه‌ی خود بخواهید تمام مقادیر ورودی مانند Ada, ADA و ada را یک نام در نظر بگیرد و همه‌ی آن‌ها را به شکل Ada نمایش دهد.

تعدادی متد سودمند دیگر هم برای تغییر وضعیّت بزرگی و کوچکی حروف وجود دارد. مثلاً می‌توانید تمام حروف یک رشته را به حروف بزرگ یا کوچک تبدیل کنید:

1
2
3
name = "Ada Lovelace"
print(name.upper())
print(name.lower())

خروجی زیر مشاهده خواهد شد:

1
2
ADA LOVELACE
ada lovelace

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

به‌کارگیری متغیّرها در رشته‌ها

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

1
2
3
4
first_name = "ada"
last_name = "lovelace"
full_name = f"{first_name} {last_name}"
print(full_name)

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

به این رشته‌ها f-string می‌گویند. f سرواژه‌ی format است، زیرا پایتون این رشته‌ها را با جایگزینیِ نامِ متغیّرهای داخلِ هر آکُلاد با مقدارِ آن، قالب‌بندی می‌کند. خروجی کد قبلی به شکل زیر است:

1
ada lovelace

با f-stringها کارهای زیادی می‌توانید انجام دهید.مثلاً می‌توانید با اطَلاعاتِ یک متغیّر پیام‌های کاملی بسازید، مانند زیر:

1
2
3
4
first_name = "ada"
last_name = "lovelace"
full_name = f"{first_name} {last_name}"
print(f"Hello, {full_name.title()}!")

در پیامی که برای تبریک گفتن به شخص ساخته می‌شود نام کامل او آورده می‌شود ۴. متد title() حروف اوّل نام کامل را بزرگ می‌کند. این کد پیامی ساده امّا با قالبی زیبا تولید می‌کند:

1
Hello, Ada Lovelace!

به جز این می‌توانید از f-stringها برای تولید یک پیام و انتساب کلّ آن به یک متغیّر استفاده کنید:

1
2
3
4
5
first_name = "ada"
last_name = "lovelace"
full_name = f"{first_name} {last_name}"
message = f"Hello, {full_name.title()}!"
print(message)

همانند کد قبلی این کد هم پیام Hello, Ada Lovelace! را نمایش می‌دهد امّا این بار با قرار دادن پیام در یک متغیّر خط ۱ فراخوانی تابع نهاییprint() را بسیار ساده‌تر کردیم. خط ۲

نکته

f-stringها اوّلین بار در نسخه‌ی ۳.۶ معرّفی شدند. اگر از نسخه‌ی ۳.۵ یا قبل از آن استفاده می‌کنید باید به جای آن از متد format() استفاده کنید. برای این کار متغیّرهای مورد نظر خود را در پرانتزهایی که پس از format می‌آیند فهرست کنید. ارجاع به هر متغیّر با یک جفت آکُلاد صورت می‌پذیرد؛ آکُلادها با مقدارهایی که در پرانتزها قرار داده‌ شده‌اند، با همان ترتیب، پر خواهند شد:

1
full_name = "{} {}".format(first_name, last_name)

افزودن فضای خالی به رشته‌ها به کمک تب و خطوط جدید

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

برای اضافه کردن تب به متن خود از ترکیب حروف \t مانند آن‌چه در خط ۱ نمایش داده شده استفاده کنید:

1
2
3
4
>>> print("Python")
Python
>>> print("\tPython")
Python

برای افزودن خط جدید به رشته‌ی خود از ترکیب حروف \n استفاده کنید:

1
2
3
4
5
>>> print("Languages:\nPython\nC\nJavaScript")
Languages:
Python
C
JavaScript

همچنین می‌توانید تب‌ها و خط‌های جدید را در یک رشته ترکیب کنید. رشته‌ی “\n\t” به پایتون می‌گوید که به خطّ جدید برو و خطّ بعدی را با یک تب شروع کن.

مثال زیر نشان می‌دهد که چگونه می‌توانید با یک رشته‌ی یک‌خطّی خروجی‌ای در چهار خط تولید کنید:

1
2
3
4
5
>>> print("Languages:\n\tPython\n\tC\n\tJavaScript")
Languages:
Python
C
JavaScript

در دو فصل آینده هنگامی که می‌خواهید با چند خط کد خطوط زیادی در خروجی تولید کنید، تب‌ها و خط‌های جدید بسیار کاربردی خواهند بود.

حذف کردن فاصله‌های خالی

در برنامه‌های شما وجود فاصله‌ی اضافی در یک رشته می‌تواند گیج‌کننده باشد. برای برنامه‌نویسان دو رشته‌ی ‘python’ و ‘python ’ تقریباً یکی هستند. امّا برای یک برنامه‌ی رایانه‌ای این‌ها دو رشته‌ی متفاوت‌اند. پایتون فاصله‌ی اضافی را در ‘python ’ تشخیص داده و تمایز آن را در نظر می‌گیرد مگر آن‌که شما جور دیگری بخواهید.

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

پایتون می‌تواند فاصله‌های خالی را در سمتِ چپ یا راستِ یک رشته تشخیص دهد. برای اطمینان یافتن از عدم وجود فاصله‌ی خالی در سمت راست یک رشته از متد rstrip() استفاده می‌کنیم:

1
2
3
4
5
6
7
>>> favorite_language = 'python '
>>> favorite_language
'python '
>>> favorite_language.rstrip()
'python'
>>> favorite_language
'python '

در پایان رشته‌ای که در خط ۱ به عنوان مقدار به favorite_language نسبت داده‌ایم یک فاصله‌ی خالی اضافی وجود دارد. وقتی در خط ۲ از پایتون می‌خواهید که این مقدار را در نشست ترمینال نمایش دهد، فاصله را در انتهای آن می‌بینید. هنگامی که در خط ۴ متد rstrip() روی متغیّر favorite_language عمل می‌کند، فاصله‌ی اضافی حذف می‌شود. با این حال این حذف شدن موقّتی است. اگر بار دیگر مقدار متغیّر favorite_language را نمایش دهید، خواهید دید که مقدار رشته همانند قبل است و فاصله‌ی اضافی همچنان وجود دارد. ۶

برای حذف دائمی فاصله از رشته باید مقدار رشته‌ی اصلاح‌شده را دوباره به نام همان متغیّر نسبت دهید:

1
2
3
4
>>> favorite_language = 'python '
>>> favorite_language = favorite_language.rstrip()
>>> favorite_language
'python'

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

همچنین می‌توانید با متد lstrip() فاصله‌های خالی یک رشته را از سمت چپ یا با متد strip() فاصله‌ها را از هر دو طرف حذف کنید:

1
2
3
4
5
6
7
>>> favorite_language = ' python '
>>> favorite_language.rstrip()
' python'
>>> favorite_language.lstrip()
'python '
>>> favorite_language.strip()
'python'

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

پرهیز از خطاهای نحوی هنگام کار با رشته‌ها

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

اینجا نحوه‌ی صحیح استفاده از علائم نقل قول تکّی و دوتایی را می‌بینیم. برنامه را به نام apostrophe.py ذخیره و آن را اجرا کنید:

1
2
message = "One of Python's strengths is its diverse community."
print(message)

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

1
One of Python's strengths is its diverse community.

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

1
2
message = 'One of Python's strengths is its diverse community.'
print(message)

چنین خروجی‌ای را خواهید دید:

1
2
3
4
File "apostrophe.py", line 1
message = 'One of Python's strengths is its diverse community.' 
                        ^ 
      SyntaxError: invalid syntax

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

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

خودآزمایی

هر کدام از این تمرین‌ها را در فایلی جداگانه با نامی مانند name_cases.py ذخیره کنید. اگر گیر کردید، کمی تامل کنید یا توصیه‌های پیوست پ را ببینید.

پیام شخصی

متغیّری برای نمایش نام یک شخص تعریف و پیامی خطاب به او چاپ کنید. پیام باید ساده باشد، مثل: “Hello Eric! would you like to learn some Python today?”

بزرگ و کوچک کردن حروف نام

متغیّری برای نمایش نام یک شخص در نظر بگیرید سپس نام او را با حروف بزرگ، کوچک و به صورت عنوان چاپ کنید.

نقل قول مشهور

نقل قولی از شخصی مشهور بیابید که مورد احترامتان باشد. نقل و قول و نام گوینده‌ی آن را چاپ کنید. پیام شما باید شامل علامت نقل قول و چیزی شبیه به این باشد: Albert Einstein once said, “A person who never made a mistake never tried anything new.”

نقل قول معروف ۲

تمرین ۲-۵ را تکرار کنید امّا این بار نام شخص معروف را با یک متغیّر نمایش دهید. سپس پیام خود را تولید کنید، آن را در یک متغیّر جدید به نام message ذخیره کنید و آن را چاپ کنید.

زدودن فاصله از نام‌ها

متغیّری برای نمایش نام یک فرد در نظر بگیرید و تعدادی فاصله‌ی خالی به ابتدا و انتهای آن اضافه کنید. از کاراکترهای ترکیبی\t و \n حداقل یک بار استفاده کنید.

یک بار نام را با حروف اضافه‌ی سر و ته آن چاپ کنید. سپس نام را با اعمال کردن توابع lstrip()، rstrip() و strip() چاپ کنید.

1.2.4 - عددها

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

اعداد صحیح

در پایتون می‌توانید اعداد صحیح را جمع (+)، تفریق (-)، ضرب (*) یا تقسیم (/) کنید.

1
2
3
4
5
6
7
8
>>> 2 + 3
5
>>> 3 - 2
1
>>> 2 * 3
6
>>> 3 / 2
1.5

پایتون در نشست ترمینال به سادگی نتیجه‌ی عملیّات را نشان می‌دهد. برای به توان رساندن از دو علامت ضرب (**) استفاده می‌کنیم:

1
2
3
4
5
6
>>> 3 ** 2
9
>>> 3 ** 3
27
>>> 10 ** 6
1000000

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

1
2
3
4
>>> 2 + 3*4
14
>>> (2 + 3) * 4
20

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

اعداد اعشاری

پایتون هر عدد ممیزدار را به عنوان ممیز شناور (float) می‌شناسد. این اصطلاح در بسیاری از زبان‌های برنامه‌نویسی به کار می‌رود زیرا به این حقیقت که ممیز می‌تواند در هر جای عدد ظاهر شود (=شناور باشد) دلالت دارد. هر زبان برنامه‌نویسی باید برای مدیریت صحیح اعداد اعشاری به دقّت طرّاحی شود تا صرف نظر از محل قرارگیری ممیّز رفتار آن عدد درست باشد.

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

1
2
3
4
5
6
7
8
>>> 0.1 + 0.1
0.2
>>> 0.2 + 0.2
0.4
>>> 2 * 0.1
0.2
>>> 2 * 0.2
0.4

امّا متوجّه باشید که گاهی در پاسخ خود ارقامی با تعداد نامشخّص در بخش اعشاری خواهید دید:

1
2
3
4
5

>>> 0.2 + 0.1
0.30000000000000004
>>> 3 * 0.1
0.30000000000000004

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

اعداد صحیح و اعشاری

هنگام تقسیمِ دو عدد صحیح و بخش‌پذیر که حاصل آن نیز عددی صحیح است، به عددی اعشاری برخواهید خورد:

1
2
>>> 4/2
2.0

همچنین اگر در هر عملیّاتی یک عدد صحیح و اعشاری را ترکیب کنید، حاصل اعشاری خواهد بود:

1
2
3
4
5
6
>>> 1 + 2.0
3.0
>>> 2 * 3.0
6.0
>>> 3.0 ** 2
9.0

پیش‌فرض پایتون در هر عملیّات عددی عدد اعشاری است، حتّی اگر خروجی عملیّات عددی صحیح باشد.

زیرخط در اعداد

هنگام نوشتن اعداد بزرگ می‌توانید به کمک زیرخط رقم‌های آن را دسته‌بندی کنید تا این اعداد خواناتر شوند:

1
>>> universe_age = 14_000_000_000

هنگام چاپ اعدادی که با زیرخط تعریف شده‌اند پایتون تنها رقم‌ها را نمایش می‌دهد:

1
2
>>> print(universe_age)
14000000000

پایتون هنگام ذخیره کردن این مقادیر زیرخط را نادیده می‌گیرد. حتّی اگر ارقام را در دسته‌های سه‌تایی قرار ندهید، باز هم مقدار تغییر نمی‌کند. برای پایتون 1000 با 1_000 یا 10_00 تفاوتی ندارد. این ویژگی در اعداد صحیح و اعشاری قابل استفاده است امّا تنها در پایتون ۳.۶ و بعد از آن در دسترس است.

انتساب چندگانه

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

برای مثال اینجا سه متغیّر x، y و z را با صفر مقدارهای اولیّه می‌کنیم:

1
>>> x, y, z = 0, 0, 0

باید نام متغیّرها و همچنین مقدار آن‌ها را با کاما از یکدیگر جدا کنید. پایتون هر مقدار را به متغیّر متناظرش منسوب خواهد کرد. تا زمانی که تعداد مقادیر با تعداد متغیّرها برابر باشد، پایتون آن‌ها را به درستی با هم جور می‌کند.

ثابت‌ها

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

1
MAX_CONNECTIONS = 5000

اگر می‌خواهید در برنامه‌ی خود با متغیّری مثل یک ثابت برخورد کنید نام آن را با تماماً با حروف بزرگ بنویسید.

خودآزمایی

عدد هشت

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

1
print(5+3)

خروجی چهار خط را به سادگی نمایش می‌دهد که در هر کدام عدد ۸ نمایش داده شده است.

عدد موردعلاقه

متغیّری برای نمایش عدد مورد علاقه‌ی خود تعریف کنید. سپس با استفاده از این متغیّر پیامی تولید و چاپ کنید که عدد مورد علاقه‌ی شما را فاش می‌کند.

1.2.5 - توضیحات

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

چگونه یک توضیح بنویسیم؟

علامت هَش (#) در پایتون نمایان‌گر یک توضیح است. مفسّر پایتون هر آن‌چه را پس از این علامت بیاید نادیده می‌گیرد. برای مثال:

1
2
# Say hello to everyone.
print("Hello Python people!")

پایتون از خط اوّل می‌گذرد و دومی را اجرا می‌کند:

1
Hello Python people!

چه‌جور توضیحی باید به برنامه اضافه کنید؟

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

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

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

خودآزمایی

اضافه کردن توضیح

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

1.2.6 - ذنِ پایتون

برنامه‌نویسان باتجربه‌ی پایتون همواره شما را به پرهیز از پیچیدگی و در پی گرفتن سادگی، هر جا که ممکن باشد، تشویق می‌کنند. فلسفه‌ی جامعه‌ی پایتون در “The Zen of Python” اثر Tom Peters شرح داده شده است. با وارد کردن import this در مفسّر پایتون خود می‌توانید این مجموعه‌ی خلاصه‌ از اصول نوشتن کد پایتون خوب را پیدا کنید. کل Zen of Python را اینجا نمایش نداده‌ایم امّا برای کمک به درک اهمّیّت آن به عنوان برنامه‌نویس تازه‌کار پایتون چند خط از آن را می‌آوریم.

1
2
3
>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.

برنامه‌نویسان پایتون این نکته را پذیرفته‌اند که کد می‌تواند زیبا و ظریف باشد. افراد با برنامه‌نویسی مسائل را حل می‌کنند. برنامه‌نویسان همواره راه‌حل‌های بهینه، خوش‌ساخت و حتّی زیبا را ستوده‌اند. همان‌طور که پایتون را فرامی‌گیرید و از آن برای نوشتن برنامه‌های بیشتری استفاده می‌کنید، ممکن است روزی کسی روی شانه‌های شما بایستد و بگوید: «عجب کد زیبایی!»

1
Simple is better than complex.

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

1
Complex is better than complicated.

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

1
Readability counts.

حتّی اگر کد شما پیچیده است تلاش کنید آن را خوانا نگه دارید. زمانی که روی پروژه‌ای با برنامه‌های پیچیده کار می‌کنید بر روی نوشتن توضیحات پرمعنی تمرکز کنید.

1
There should be one-- and preferably only one --obvious way to do it.

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

1
Now is better than never.

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

همچنان که به فصل بعدی می‌روید و پرداختن به موضوعات پیچیده‌تر را شروع می کنید این فلسفه‌ی سادگی و شفافیّت را در ذهن داشته باشید.

برنامه‌نویسان مجرّب کد شما را خواهند پسندید، با کمال میل به شما بازخورد خواهند داد و در پروژه‌های جذّاب با شما همکاری خواهند کرد.

خودآزمایی

ذن پایتون

import this را در یک ترمینال پایتون وارد کنید و باقی اصول را مرور کنید.

1.2.7 - خلاصه

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

در فصل ۳ ذخیره کردن مجموعه‌ای از اطّلاعات را در ساختمان داده‌ای به نام لیست خواهید آموخت و کار کردن با لیست و تغییر دادن اطّلاعات داخل آن را یاد خواهید گرفت.


1.3 - معرّفی لیست‌ها

در این فصل و فصل بعد ابتدا خواهید آموخت که لیست‌ها چه هستند و سپس کار کردن با اعضای آن‌ها را آغاز می‌کنید. لیست‌ها امکان ذخیره‌سازی مجموعه‌ای از اطّلاعات (چه به تعداد کم چه میلیون‌ها عدد) را در یک مکان فراهم می‌آورند. لیست‌ها یکی از قدرتمندترین ویژگی‌های پایتون هستند که در اختیار برنامه‌نویسان تازه‌کار قرار گرفته و بسیاری از مفاهیم مهم برنامه‌نویسی را به یکدیگر پیوند می‌دهند.

1.3.1 - لیست چیست؟

لیست مجموعه‌ای از عناصر با ترتیبی مشخّص است. می‌توانید لیستی شامل حروف الفبا، اعداد ۰ تا ۹ یا نام تمام افراد خانواده‌ی خود درست کنید. شما می‌توانید هرچه بخواهید درون یک لیست قرار دهید و عناصر داخل لیست الزاماً ارتباطی با یکدیگر ندارند. از آنجایی‌که هر لیست معمولاً بیش از یک عضو دارد بهتر است آن را به صورت جمع نام‌گذاری کنید، مانند letters، digits یا names.

در پایتون از کروشه ( [] ) برای نمایش یک لیست استفاده می‌شود و اعضای داخل آن با کمک کاما از یکدیگر جدا می‌شوند. اینجا مثالی از یک لیست شامل نام چند نوع دوچرخه آورده می‌شود:

1
2
bicycles = ['trek', 'cannondale', 'redline', 'specialized']
print(bicycles)

اگر از پایتون بخواهید که این لیست را چاپ کند، آن را به روش خاص خود، با کروشه، نمایش می‌دهد:

1
['trek', 'cannondale', 'redline', 'specialized']

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

دسترسی به اعضای لیست

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

برای مثال اوّلین نوع دوچرخه را از لیستِ bicycles بیرون می‌کشیم:

1
2
bicycles = ['trek', 'cannondale', 'redline', 'specialized']
print(bicycles[0])

روش کار در 2 نمایان است. زمانی که یک عنصر لیست را درخواست می‌کنیم، پایتون آن را بدون کروشه نمایش می‌دهد:

1
trek

این نتیجه‌ای است که انتظار دارید کاربران ببینند– خروجی تمیز و مرتّب.

همچنین می‌توانید متدهای مخصوص رشته‌ها در فصل ۲ را روی هر عضو لیست اعمال کنید. برای مثال می‌توان به کمک متد title() به عضو ’trek’ شکل بهتری داد:

1
2
bicycles = ['trek', 'cannondale', 'redline', 'specialized']
print(bicycles[0].title())

این مثال خروجی‌ای همانند مثال قبل تولید می‌کند، با این تفاوت که حرف اول آن را بزرگ می‌کند.

شماره‌گذاری شاخص‌ از ۰ شروع می‌شود نه ۱

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

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

مثال زیر نام دوچرخه‌ها را با شاخص‌های ۱ و ۳ درخواست می‌کند:

1
2
3
bicycles = ['trek', 'cannondale', 'redline', 'specialized']
print(bicycles[1])
print(bicycles[3])

این کد دوچرخه‌های دوم و چهارم را برمی‌گرداند:

1
2
cannondale
specialized

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

1
2
bicycles = ['trek', 'cannondale', 'redline', 'specialized']
print(bicycles[-1])

این کد مقدار ‘specialized’ را برمی‌گرداند. این روش بسیار سودمند است زیرا در بسیاری از موارد شما بدون اطّلاع از تعداد عضوهای لیست می‌خواهید به آخرین عضو دسترسی پیدا کنید. این قاعده را می‌توان به دیگر شاخص‌های منفی نیز گسترش داد: شاخص ۲- عضو دو تا مانده به آخر و ۳- سه تا مانده به آخر را برمی‌گرداند و … .

به‌کارگیری مقادیر اعضای یک لیست

می‌توانید از اعضای یک لیست به صورت جداگانه درست همانند هر متغیّر دیگری استفاده کنید. برای مثال به کمک f-string می‌توانید پیامی بر اساس یکی از عضو‌های لیست ساخت.

اکنون با بیرون کشیدن نام اوّلین دوچرخه و به کمک مقدار آن پیامی تولید می‌کنیم:

1
2
3
bicycles = ['trek', 'cannondale', 'redline', 'specialized']
message = f"My first bicycle was a {bicycles[0].title()}."
print(message)

در خط 2 جمله‌ای به کمک مقدار bicycles[0] ساخته و آن را به متغیّر message نسبت دادیم. خروجی کد جمله‌ای ساده در مورد اوّلین دوچرخه‌ی لیست است:

1
My first bicycle was a Trek.

خودآزمایی

برای کسب تجربه‌ی کار کردن مستقیم با لیست‌های پایتون برنامه‌های کوتاه زیر را بنویسید. بهتر است برای مرتب نگه‌داشتن تمرین‌های هر فصل پوشه‌ای جداگانه برای هر یک بسازید .

نام‌ها

نام چند تن از دوستان خود را در لیستی به نام names ذخیره کنید. نام هر کدام از افراد را یک به یک با فراخواندن هر عضو لیست چاپ کنید.

تبریک

تمرین ۳-۱ را تکرار کنید امّا این دفعه به جای چاپ کردن اسم‌ها به تنهایی، پیامی خطاب به آن‌ها بنویسید. متن هر پیام باید یکسان باشد امّا پیام باید برای هر کس شخصی‌شده باشد.

لیست شخصی

روش حمل و نقل مطلوب خود (موتورسیکلت، ماشین یا …) را در نظر بگیرید، لیستی شامل چند نمونه از آن‌ها درست کنید و عبارت‌هایی در مورد اعضای این لیست چاپ کنید، مثلاً “I would like to own a Honda motorcycle”.

1.3.2 - تغییر دادن، افزودن و حذف کردن عضوها

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

تغییر دادن اعضای لیست

دستور تغییر دادن یک عضو شبیه به دستور دسترسی به عضوهای لیست است. برای تغییر یک عضو باید نام لیست را به همراه شاخص عضوی که می‌خواهید تغییر دهید بنویسید و سپس مقدار جدید را به آن نسبت دهید.

برای مثال فرض کنید لیستی از انواع موتورسیکلت داریم می‌خواهیم اوّلین عضو آن که ‘honda’ است تغییر دهیم. چگونه می‌توانیم این کار را انجام دهیم؟

1
2
3
4
motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles)
motorcycles[0] = 'ducati'
print(motorcycles)

کد خط 1 لیست اولیّه و ‘honda’ را به عنوان اوّلین عضو آن تعریف می‌کند. خط 3 مقدار اوّلین عضو را به ‘ducati’ تغییر می‌دهد. خروجی تغییر اوّلین عضو و دست‌نخورده‌مانده ماندن باقی اعضا را نشان می‌دهد:

1
2
['honda', 'yamaha', 'suzuki']
['ducati', 'yamaha', 'suzuki']

نه فقط اوّلین عضو بلکه تمام اعضای یک لیست را می‌توان تغییر داد.

افزودن عضو به لیست

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

الحاق کردن اعضا به انتهای یک لیست

ساده‌ترین روش برای افزودن یک عضو الحاق کردن آن به لیست است. با این کار عضو به انتهای لیست اضافه می‌شود. با استفاده از لیستی که در مثال قبل ساختیم، یک عضو جدید به نام ‘ducati’ به انتهای آن اضافه می‌کنیم:

1
2
3
4
motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles)
motorcycles.append('ducati')
print(motorcycles)

متد append() در خط 3 ‘ducati’ را بدون اثر گذاشتن بر دیگر اعضا، به لیست اضافه می‌کند:

1
2
['honda', 'yamaha', 'suzuki']
['honda', 'yamaha', 'suzuki', 'ducati']

متد append() ساختن لیست‌ها به روش پویا را ساده می‌کند. برای مثال می‌توانید یک لیست خالی ایجاد کنید و سپس اعضا را یک به یک به آن بیفزایید. اکنون عضوهای ‘honda’، ‘yamaha’ و ‘suzuki’ را به یک لیست خالی اضافه می‌کنیم:

1
2
3
4
5
motorcycles = []
motorcycles.append('honda')
motorcycles.append('yamaha')
motorcycles.append('suzuki')
print(motorcycles)

نتیجه‌ عیناً مشابه لیست‌های مثال‌های قبلی است:

1
['honda', 'yamaha', 'suzuki']

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

افزودن عضوها داخل لیست

می‌توانید به کمک متد insert() عضو جدیدی را به هر مکانی از لیست اضافه کنید. این کار با مشخّص کردن شاخص و مقدار عنصر جدید انجام می‌شود:

1
2
3
motorcycles = ['honda', 'yamaha', 'suzuki']
motorcycles.insert(0, 'ducati')
print(motorcycles)

در این مثال کد خط 2 مقدار ‘ducati’ را به ابتدای لیست اضافه می‌کند. متد insert() در مکان صفرم فضایی خالی ایجاد می‌کند و مقدار ‘ducati’ را در آن ذخیره می‌کند. این عمل مکان تمام مقادیر دیگر را یکی به راست منتقل می‌کند:

1
['ducati', 'honda', 'yamaha', 'suzuki']

حذف کردن عضوی از لیست

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

حذف کردن یک عضو به کمک عبارت del

اگر مکان عضوی را که می‌خواهید حذف کنید از پیش می‌دانید، می‌توانید از عبارت del استفاده کنید.

1
2
3
4
motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles)
del motorcycles[0]
print(motorcycles)

کد خط 3 از del برای حذف کردن اوّلین عنصر، ‘honda’، از لیست موتورسیکلت‌ها استفاده می‌کند:

1
2
['honda', 'yamaha', 'suzuki']
['yamaha', 'suzuki']

به کمک del می‌توانید هر عضو لیست را در هر مکانی پاک کنید به شرطی که شاخص آن را بدانید. برای مثال اینجا دومین عضو لیست، ‘yahama’، را حذف می‌کنیم:

1
2
3
4
motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles)
del motorcycles[1]
print(motorcycles)

دومین موتورسیکلت از لیست حذف می‌شود:

1
2
['honda', 'yamaha', 'suzuki']
['honda', 'suzuki']

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

حذف کردن یک عنصر به کمک متد pop()

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

متد pop() آخرین عضو یک لیست را حذف می‌کند امّا اجازه می‌دهد با مقدار حذف شده پس از حذف کار کنید. عبارت pop حاصل نگاه کردن به لیست به عنوان یک پشته و برداشتن بالاترین عضو آن است. در این همانندسازیْ بالای پشته همان انتهای لیست است.

حال آخرین حذف لیست موتورسیکلت‌ها را برمی‌داریم:

1
2
3
4
5
motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles)
popped_motorcycle = motorcycles.pop()
print(motorcycles)
print(popped_motorcycle)

در خط 1 لیست motorcycles را تعریف می‌کنیم. در خط 3 مقداری را از انتهای لیست برداشته و در متغیّری به نام popped_motorcycle ذخیره می‌کنیم. در 4 لیست را چاپ می‌کنیم تا ببینیم که این عضو از آن حذف شده است یا نه. در 5 با چاپ مقدار برداشته شده اطمینان حاصل می‌کنیم که همچنان به مقدار آن دسترسی داریم.

خروجی نشان می‌دهد که مقدار ‘suzuki’ از انتهای لیست حذف شده و در متغیّری به نام popped_variable ذخیره شده است:

1
2
3
['honda', 'yamaha', 'suzuki']
['honda', 'yamaha']
suzuki

متد pop() کی سودمند است؟ فرض کنید که موتورسیکلت‌ها به ترتیبِ تاریخِ خریده شدن در لیست ذخیره شده‌اند. در این مورد می‌توانید از متد pop() جمله‌ای در مورد آخرین موتورسیکلتی که خریده‌ایم چاپ کنیم:

1
2
3
motorcycles = ['honda', 'yamaha', 'suzuki']
last_owned = motorcycles.pop()
print(f"The last motorcycle I owned was a {last_owned.title()}.")

خروج جمله‌ای ساده درباره‌ی آخرین موتورسیکلتی‌ست که صاحب شده‌ایم:

1
The last motorcycle I owned was a Suzuki.

خارج کردن عضو از هرجای لیست

می‌توانید با قرار دادن شاخص عضو در داخلِ پرانتزهای pop()، از این متد برای خارج کردن هر عضوی از هرجای لیست استفاده کنید.

1
2
3
motorcycles = ['honda', 'yamaha', 'suzuki']
first_owned = motorcycles.pop(0)
print(f"The first motorcycle I owned was a {first_owned.title()}.")

ابتدا در 2 موتور سیکلت اول را از لیست برمی‌داریم و سپس پیامی در مورد آن در 3 چاپ می‌کنیم. خروجی درباره‌ی اوّلین موتورسیکلتی که داشته‌ام جمله‌ای ساده چاپ می‌کند:

1
The first motorcycle I owned was a Honda.

به خاطر داشته باشید که هر بار از pop() برای عضوی استفاده می‌کنید، آن را برای همیشه از لیست حذف می‌کنید.

اگر در بین به‌کارگیری del و pop() مردّد هستید راه ساده‌ای برای تصمیم‌گیری وجود دارد: هرگاه دیگر به هیچ‌عنوان به یک عضو احتیاج نداشتید و خواستید آن را حذف کنید از del استفاده کنید امّا اگر خواستید از مقدار آن پس از حذف شدن بهره بگیرید متد pop() را به کاربگیرید.

حذف کردن یک عضو بر اساس مقدار

گاهی مکان عضوی را که می‌خواهید حذف کنید نمی‌دانید. اگر می‌خواهید تنها با دانستن مقدار یک عضو آن را از حذف کنید می‌توانید از متد remove() استفاده کنید.

برای مثال فرض کنید می‌خواهیم مقدار ‘ducati’ را از لیست موتورسیکلت‌ها حذف کنیم:

1
2
3
4
motorcycles = ['honda', 'yamaha', 'suzuki', 'ducati']
print(motorcycles)
motorcycles.remove('ducati')
print(motorcycles)

خط 3 از پایتون می‌خواهد تا عضوی را با مقدار ‘ducati’ یافته و آن را حذف کند:

1
2
['honda', 'yamaha', 'suzuki', 'ducati']
['honda', 'yamaha', 'suzuki']

از متد remove() برای کار کردن با مقدارهای حذف‌شده نیز می‌توانید استفاده کنید. اکنون مقدار ‘ducati’ را حذف و جمله‌ای درباره‌ی علّت حذف شدن آن چاپ کنیم:

1
2
3
4
5
6
motorcycles = ['honda', 'yamaha', 'suzuki', 'ducati']
print(motorcycles)
too_expensive = 'ducati'
motorcycles.remove(too_expensive)
print(motorcycles)
print(f"\nA {too_expensive.title()} is too expensive for me.")

پس از تعریف لیست در 1، مقدار ‘ducati’ را در 3 به متغیّری به نام too_expensive منسوب کردیم. سپس در خط 4 از پایتون خواستیم که این مقدار را به کمک این متغیّر از لیست حذف کند. در خط 6 مقدار ‘ducati’ از لیست حذف شده امّا مقدار آن به کمک متغیّر too_expensive در دسترس است و می‌توانیم به کمک آن پیامی در مورد دلیل حذف شدن ‘ducati’ از لیست چاپ کنیم:

1
2
3
['honda', 'yamaha', 'suzuki', 'ducati']
['honda', 'yamaha', 'suzuki']
A Ducati is too expensive for me.
نکته
متد remove() اوّلین مورد وقوع مقداری را که مشخّص کرده‌اید حذف می‌کند. اگر این امکان وجود دارد که یک مقدار چند بار تکرار شده باشد ، از یک حلقه برای اطمینان یافتن از حذف تمامی موارد آن مقدار استفاده کنید. چگونگی این کار را در فصل هفتم خواهید آموخت.

خودآزمایی

تمرین‌های زیر نسبت به فصل ۲ کمی پیچیده‌ترند امّا به شما این فرصت را می‌دهند تا از لیست‌ها به تمام روش‌هایی که توضیح داده شد استفاده کنید.

لیست مهمان

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

تغییر دادن لیست مهمانان

همین الان متوجه شدید که یکی از مهمان‌ها نمی‌تواند برای شام بیاید، در نتیجه به ارسال دعوت‌نامه‌های جدید نیاز پیدا کردید. باید کس دیگری را برای مهمانی پیدا کنید.

  • از برنامه تمرین ۳-۴ شروع کنید.یک تابع print() اضافه کنید و در پایان برنامه نام شخصی را که نمی‌تواند به مهمانی بیاید چاپ کنید.
  • لیست را تغییر دهید و نام کسی را که دعوت را نپذیرفته با نام مهمان جدید جایگزین کنید.
  • دسته‌ی دوم دعوت‌نامه را برای هر یک از افرادی که هنوز در لیست هستند چاپ کنید.

مهمان‌های بیشتر

میز شام جدیدی پیدا کرده‌اید و اکنون جا برای مهمان‌های بیشتری باز شده است. سه فرد جدید برای دعوت کردن بیابید.

  • از برنامه تمرین ۳-۴ یا ۳-۵ شروع کنید. یک تابع print() به انتهای برنامه بیفزایید و به همه اطّلاع دهید که میز شام بزرگتری پیدا کردید.
  • با استفاده از insert() یک مهمان جدید به ابتدای لیست بیفزایید.
  • به کمک insert() مهمانی جدید به میانه‌ی لیست اضافه کنید.
  • از append() بهره بگیرید و مهمان جدید دیگری را به انتهای لیست اضافه کنید.
  • یک دعوت‌نامه جدید برای هر یک از افراد حاضر در لیستِ خود چاپ کنید.

کوچک کردن لیست مهمان

مطّلع شده‌اید که میز شام جدید به موقع نمی‌رسد و تنها برای دو مهمان جا دارید.

  • از برنامه ۳-۶ شروع کنید خط جدیدی به آن اضافه و پیامی چاپ کنید که تنها می‌توانید دو مهمان دعوت کنید.
  • برای حذف کردن مهمان‌ها، چند بار از pop() استفاده کنید تا تنها دو مهمان در لیست باقی بماند. هر بار که نامی را از لیست حذف می‌کنید پیامی خطاب به آن فرد چاپ کنید و از او به دلیل مشکل پیش آمده عذر بخواهید.
  • پیامی خطاب به دو مهمان باقی‌مانده در لیست چاپ کنید و به آن‌ها اطّلاع دهید که همچنان دعوت هستند.
  • با استفاده از del دو نام باقیمانده را هم از لیست حذف کنید تا به یک لیست خالی برسید. لیست خود را چاپ کنید تا مطمئن شوید که در پایان برنامه‌ی خود یک لیست خالی باقیمانده است.

1.3.3 - سازماندهی یک لیست

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

مرتّب کردن دائمی یک لیست به کمک sort()

متد sort() پایتون مرتّب کردن لیست‌ها را نسبتاً ساده می‌کند. فرض کنید لیستی از خودروها داریم و می‌خواهیم ترتیب لیست را تغییر بدهیم تا آن‌ها را به ترتیب حروف الفبا ذخیره کنیم. برای این که کار راحت‌تر باشد فرض کنید تمام مقادیر لیست با حروف کوچک نوشته شده‌اند.

1
2
3
cars = ['bmw', 'audi', 'toyota', 'subaru']
cars.sort()
print(cars)

متد sort() که در 2 نمایش داده شده ترتیب لیست را به صورت دائمی عوض می‌کند. خودروها اکنون دیگر به ترتیب الفبایی هستند و نمی‌توانیم آن‌ها را به ترتیب اولیّه برگردانیم:

1
['audi', 'bmw', 'subaru', 'toyota']

برای مرتّب کردن به ترتیب عکس (از حروف بزرگ به کوچک) می‌توانید نشانوند reverse=True را به متد sort() بدهید. این مثال لیست خودروها را به ترتیب الفبایی معکوس مرتّب می‌کند:

1
2
3
cars = ['bmw', 'audi', 'toyota', 'subaru']
cars.sort(reverse=True)
print(cars)

ترتیب لیست مجدداً به طور دائمی تغییر یافته است:

1
['toyota', 'subaru', 'bmw', 'audi']

مرتّب کردن موقّت یک لیست به کمک تابع sorted()

برای حفظ کردن ترتیب اصلی لیست یا تنها نمایش دادن آن با ترتیبی مشخّص، می‌توانید از تابع sorted() استفاده کنید. این تابع کمک می‌کند که لیست را با ترتیبی خاص صرفاً نمایش دهید امّا بر ترتیب اصلی آن تاثیری نمی‌گذارد.

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

1
2
3
4
5
6
7
cars = ['bmw', 'audi', 'toyota', 'subaru']
print("Here is the original list:")
print(cars)
print("\nHere is the sorted list:")
print(sorted(cars))
print("\nHere is the original list again:")
print(cars)

ابتدا در2 لیست را با ترتیب اصلی و سپس در 4 به ترتیب حروف الفبا چاپ می‌کنیم. پس از نمایش لیست مرتّب، در ۳ نشان می‌دهیم که ترتیب اصلی لیست دست‌نخورده باقی مانده است.(6)

1
2
3
4
5
6
7
8
Here is the original list:
['bmw', 'audi', 'toyota', 'subaru']

Here is the sorted list:
['audi', 'bmw', 'subaru', 'toyota']

Here is the original list again:
['bmw', 'audi', 'toyota', 'subaru']

دقّت کنید که در 5 و پس از به‌کارگیری تابع sorted() لیست همچنان به ترتیب اصلی خود باقیمانده است. تابع sorted() نیز نشان‌وند reverse=True را برای مرتّب کردن معکوس الفبایی می‌پذیرد.

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

چاپ کردن لیست با ترتیب معکوس

برای عکس کردن ترتیب اصلی یک لیست می‌توانید از متد reverse() استفاده کنید. اگر از ابتدا لیست خودروها را به ترتیب زمانِ خریدن مرتّب کرده باشیم، می‌توانیم به سادگی لیست را به ترتیبِ زمانیِ معکوس بازآرایی کنیم:

1
2
3
4
cars = ['bmw', 'audi', 'toyota', 'subaru']
print(cars)
cars.reverse()
print(cars)

توجّه داشته باشید که reverse() اعضا را به ترتیب عکسِ حروفِ الفبا مرتّب نمی‌کند بلکه تنها به سادگی ترتیب آن‌ها را عکس می‌کند:

1
2
['bmw', 'audi', 'toyota', 'subaru']
['subaru', 'toyota', 'audi', 'bmw']

متد reverse() ترتیب لیست را به طور دائم عوض می‌کند امّا می‌توانید با اعمال دوباره‌ی آن، لیست را به ترتیب اصلی آن برگردانید.

یافتن طول یک لیست

طول یک لیست را می‌توان به سرعت به کمک تابع len() یافت. لیستِ این مثال چهار عضو دارد پس طول آن ۴ است:

1
2
3
>>> cars = ['bmw', 'audi', 'toyota', 'subaru']
>>> len(cars)
4

استفاده از len() هنگامی سودمند است که بخواهید تعیین کنید در بازی چند موجود بیگانه برای کشتن باقیمانده است، تعداد داده‌هایی که برای بصری‌سازی با مدیریت شوند کدام است یا تعداد کاربران ثبت نام کرده در وبسایت چندتاست.

نکته
استفاده از len() هنگامی سودمند است که بخواهید تعیین کنید در بازی چند موجود بیگانه برای کشتن باقیمانده است، تعداد داده‌هایی که برای بصری‌سازی با مدیریت شوند کدام است یا تعداد کاربران ثبت نام کرده در وبسایت چندتاست.

خودآزمایی

تماشای جهان

به پنج نقطه از جهان فکر کنید که دوست دارید آن‌ها از نزدیک ببینید.

  • این مکان‌ها را در لیستی ذخیره کنید. مطمئن شوید که ترتیب ذخیره الفبایی نباشد.
  • لیست را با ترتیب اصلی چاپ کنید. نگران چاپ کردن تمیز و منظم نباشید و لیست را به صورت یک لیست خام پایتون چاپ کنید.
  • از sorted() برای چاپ لیست به ترتیب الفبایی و بدون دست خوردن آن استفاده کنید.
  • نشان دهید که لیست شما همچنان با ترتیب اصلی ذخیره شده است.
  • از sorted() برای مرتّب‌سازی الفبایی معکوس بدون دست زدن به ترتیب لیست اصلی استفاده کنید.
  • نشان دهید که لیست شما همچنان با ترتیب اصلی ذخیره شده است.
  • از reverse() برای معکوس کردن ترتیب اعضای لیست استفاده کنید. آن را چاپ کنید تا نشان دهید که ترتیب عوض شده است.
  • دوباره از reverse() برای معکوس کردن لیست استفاده کنید. نشان دهید که لیست به ترتیب اصلی خود برگشته است.
  • از sort() برای مرتب کردن لیست به ترتیب الفبا استفاده کنید.آن را چاپ کنید تا نشان دهید ترتیب عوض شده است.
  • از sort() برای مرتب کردن لیست به ترتیب الفبایی معکوس استفاده کنید. آن را چاپ کنید تا نشان دهید ترتیب عوض شده است.

مهمان‌های شام

با کار بر روی یکی از تمرین‌های ۳-۴ تا ۳-۷ در صفحه‌ی ***، به کمک len() پیامی چاپ کنید که تعداد افراد دعوت‌شده را نمایش دهد.

تمام تابع‌ها

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

1.3.4 - پرهیز از خطای شاخص هنگام کار کردن با لیست‌ها

هنگامی که برای اولین بار با لیست‌ها کار می‌کنید عموماً به نوعی خطا برمی‌خورید. فرض کنید لیستی شامل سه عضو داریم و عضو چهارم آن را فرامی‌خوانیم:

1
2
motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles[3])

این مثال به خطای شاخص منجر می‌شود:

1
2
3
4
Traceback (most recent call last):
File "motorcycles.py", line 2, in <module>
print(motorcycles[3])
IndexError: list index out of range

پایتون برای یافتن عنصری با شاخص ۳ تلاش می‌کند امّا پس از جستجو عضوی با این شماره پیدا نمی‌کند. به دلیل ماهیت یکی‌جابه‌جای شماره‌گذاری در لیست‌ها این خطا متداول است. همه فکر می‌کنند که شماره‌ی سومین عضو ۳ است زیر شمارش را از ۱ شروع می‌کنند. امّا پایتون شاخص سومین عضو را ۲ می‌پندارد زیرا شمارش را از ۰ شروع می‌کند.

خطای شاخص به این معنی است که پایتون نمی‌تواند عضوی با شاخصی که معلوم کرده‌اید پیدا کند. اگر در برنامه‌تان به این خطا برخوردید شاخص را یکی جابه‌جا کنید. برنامه را مجدداً اجرا کنید و ببینید آیا نتایج درست است یا نه.

به خاطر داشته باشید که همیشه می‌توانید با به‌کارگیری شاخص ۱- به آخرین عضو لیست دسترسی پیدا کنید. این مورد همیشه کار می‌کند حتّی اگر طول لیست شما از آخرین باری که به آن دسترسی داشته‌اید تغییر کرده باشد:

1
2
motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles[-1])

شاخص ۱- همیشه آخرین عضو لیست را برمی‌گرداند که در این مورد مقدار ‘suzuki’ است:

1
suzuki

تنها زمانی این رویکرد به خطا می‌خورد که آخرین عضو یک لیست تهی را درخواست کنید:

1
2
motorcycles = []
print(motorcycles[-1])

motorcycles هیچ عضوی ندارد پس پایتون خطای شاخص دیگری برمی‌گرداند:

1
2
3
4
Traceback (most recent call last):
File "motorcyles.py", line 3, in <module>
print(motorcycles[-1])
IndexError: list index out of range
نکته
اگر خطای شاخص رخ می‌دهد و نمی‌توانید بفهمید که چگونه آن را برطرف کنید لیست خود یا تنها طول آن را چاپ کنید. لیست شما ممکن است با آن‌چه می‌پنداشتید خیلی تفاوت داشته باشد، به ویژه در مواردی که آن را به صورت پویا در برنامه مدیریت می‌کنید. دیدن لیست واقعی یا تعداد دقیق اعضای آن می‌تواند به سردرآوردن از این خطاهای منطقی کمک کند.

1.3.5 - خلاصه

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

در فصل ۴ خواهید آموخت که با اعضای یک لیست چگونه بهینه‌تر کار کنید. تنها با چند خط و با حلقه زدن و بررسی تک تک اعضای لیست، می‌توانید بهینه‌تر عمل کنید؛ حتّی اگر لیست شما شامل هزاران یا میلیون‌ها عضو باشد.


1.4 - کار کردن با لیست‌ها

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

1.4.1 - حلقه زدن در کل یک لیست

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

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

اکنون از حلقه‌ی for برای چاپ کردن تک‌تک نام‌ها در لیست شعبده‌بازها استفاده می‌کنیم:

1
2
3
magicians = ['alice', 'david', 'carolina']
for magician in magicians:
    print(magician)

در خط ۱ همانند کاری که در فصل سوم کردیم، از تعریف یک لیست شروع می‌کنیم. در خط ۲ یک حلقه‌ی for تعریف می‌کنیم. این خط از پایتون می‌خواهد تا یک نام را از لیست magicians بیرون کشیده و آن را به متغیّر magician نسبت دهد. در خط ۳ به پایتون می‌گوییم که نامی را که هم‌اکنون به magician نسبت داده چاپ کند. پایتون سپس خط‌های ۲ و ۳ را برای تمام نام‌های داخل لیست تکرار می‌کند. شاید خواندن این کد به این صورت برای شما سودمند باشد: «برای هر شعبده‌باز در لیست شعبده‌بازها، نام شعبده‌باز را چاپ کن.» در خروجی به سادگی چاپ شدن هر نام موجود در لیست را خواهیم دید:

1
2
3
alice
david
carolina

نگاهی نزدیک‌تر به حلقه‌زدن

مفهوم حلقه زدن بسیار مهم است زیرا یکی از معمول‌ترین راه‌ها برای خودکارسازی وظایف تکراری با رایانه است. به عنوان یک مثال در حلقه‌ی ساده‌ای که در magicians.py به کار بردیم، پایتون ابتدا اوّلین خط حلقه را می‌خواند:

1
for magician in magicians:

این خط به پایتون می‌گوید که اوّلین مقدار را از لیست magicians بیرون بکشد و آن را به متغیّر magician نسبت بدهد. اوّلین مقدار ‘alice’ است. پایتون سپس خط بعدی را می‌خواند:

1
print(magician)

پایتون مقدار فعلی magician را که همچنان ‘alice’ است چاپ می‌کند. از آن‌جایی که لیست مقادیر دیگری را نیز دربرمی‌گیرد، پایتون به اوّلین خط حلقه بازمی‌گردد:

1
for magician in magicians:

پایتون نام بعدی را از لیست می‌خواند و مقدار آن را به متغیّر magician منسوب می‌کند و سپس همان خط را اجرا می‌کند:

1
print(magician)

پایتون بار دیگر مقدار magician را چاپ می‌کند که الان ‘david’ است و بار دیگر کل حلقه را تا فرارسیدنِ آخرین مقدارِ درون لیست، ‘carolina’، تکرار می‌کند. چون مقدار دیگری در لیست وجود ندارد پایتون به خط بعد از حلقه می‌رود. در این مورد چون بعد از حلقه‌ی for چیزی وجود ندارد برنامه به سادگی پایان می‌یابد.

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

همچنین بدانید که هنگام نوشتن حلقه‌های for، برای متغیّر موقّت حلقه که هر مقدارِ درون لیست به آن منسوب می‌شود، می‌توانید هر اسم دلخواهی را انتخاب کنید. با این حال انتخاب کردن نامی معنادار که بیانگر یک عضو لیست باشد سودمند است. برای مثال راه مناسب برای نوشتن یک حلقه‌ی for برای لیست گربه‌ها، سگ‌ها و یک لیست عمومی از اقلام در اینجا می‌آید:

1
2
3
for cat in cats:
for dog in dogs:
for item in list_of_items:

این قراردادها در نامگذاری به شما کمک می‌کند تا اتّفاقاتی را که در داخل حلقه‌ی for برای هر عضو آن می‌افتد دنبال کنید. به‌کارگیری اسم‌های مفرد و جمع کمک می‌کند که بفهمید یک بخش کد روی یک عضو لیست کار می‌کند یا تمام آن.

1.4.2 - کارهای بیشتر در یک حلقه‌ی for

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

1
2
3
magicians = ['alice', 'david', 'carolina']
for magician in magicians:
    print(f"{magician.title()}, that was a great trick!")

تنها تفاوت این کد در خط ۱ است که در آن پیامی خطاب به هر شعبده‌باز چاپ کردیم که با نام او آغاز می‌شود. در اوّلین گام حلقه مقدار magician برابر با ‘alice’ است پس پایتون اوّلین پیام را برای او چاپ می‌کند. دومین پیام با ‘David’ و سومین مورد با ‘Carolina’ آغاز خواهد شد.

خروجی پیامی شخصی‌شده را برای هر شعبده‌بازِ داخلِ لیست نشان می‌دهد:

1
2
3
Alice, that was a great trick!
David, that was a great trick!
Carolina, that was a great trick!

همچنین می‌توانید هر تعداد خط کد که خواستید در حلقه‌ی for بنویسید. هر خط تورفته که بعد از خط for magician in magicians آمده، داخل حلقه تلقی شده و بر روی هر مقدارِ داخلِ لیست یک بار اجرا می‌شود. در نتیجه می‌توانید هر آن‌چه می‌خواهید با هر کدام از اعضای لیست انجام دهید.

اکنون خط دومی را به پیام خود اضافه می‌کنیم تا به هر شعبده‌باز بگوییم که منتظر شعبده‌ی بعدی او هستیم:

1
2
3
4
magicians = ['alice', 'david', 'carolina']
for magician in magicians:
    print(f"{magician.title()}, that was a great trick!")
    print(f"I can't wait to see your next trick, {magician.title()}.\n")

از آن‌جایی که هر دو دستور print() را با تورفتگی نوشته‌ایم، هر دو خط برای هر شعبده‌باز داخل لیست یک بار اجرا می‌شود. خط جدید (\n) در دومین دستور print() در خط ۲، پس از هر بار اجرای حلقه یک خط خالی اضافه می‌کند. این کار مجموعه‌ای از پیام‌ها را ایجاد می‌کند که با نظم برای هر عضو لیست گروه‌بندی شده‌اند:

1
2
3
4
5
6
7
8
Alice, that was a great trick!
I can't wait to see your next trick, Alice.

David, that was a great trick!
I can't wait to see your next trick, David.

Carolina, that was a great trick!
I can't wait to see your next trick, Carolina.

به حلقه‌های for خود هر تعداد خط که بخواهید می‌توانید اضافه کنید. در عمل هنگام استفاده از حلقه‌ی for عملیّات متفاوت و متعدّدی را با اعضای لیست انجام می‌دهید.

انجام دادن کاری پس از حلقه‌ی for

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

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

1
2
3
4
5
magicians = ['alice', 'david', 'carolina']
for magician in magicians:
    print(f"{magician.title()}, that was a great trick!")
    print(f"I can't wait to see your next trick, {magician.title()}.\n")
print("Thank you, everyone. That was a great magic show!")

چنانکه قبلاً دیدید دو دستور print() نخست، خطاب به هر شعبده‌باز چاپ می‌شود. امّا چون خط ۳ تورفته نیست، تنها یک بار چاپ می‌شود:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Alice, that was a great trick!
I can't wait to see your next trick, Alice.

David, that was a great trick!
I can't wait to see your next trick, David.

Carolina, that was a great trick!
I can't wait to see your next trick, Carolina.

Thank you, everyone. That was a great magic show!

هنگام پردازش اطّلاعات با یک حلقه‌ی for درمی‌یابید که روش خوبی برای خلاصه کردنِ عملیّاتی است که روی کلِّ مجموعه‌ی داده‌ها انجام می‌گیرد. برای مثال فرض کنید از یک حلقه‌ی for برای آماده‌سازی اوّلیّه‌ی یک بازی استفاده می‌کنید به نحوی که با پردازش لیستی از شخصیت‌ها، آن را یکی یکی روی صفحه نمایش می‌دهید. ممکن است پس از حلقه کدی اضافه کنید که پس از نمایشِ تمامِ شخصیت‌های بازی دکمه‌ی Play Now را نشان دهد.

1.4.3 - کارهای بیشتر در یک حلقه‌ی for

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

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

اکنون برخی از معمول‌ترین خطاهای تورفتگی را بررسی می‌کنیم.

فراموش کردن تورفتگی

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

1
2
3
magicians = ['alice', 'david', 'carolina']
for magician in magicians:
    print(magician)

فراخوانی print() در خط ۳ باید تورفته باشد امّا نیست. هنگامی که پایتون به دنبال خطی تورفته باشد امّا آن را نیابد، به شما می‌گوید که کدام خط چنین مشکلی دارد.

1
2
3
4
File "magicians.py", line 3
    print(magician)
                    ^
IndentationError: expected an indented block

این نوع از خطای تورفتگی را می‌توان با تصحیح خط یا خط‌هایی که بلافاصله بعد از عبارت for آمده‌اند برطرف کرد.

فراموش کردن تورفتگی خط‌های اضافی

گاهی حلقه‌ی شما بدون هیچ خطایی اجرا می‌شود امّا نتیجه‌ی مطلوب را نمی‌گیرید. این گاهی به این دلیل رخ می‌دهد که می‌خواهید چند وظیفه را در یک حلقه انجام دهید امّا فراموش کرده‌اید که برخی از خط‌ها را تورفته کنید.

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

1
2
3
4
magicians = ['alice', 'david', 'carolina']
for magician in magicians:
    print(f"{magician.title()}, that was a great trick!")
print(f"I can't wait to see your next trick, {magician.title()}.")

فراخوانی print() در خط ۲ باید با تورفتگی همراه باشد، امّا چون پایتون حداقل یک خطِ تورفته‌ی درست بعد از حلقه‌ی for می‌بیند، خطایی اعلام نمی‌کند. در نتیجه print() اول که خط آن تورفته است برای هر کدام از نام‌ها یک بار اجرا می‌شود. دومین print() را بدون تورفتگی نوشتیم پس تنها یک بار بعد از پایان کار حلقه اجرا می‌شود. از آن‌جایی که آخرین مقداری که به magician نسبت داده شده ‘carolina’ است، او تنها کسی است که پیام ’looking forward to the next trick’ را دریافت می‌کند:

1
2
3
4
Alice, that was a great trick!
David, that was a great trick!
Carolina, that was a great trick!
I can't wait to see your next trick, Carolina.

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

تورفتگی‌های بی‌مورد

اگر به طور تصادفی خطی را تورفته کنید بدون این که نیاز باشد پایتون در مورد آن به شما اطّلاع خواهد داد:

1
2
message = "Hello Python world!"
    print(message)

دستور print() در خط ۲ نیازی به تورفتگی ندارد چون بخشی از حلقه نیست؛ بنابراین پایتون این خطا را گزارش می‌کند:

1
2
3
4
File "hello_world.py", line 2
    print(message)
    ^
IndentationError: unexpected indent

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

تورفتگی بی‌مورد پس از حلقه

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

برای مثال اجازه دهید ببینیم با تورفته کردن خطی که حاوی پیام تشکر به همه‌ی شعبده‌بازها بود، چه اتّفاقی می‌افتد:

1
2
3
4
5
magicians = ['alice', 'david', 'carolina']
for magician in magicians:
    print(f"{magician.title()}, that was a great trick!")
    print(f"I can't wait to see your next trick, {magician.title()}.\n")
    print("Thank you everyone, that was a great magic show!")

چون که خط ۴ تورفته است، یک بار برای هر فرد داخل لیست چاپ می‌شود، چنانکه اینجا می‌بینید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Alice, that was a great trick!
I can't wait to see your next trick, Alice.

Thank you everyone, that was a great magic show!
David, that was a great trick!
I can't wait to see your next trick, David.

Thank you everyone, that was a great magic show!
Carolina, that was a great trick!
I can't wait to see your next trick, Carolina.

Thank you everyone, that was a great magic show!

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

فراموش کردن دونقطه

دونقطه‌ی پایان عبارت for به پایتون می‌گوید که خط بعدی را به عنوان آغاز حلقه در نظر بگیرد.

1
2
3
magicians = ['alice', 'david', 'carolina']
for magician in magicians
    print(magician)

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

خودآزمایی

۴-۱- پیتزاها

به دستِ کم سه نوع پیتزای دلخواه خود فکر کنید. نام آن‌ها را در لیستی ذخیره کنید و سپس به کمک حلقه‌ی for نام هرکدام را چاپ کنید.

  • حلقه را به نحوی تغییر دهید که به جای نام هر پیتزا جمله‌ای شامل نام آن چاپ شود. برای هر پیتزا باید جمله‌ای شبیه به «I like pepperoni pizza» در خروجی داشته باشید.
  • خطی به پایان برنامه‌ی خود، خارج از حلقه‌ی for، اضافه کنید که بیان می‌کند شما چه‌قدر پیتزا دوست دارید. خروجی باید از سه یا چند خط در مورد انواع پیتزاهای مورد علاقه‌ی شما تشکیل شده باشد و سپس جمله‌ای مانند I really like pizza! اضافه شود.

۴-۲- حیوانات

حداقل سه حیوان مختلف با ویژگی‌های مشترک در نظر بگیرید. نام آن‌ها را در لیستی ذخیره کنید و به کمک یک حلقه‌ی for نام هر یک را چاپ کنید.

  • برنامه را به نحوی تغییر دهید که جمله‌ای درباره‌ی هر حیوان چاپ کند، مثلاً A dog would make a great pet.
  • خطّی به پایان برنامه اضافه کنید که ویژگی مشترک این حیوانات را بیان کند. مثلاً می‌توانید جمله‌ای مانند Any of these animals would make a great pet!.

1.4.4 - پرهیز از خطای تورفتگی

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

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

اکنون برخی از معمول‌ترین خطاهای تورفتگی را بررسی می‌کنیم.

فراموش کردن تورفتگی

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

1
2
3
magicians = ['alice', 'david', 'carolina']
for magician in magicians:
print(magician)

فراخوانی print() در 1 باید تورفته باشد امّا نیست. هنگامی که پایتون به دنبال خطی تورفته باشد امّا آن را نیابد، به شما می‌گوید که کدام خط چنین مشکلی دارد.

1
2
3
4
File "magicians.py", line 3
    print(magician)
    ^
IndentationError: expected an indented block

این نوع از خطای تورفتگی را می‌توان با تصحیح خط یا خط‌هایی که بلافاصله بعد از عبارت for آمده‌اند برطرف کرد.

فراموش کردن تورفتگی خط‌های اضافی

گاهی حلقه‌ی شما بدون هیچ خطایی اجرا می‌شود امّا نتیجه‌ی مطلوب را نمی‌گیرید. این گاهی به این دلیل رخ می‌دهد که می‌خواهید چند وظیفه را در یک حلقه انجام دهید امّا فراموش کرده‌اید که برخی از خط‌ها را تورفته کنید.

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

1
2
3
4
magicians = ['alice', 'david', 'carolina']
for magician in magicians:
    print(f"{magician.title()}, that was a great trick!")
print(f"I can't wait to see your next trick, {magician.title()}.\n")

فراخوانی print() در 1 باید با تورفتگی همراه باشد، امّا چون پایتون حداقل یک خطِ تورفته‌ی درست بعد از حلقه‌ی for می‌بیند، خطایی اعلام نمی‌کند. در نتیجه print() اول که خط آن تورفته است برای هر کدام از نام‌ها یک بار اجرا می‌شود. دومین print() را بدون تورفتگی نوشتیم پس تنها یک بار بعد از پایان کار حلقه اجرا می‌شود. از آن‌جایی که آخرین مقداری که به magician نسبت داده شده ‘carolina’ است، او تنها کسی است که پیام ’looking forward to the next trick’ را دریافت می‌کند:

1
2
3
4
Alice, that was a great trick!
David, that was a great trick!
Carolina, that was a great trick!
I can't wait to see your next trick, Carolina.

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

تورفتگی‌های بی‌مورد

اگر به طور تصادفی خطی را تورفته کنید بدون این که نیاز باشد پایتون در مورد آن به شما اطّلاع خواهد داد:

1
2
message = "Hello Python world!"
    print(message)

دستور print() در 1 نیازی به تورفتگی ندارد چون بخشی از حلقه نیست؛ بنابراین پایتون این خطا را گزارش می‌کند:

1
2
3
4
File "hello_world.py", line 2
    print(message)
    ^
IndentationError: unexpected indent

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

تورفتگی بی‌مورد پس از حلقه

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

برای مثال اجازه دهید ببینیم با تورفته کردن خطی که حاوی پیام تشکر به همه‌ی شعبده‌بازها بود، چه اتّفاقی می‌افتد:

1
2
3
4
5
magicians = ['alice', 'david', 'carolina']
for magician in magicians:
    print(f"{magician.title()}, that was a great trick!")
    print(f"I can't wait to see your next trick, {magician.title()}.\n")
    print("Thank you everyone, that was a great magic show!")

چون که خط 1 تورفته است، یک بار برای هر فرد داخل لیست چاپ می‌شود، چنانکه اینجا می‌بینید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Alice, that was a great trick!
I can't wait to see your next trick, Alice.

Thank you everyone, that was a great magic show!
David, that was a great trick!
I can't wait to see your next trick, David.

Thank you everyone, that was a great magic show!
Carolina, that was a great trick!
I can't wait to see your next trick, Carolina.

Thank you everyone, that was a great magic show!

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

فراموش کردن دونقطه

دونقطه‌ی پایان عبارت for به پایتون می‌گوید که خط بعدی را به عنوان آغاز حلقه در نظر بگیرد.

1
2
3
magicians = ['alice', 'david', 'carolina']
for magician in magicians
print(magician)

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

1.4.5 - ساختن لیست‌های عددی

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

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

به‌کارگیری تابع range

دستور range() در پایتون ایجاد دنباله‌ای از اعداد را آسان می‌کند. برای مثال به کمک آن می‌توانید دنباله‌ای از اعداد را مانند زیر چاپ کنید:

1
2
for value in range(1, 5):
    print(value)

هر چند به نظر می‌رسد این کد باید اعداد ۱ تا ۵ را چاپ کند، امّا عدد ۵ را چاپ نمی‌کند:

1
2
3
4
1
2
3
4

در این مثال range() تنها اعداد ۱ تا ۴ را چاپ می‌کند. این یکی دیگر از نتایج رفتار یکی‌جا‌به‌جا است که در زبان‌های برنامه‌نویسی غالباً دیده می‌شود. تابع range() باعث می‌شود پایتون از اولین مقدار داده‌شده شروع به شمارش کند و با رسیدن به دومین عددی که تعیین کرده‌اید توقف کند. از آن‌جایی که این تابع قبل از رسیدن به دومین مقدار مشخّص‌شده توقف می‌کند خروجی هرگز شامل مقدار پایانی نخواهد بود که در این مثال عدد ۵ است.

برای چاپ کردن اعداد ۱ تا ۵ از range(1,6) استفاده می‌کنید:

1
2
for value in range(1, 6):
    print(value)

این بار خروجی از ۱ آغاز و به ۵ ختم می‌شود:

1
2
3
4
5
1
2
3
4
5

اگر هنگام استفاده از range() خروجی با آن‌چه انتظار دارید متفاوت است سعی کنید مقدار پایانی را یکی جابه‌جا کنید.

همچنین می‌توانید به تابع range() تنها یک نشان‌وند بدهید آن‌گاه این تابع دنباله‌ی اعداد را از صفر آغاز می‌کند. برای مثال range(6) اعداد ۰ تا ۵ را برمی‌گرداند.

استفاده از range() برای تولید لیستی از اعداد

اگر بخواهید لیستی از اعداد تولید کنید می‌توانید به کمک تابع list()، خروجیِ تابعِ range() را مستقیماً به لیست تبدیل کنید. زمانی که تابع range() را داخل list() قرار می‌دهید خروجی لیستی از اعداد خواهد بود.

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

1
2
numbers = list(range(1, 6))
print(numbers)

و نتیجه به این شکل است:

1
[1, 2, 3, 4, 5]

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

برای مثال اینجا نحوه‌ی تولید اعداد زوج بین ۱ تا ۱۰ را می‌بینیم:

1
2
even_numbers = list(range(2, 11, 2))
print(even_numbers)

در این مثال تابع range() از مقدار ۲ شروع می‌کند و سپس ۲ تا به آن اضافه می‌کند. این تابع تا زمان رسیدن یا گذشتن از مقدار پایانی، اینجا ۱۱، اضافه کردن ۲ را ادامه می‌دهد و این خروجی را تولید می‌کند:

1
[2, 4, 6, 8, 10]

به کمک تابع range() می‌توانید تقریباً هر نوع مجموعه‌ی عددی را تولید کنید. برای مثال فرض کنید که می‌خواهید لیست اوّلین ۱۰ عددِ مربّع را که توانِ دوی اعداد ۱ تا ۱۰ است تولید کنید. در پایتون دو علامت ستاره (**) بیانگر توان است. اینجا روشی نشان داده شده که شما هم احتمالاً برای قرار دادن ۱۰ عدد مربّع در یک لیست استفاده خواهید کرد:

1
2
3
4
5
6
squares = []
for value in range(1, 11):
    square = value ** 2
    squares.append(square)

print(squares)

در 1 از یک لیست تهی شروع می‌کنیم. در 2 به پایتون می‌گوییم که به کمک تابع range() در اعداد ۱ تا ۱۰ حلقه بزند. در 3 مقدار فعلی به توان ۲ رسیده و در متغیّر square ذخیره می‌شود. در 4، مقدار جدید square به لیست squares افزوده می‌شود. در نهایت با پایان یافتن حلقه لیست اعداد مربّع در 5 چاپ می‌شود:

1
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

برای مختصرتر نوشتن این کد می‌توانیم متغیّر موقّت square را حذف و مقادیر جدید را مستقیماً به لیست بیفزاییم:

1
2
3
4
squares = []
for value in range(1,11):
    squares.append(value**2)
print(squares)

کد در خط 1 همانند خط‌های 3 و 4 فایل squares.py کار می‌کند. هر کدام از مقدارها در حلقه به توان ۲ رسیده و بلافاصله به لیستِ اعدادِ مربّع افزوده می‌شود.

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

محاسبات آماری ساده با لیستی از اعداد

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

1
2
3
4
5
6
7
>>> digits = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
>>> min(digits)
0
>>> max(digits)
9
>>> sum(digits)
45
نکته
در مثال‌های این بخش از لیست‌های کوچک اعداد بهره گرفته می‌شود تا به سادگی در صفحه جا شوند امّا اگر لیست شما شامل میلیون‌ها عدد یا بیشتر نیز باشد برنامه‌ها به همین ترتیب کار خواهند کرد.

تولید لیست به روش دربرداری

این رویکرد پیش از این برای تولیدِ لیستِ squares با سه یا چهار خط کد توضیح داده شد. روش دربرداری برای تولید لیست به شما امکان می‌دهد تا همین کار را تنها در یک خط انجام دهید. این روش حلقه‌ی for و ایجاد عناصر جدید را در یک خط ترکیب و هر عضو جدید را به طور خودکار اضافه می‌کند. ارائه کردن این روش به برنامه‌نویسانِ تازه‌کار معمولاً متداول نیست امّا اینجا آورده می‌شود چون شما به زودی و به محض آغازِ بررسیِ کدهای دیگران به آن برخواهید خورد.

مثال زیر ساختن لیستی مشابه با لیست اعداد مربّع را که پیش از این دیده‌اید نشان می‌دهد، امّا این بار با استفاده از روش دربرداری:

1
2
squares = [value**2 for value in range(1, 11)]
print(squares)

برای استفاده از این دستور، از یک نامِ گویا برای لیست، مثل squares، شروع کنید. حال یک کروشه باز کنید و برای مقادیری که می‌خواهید در لیست ذخیره کنید، عبارتی تعریف کنید. در این مثال این عبارت value**2 است که مقدارها را به توان ۲ می‌رساند. در ادامه یک حلقه‌ی for تعریف کنید تا اعدادِ ورودی به عبارتِ فوق را تولید کند. سرانجام کروشه را ببندید. حلقه‌ی for در این مثال for value in range(1,11) است که مقادیر ۱ تا ۱۰ را به عبارت value**2 می‌خوراند. توجّه کنید که در پایان حلقه‌ی for از دونقطه استفاده نشده است.

نتیجه مشابه همان لیستِ اعداد مربّعی است که پیش‌تر دیدید:

1
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

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

خودآزمایی

شمردن تا ۲۰

از یک حلقه‌ی for برای شمردن از ۱ تا ۲۰، و خودِ ۲۰، استفاده کنید.

یک میلیون

لیستی از اعدادِ ۱ تا یک میلیون بسازید سپس به کمک حلقه‌ی for آن را چاپ کنید. (اگر نمایش خروجی طولانی شد آن را با فشردن Ctrl+C یا بستن پنجره‌ی خروجی متوقّف کنید.)

جمع کردن یک میلیون عدد

لیستی از اعداد ۱ تا یک میلیون بسازید سپس با کمک min() و max() مطمئن شوید که لیست واقعاً از ۱ شروع و به یک میلیون ختم می‌شود. سپس از sum() استفاده کنید تا دریابید پایتون چه‌قدر سریع می‌توان یک میلیون عدد را با هم جمع کند.

اعداد فرد

به کمک ورودی یا نشان‌وند سوم تابع range() لیستی از اعداد فرد بین ۱ تا ۲۰ درست کنید. سپس به کمک یک حلقه‌ی for آن را چاپ کنید.

مضارب ۳

لیستی از مضرب‌های ۳ بین ۳ و ۳۰ بسازید و با یک حلقه‌ی for اعداد داخل آن را چاپ کنید.

اعداد مکعّب

به عددی که به توان ۳ رسیده باشد مکعّب گویند. برای مثال مکعّب ۲ در پایتون با 3**2 نمایش داده می‌شود. لیستی از ۱۰ عدد مکعّب نخست (که مکعّب اعداد صحیح از ۱ تا ۱۰ است)، بسازید و با یک حلقه‌ی for این اعداد را نمایش دهید.

اعداد مکعّب به روش دربرداری

به کمکِ این روش لیستی از ۱۰ عدد مکعّب نخست تولید کنید.

1.4.6 - کار کردن با بخشی از یک لیست

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

ساختن برشی از لیست

برای برش دادن یک لیست شاخص آغازی و پایانی اعضای مورد نظر خود را مشخّص می‌کنید. همانند تابع range() پایتون پیش از رسیدن به آخرین عضوِ مشخّص شده توقّف می‌کند. برای بریدن سه عضو اوّل یک لیست باید شاخص‌های ۰ تا ۳ را درخواست کنید که اعضای ۰، ۱ و ۲ را برمی‌گرداند.

مثال زیر درباره‌ی لیستی از بازیکن‌های یک تیم است:

1
2
players = ['charles', 'martina', 'michael', 'florence', 'eli']
print(players[0:3])

کد در 1 برشی از لیست را چاپ می‌کند که شامل سه بازیکن اوّل است. در خروجی ساختار لیست حفظ شده و شامل سه بازیکن است:

1
['charles', 'martina', 'michael']

می‌توانید هر زیرمجموعه‌ای را از یک لیست تولید کنید. مثلاً اگر برشی از لیست را شامل اعضای دوم، سوم و چهارم بخواهید می‌توانید آن را از شاخص ۱ آغاز و به شاخص ۴ ختم کنید:

1
2
players = ['charles', 'martina', 'michael', 'florence', 'eli']
print(players[1:4])

این بار برش از ‘martina’ شروع و با ‘florence’ تمام می‌شود:

1
['martina', 'michael', 'florence']

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

1
2
players = ['charles', 'martina', 'michael', 'florence', 'eli']
print(players[:4])

بدونِ شاخصِ آغازین، پایتون از اوّلِ لیست شروع می‌کند:

1
['charles', 'martina', 'michael', 'florence']

از همین ترکیب می‌توانید برای برش‌های انتهایی لیست استفاده کنید. برای نمونه اگر برشی شامل همه‌ی اعضا از شاخص سوم تا پایان لیست را بخواهید می‌توانید شاخص شروع را ۲ گذاشته و شاخص پایان را حذف کنید:

1
2
players = ['charles', 'martina', 'michael', 'florence', 'eli']
print(players[2:])

پایتون عضو سوم تا پایان را برمی‌گرداند:

1
['michael', 'florence', 'eli']

به این ترتیب می‌توانید صرف نظر از طول لیست، تمام اعضای بین یک شاخص خاص و پایان آن را در خروجی داشته باشید. به خاطر بیاورید که شاخص منفی می‌توانست عضوی را در فاصله‌ی مشخّصی از پایان لیست برگرداند. پس به کمک آن می‌توانید برش‌هایی از انتهای لیست تولید کنید. برای مثال اگر بخواهیم سه بازیکنِ آخرِ ترکیبِ تیم را برگردانیم از برش players[-3:] استفاده می‌کنیم:

1
2
players = ['charles', 'martina', 'michael', 'florence', 'eli']
print(players[-3:])

این مثال نام سه بازیکن آخر را چاپ می‌کند و با افزایش تعداد بازیکنانِ لیست هم بدون مشکل کار خواهد کرد.

نکته
هنگام تعریف برش می‌توانید عدد سومی هم داخل کروشه بنویسید. این عدد مشخّص می‌کند که پایتون چند عضو را در بازه‌ی مشخّص‌شده نادیده بگیرد. (مانند آن‌چه که در تابع range بود.)

حلقه زدن در یک برش

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

1
2
3
4
players = ['charles', 'martina', 'michael', 'florence', 'eli']
print("Here are the first three players on my team:")
for player in players[:3]:
    print(player.title())

پایتون در 1 به جای حلقه زدن در کل لیست، تنها در سه نام اول می‌چرخد:

1
2
3
4
Here are the first three players on my team:
Charles
Martina
Michael

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

کپی کردن یک لیست

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

برای کپی کردن یک لیست می‌توانید با حذف کردن شاخص‌های ابتدایی و پایانی ([:]) برشی از آن را تهیه کنید که شامل تمام اعضای لیست اوّلّیه است. این کار به پایتون می‌گوید که برشی از لیست تهیه کند که از عضو اول شروع و به عضو آخر ختم می‌شود و باعث ایجاد یک کپی از کل لیست می‌شود.

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

1
2
3
4
5
6
my_foods = ['pizza', 'falafel', 'carrot cake']
friend_foods = my_foods[:]
print("My favorite foods are:")
print(my_foods)
print("\nMy friend's favorite foods are:")
print(friend_foods)

در 1 لیستی از غذاهایی را که دوست داریم به نام my_foods درست کرده‌ایم. در 2 لیست جدیدی به نام friend_foods می‌سازیم. با درخواست برشی از my_foods بدون مشخّص کردن هیچ شاخصی و ذخیره کردن آن در friend_foods، یک کپی از my_foods ایجاد می‌کنیم. اگر هر دو لیست را چاپ کنیم می‌بینیم که هر دو دربرگیرنده‌ی غذاهای مشابهی هستند:

1
2
3
4
My favorite foods are:
['pizza', 'falafel', 'carrot cake']
My friend's favorite foods are:
['pizza', 'falafel', 'carrot cake']

برای این که ثابت کنیم واقعاً دو لیست جداگانه داریم یک غذای جدید به هر یک از لیست‌ها اضافه می‌کنیم تا نشان دهیم که هر لیست غذاهای مورد علاقه‌ی یک نفر را به صورت جداگانه نگه می‌دارد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
my_foods = ['pizza', 'falafel', 'carrot cake']
friend_foods = my_foods[:]

my_foods.append('cannoli')
friend_foods.append('ice cream')

print("My favorite foods are:")
print(my_foods)

print("\nMy friend's favorite foods are:")
print(friend_foods)

در 1 همانند قبل، اعضای اوّلیّه‌ی داخل my_foods را در لیست جدید friend_foods کپی کردیم. در ادامه یک غذای جدید به هر لیست اضافه می‌کنیم: در 2 ‘cannoli’ را به my_foods و در 3 ‘ice cream’ را به friend_foods افزودیم. سپس هر دو لیست را چاپ می‌کنیم تا ببینیم هر کدام از این غذاها به لیست مناسب اضافه شده‌اند یا نه:

1
2
3
4
5
My favorite foods are:
['pizza', 'falafel', 'carrot cake', 'cannoli']

My friend's favorite foods are:
['pizza', 'falafel', 'carrot cake', 'ice cream']

خروجی نشان می‌دهد که ‘cannoli’ اکنون در لیست غذاهای مورد علاقه‌ی ما وجود دارد امّا ‘ice cream’ اینچنین نیست. در 5 می‌بینیم که ‘ice cream’ در لیست دوستمان هست امّا ‘cannoli’ نیست. اگر صرفاً و به سادگی متغیّر friend_foods را برابر با my_foods قرار می‌دادیم، منجر به ایجاد دو لیست جداگانه نمی‌شد. برای مثال اینجا می‌بینیم اگر لیستی را بدون استفاده از برش کپی کنیم چه اتّفاقی می‌افتد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
my_foods = ['pizza', 'falafel', 'carrot cake']

# This doesn't work:
friend_foods = my_foods
my_foods.append('cannoli')
friend_foods.append('ice cream')

print("My favorite foods are:")
print(my_foods)

print("\nMy friend's favorite foods are:")
print(friend_foods)

در 1 به جای کپی کردن my_foods در friend_foods، صرفاً این دو متغیّر را برابر با یکدیگر قرار دادیم. این دستور به پایتون می‌گوید که متغیّر جدید friend_foods را به لیستی ارتباط دهد که خود قبلاً با my_foods مرتبط بوده است. بنابراین الان هر دو متغیّر به یک لیست مشابه اشاره می‌کنند. در نتیجه اگر ‘cannoli’ را به my_foods اضافه کنید این عضو در friend_foods نیز ظاهر خواهد شد. به طور مشابه اگر ‘ice cream’ را فقط به friend_foods اضافه کنید در هر دو لیست دیده خواهد شد.

خروجی نشان می‌دهد که هر دو لیست یکسان هستند و این چیزی نیست که ما می‌خواستیم:

1
2
3
4
5
My favorite foods are:
['pizza', 'falafel', 'carrot cake', 'cannoli', 'ice cream']

My friend's favorite foods are:
['pizza', 'falafel', 'carrot cake', 'cannoli', 'ice cream']
نکته
الان نگران جزئیات این مثال نباشید. اساساً هر جا هنگام کپی کردنِ یک لیست رفتاری غیرمنتظره دیدید مطمئن شوید که همانند مثال اوّل، از یک برش استفاده کرده‌اید.

خودآزمایی

برش‌ها

به یکی از برنامه‌هایی که در این فصل نوشته‌اید چند خط اضافه کنید تا کارهای زیر را انجام دهد:

  • پیام The first three items in the list are: و سپس به کمک یک برش سه عضو اوّل لیست برنامه را چاپ کند.
  • پیام Three items from the middle of the list are: و سپس به کمک یک برش سه عضو میانی لیست برنامه را چاپ کند.
  • پیام The last three items in the list are: و سپس به کمک یک برش سه عضو آخر لیست برنامه را چاپ کند.

پیتزاهای من، پیتزاهای تو

با شروع از برنامه‌ی تمرین ۴-۱ یک کپی از لیست پیتزاها ایجاد کنید و آن را friend_pizzas بنامید. سپس این کارها را انجام دهید:

  • یک پیتزای جدید به لیست اصلی اضافه کنید. پیام My favorite pizzas are: و در ادامه به کمک یک حلقه‌ی for لیست اول را چاپ کنید. پیام My friend's pizzas are: و با یک حلقه‌ی for لیست دوم را چاپ کنید. مطمئن شوید که هر پیتزا در لیست مناسب ذخیره شده باشد.

حلقه‌های بیشتر

در تمام نسخه‌های foods.py برای صرفه‌جویی در فضا از به‌کارگیری حلقه‌ی for برای چاپ لیست پرهیز شده است. یکی از نسخه‌ها را انتخاب کنید و دو حلقه‌ی for برای چاپ کردن هر کدام از لیست‌ها به آن بیفزایید.

1.4.7 - چندتایی‌ها

لیست‌ها برای ذخیره‌سازی مجموعه‌ای از اعضا مناسب است که ممکن است در طول برنامه تغییر کنند. قابلیّت تغییر دادن لیست‌ها به ویژه زمانی مهم است که بخواهید با فهرستی از کاربران یک وب‌سایت یا شخصیت‌های یک بازی کار کنید. با این حال گاهی به ساختن لیستی نیاز دارید که نباید تغییر کند. چندتایی‌ها چنین امکانی به شما می‌دهند. پایتون به مقادیری که مقدارشان قابل تغییر نیست تغییرناپذیر می‌گوید و لیستِ تغییرناپذیرْ چندتایی نامیده می‌شود.

تعریف کردن یک چندتایی

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

برای مثال اگر مستطیلی داشته باشیم که اندازه‌ی آن همیشه باید ثابت باشد، برای اطمینان از تغییر نیافتن اندازه‌ها می‌توانیم ابعاد آن را در یک چندتایی قرار دهیم:

1
2
3
dimensions = (200, 50)
print(dimensions[0])
print(dimensions[1])

چندتایی dimensions را با استفاده از پرانتز به جای کروشه در 1 تعریف کرده‌ایم. در 2 به کمک همان روشی که برای دسترسی به اعضای لیست استفاده می‌کردیم، هر عضو چندتایی را جداگانه چاپ می‌کنیم:

1
2
200
50

حال ببینیم اگر سعی کنیم یکی از عناصر داخل چندتایی را تغییر دهیم چه اتّفاقی می‌افتد:

1
2
dimensions = (200, 50)
dimensions[0] = 250

کد در 1 سعی می‌کند مقدار اولّین بُعد را تغییر دهد امّا پایتون خطایی مربوط به نوع برمی‌گرداند. دلیل این مساله این است که سعی کرده‌ایم یک چندتایی را تغییر دهیم و این کار برای این نوع مجاز نیست، پایتون می‌گوید که نمی‌توانیم مقدار جدیدی به یکی از اعضای داخل چندتایی نسبت دهیم:

1
2
3
4
Traceback (most recent call last):
  File "dimensions.py", line 2, in <module>
    dimensions[0] = 250
TypeError: 'tuple' object does not support item assignment

این کار به نفع ماست چون انتظار داریم هر گاه خطی در برنامه تلاش کرد ابعاد مستطیل را عوض کند، پایتون اعلام خطا کند.

نکته

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

1
my_t = (3,)

معمولاً ساختن چندتایی با یک عضو بی‌معناست امّا در مواردی که آن‌ها را به طور خودکار تولید می‌کنیم ممکن است چنین حالتی رخ دهد.

حلقه زدن در تمام مقادیر یک چندتایی

درست همانند کاری که در لیست‌ها انجام می‌دادید، می‌توانید به کمک یک حلقه‌ی for در تمام اعضای یک چندتایی بچرخید:

1
2
3
dimensions = (200, 50)
for dimension in dimensions:
    print(dimension)

پایتون تمام اعضای چندتایی را برمی‌گرداند، درست مانند لیست‌ها:

1
2
200
50

رونویسی یک چندتایی

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

1
2
3
4
5
6
7
8
9
dimensions = (200, 50)
print("Original dimensions:")
for dimension in dimensions:
    print(dimension)

dimensions = (400, 100)
print("\nModified dimensions:")
for dimension in dimensions:
    print(dimension)

خط‌هایی که از 1 شروع می‌شوند چندتاییِ اوّلیّه را تعریف و آن را چاپ می‌کنند. در 2 یک چندتاییِ جدید به متغیّر dimensions نسبت می‌دهیم. در 3 ابعاد جدید را چاپ می‌کنیم. پایتون این بار خطایی اعلام نمی‌کند زیرا بازنشانی یک متغیّر مجاز است:

1
2
3
4
5
6
7
Original dimensions:
200
50

Modified dimensions:
400
100

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

خودآزمایی

بوفه

یک رستورانِ بوفه‌ای تنها پنج غذای اصلی ارائه می‌کند. پنج غذای ساده را در نظر بگیرید و نام آن‌ها را در یک چندتایی ذخیره کنید.

  • با یک حلقه‌ی for غذاهایی را که رستوران عرضه می‌کند چاپ کنید.
  • سعی کنید یکی از مقادیر چندتایی را تغییر دهید و مطمئن شوید پایتون ایجاد تغییرات را رد می‌کند.
  • رستوران فهرست غذاها را تغییر می‌دهد و دو غذا را جایگزین می‌کند. یک خط برای بازنویسی چندتایی اضافه کنید و سپس به کمک حلقه‌ی for تمام اعضای فهرستِ بازنگری‌شده را چاپ کنید.

1.4.8 - شکل‌دهی کدها

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

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

شیوه‌نامه

هرگاه کسی بخواهد تغییری در زبان پایتون ایجاد کند یک پیشنهادِ پیشرفتِ پایتون (PEP) می‌نویسد. یکی از قدیمی‌ترین PEPها PEP 8 است که به برنامه‌نویسان پایتون یاد می‌دهد به چه شیوه‌ای کد خود را شکل دهند. PEP 8 نسبتاً طولانی است ولی بیشتر آن مربوط به کدهایی است که از آن‌چه شما تا الان دیده‌اید پیچیده‌ترند.

شیوه‌نامه‌ی پایتون بر اساس این تفکر نوشته شده است که کد بیشتر از آنکه نوشته شود خوانده می‌شود. شما کد را یک بار می‌نویسید و سپس هنگام اشکال‌زدایی خواندن آن را شروع می‌کنید. هنگام افزودنِ یک ویژگیِ جدید به برنامه‌ی خود زمان بیشتری صرف خواندن کد می‌کنید. هنگامی که کد خود را با دیگر برنامه‌نویسان به اشتراک می‌گذارید آن‌ها نیز کد شما را خواهند خواند.

اگر بین نوشتن کدی که راحت‌تر نوشته شود و کدی که راحت‌تر خوانده شود حقّ انتخاب داشته باشید، برنامه‌نویسان پایتون تقریباً همیشه شما را به انتخاب گزینه‌ی دوم تشویق می‌کنند. راهنماهای زیر به شما کمک می‌کنند که از ابتدا کدی شفاف‌تر و واضح‌تر بنویسید.

تورفتگی

PEP 8 به شما توصیه می‌کند برای ایجاد تورفتگی از ۴ فاصله استفاده کنید. استفاده از ۴ فاصله خوانایی برنامه را افزایش می‌دهد و در عین حال امکان ایجاد تورفتگی‌های بیشتر را در هر خط ایجاد می‌کند.

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

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

طول خط

بسیاری از برنامه‌نویسان پایتون پیشنهاد می‌کنند که طول هر خط کمتر از ۸۰ حرف باشد. از لحاظ تاریخی این توصیه به این دلیل ایجاد شده که بیشتر رایانه‌ها تنها ۷۹ کاراکتر در هر خط ترمینال جا می‌دادند. در حال حاضر می‌توان حرف‌های بسیار بیشتری در یک خط نمایشگر جا داد امّا دلایل دیگری برای وفادار ماندن به استاندارد خط ۷۹حرفی وجود دارد. برنامه‌نویسان حرفه‌ای گاهی چند فایل را همزمان روی صفحه‌ی خود باز می‌کنند و طول استاندارد خط به آن‌ها اجازه می‌دهد که در دو یا سه فایلی که کنار هم هستند، تمام خط‌ها را ببینند. PEP 8 همچنین توصیه می‌کند طول هر خطِ توضیحی را حداکثر ۷۲ حرف بگیرید زیرا ابزارهایی که برای پروژه‌های بزرگتر به طور خودکار مستندات را تولید می‌کنند در ابتدای هر توضیح چند حرف دیگر اضافه می‌کنند.

راهنمایی‌های PEP 8 در مورد طول خط وحیِ مُنزَل نیست و برخی از گروه‌ها حد ۹۹حرفی را می‌پسندند. در زمان یادگیری لازم نیست خیلی نگران طول خط باشید امّا حواستان باشد که افرادی که به صورت مشارکتی کار می‌کنند معمولاً همیشه از راهنماهای PEP 8 پیروی می‌کنند. بیشترِ ویرایشگرها برای شما یک سرنخ بصری ایجاد می‌کنند که معمولاً یک خط عمودی روی صفحه است و این حدود را به شما نشان می‌دهند.

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

خط‌های خالی

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

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

دیگر شیوه‌نامه‌ها

PEP 8 توصیه‌های متعدّد دیگر هم برای شکل‌دهی دارد امّا بیشتر آن‌ها در برنامه‌هایی کاربرد دارند که پیچیده‌تر از برنامه‌های فعلی شما هستند. همزمان با آموختن ساختارهای پیچیده‌تر پایتون بخش‌های مربوط در راهنماهای PEP 8 را با شما به اشتراک خواهم گذاشت.

خودآزمایی

PEP 8

به شیوه‌نامه‌ی اصلی PEP 8 در آدرس https://python.org/dev/peps/pep-0008/ نگاهی بیندازید. از بیشترِ آن استفاده نخواهید کرد امّا شاید مرور آن جالب باشد.

مرور کد

سه نمونه از برنامه‌هایی را که در این فصل نوشته‌اید انتخاب کنید و آن‌ها را برای تطبیق با PEP 8تغییر دهید:

  • از چهار فاصله برای هر سطح تورفتگی استفاده کنید. ویرایشگر متنی خود را تنظیم کنید تا با هر بار فشردن دکمه‌ی TAB چهار فاصله وارد کند. (برای مشاهده‌ی دستورالعمل انجام این کار پیوست ب را ببینید.)
  • کمتر از ۸۰ حرف در هر خط استفاده کنید و ویرایشگر خود را برای نمایش یک راهنمای عمودی در موقعیّت ۸۰امین حرف تنظیم کنید.
  • در برنامه‌های خود بیش از اندازه از خط خالی استفاده نکنید.

1.4.9 - خلاصه

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

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


1.5 - گزاره‌های شرطی

1.5.1 - یک مثال ساده

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

در این فصل نوشتن آزمون‌های شرطی را برای بررسی هر نوع شرط می‌آموزید. نوشتن گزاره‌های ساده‌ی if و همچنین ساختن دنباله‌ای پیچیده از گزاره‌های if را فراخواهید گرفت تا تشخیص دهید که شرایطی که به طور دقیق تعیین کرده‌اید چه‌زمانی برآورده می‌شوند. سپس همین مفهوم را به لیست‌ها تعمیم می‌دهید و قادر خواهید بود حلقه‌ی for‌ای بنویسید که بیشتر اعضای یک لیست را به یک روش و تعداد مشخّص دیگری را به روشی دیگر مدیریّت کند.

یک مثال ساده

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

1
2
3
4
5
6
7
cars = ['audi', 'bmw', 'subaru', 'toyota']

for car in cars:
    if car == 'bmw':
        print(car.upper())
    else:
        print(car.title())

حلقه‌ی این مثال در 1 ابتدا بررسی می‌کند که آیا نام فعلی ‘car’ی ‘bmw’ است یا نه؟ اگر چنین باشد این نام را با حروف بزرگ چاپ می‌کند. اگر مقدار car چیزی جز این بود، آن را در حالت عنوان چاپ می‌کند:

1
2
3
4
Audi
BMW
Subaru
Toyota

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

1.5.2 - آزمون‌های شرطی

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

بررسیِ تساوی

بیشتر آزمون‌های شرطی مقدارِ کنونیِ یک متغیّر را با یک مقدار مشخّصِ موردِ نظر مقایسه می‌کنند. ساده‌ترین آزمون شرطیْ تساویِ مقدارِ یک متغیّر را با یک مقدار دلخواه بررسی می‌کند:

1
2
3
>>> car = 'bmw'
>>> car == 'bmw'
True

خط ۱ مقدار car را به کمک یک علامت تساوی برابر با ‘bmw’ قرار می‌دهد، چنانکه پیش از این در موارد بسیاری آن را دیده‌اید. خط ۲ به کمک دو علامت تساوی (==) بررسی می‌کند آیا مقدار car برابر با ‘bmw’ است یا نه؟ عملگر برابری در صورت یکی بودن مقادیرِ دو طرف خود، True و در غیر این صورت False برمی‌گرداند. مقادیرِ این مثال به هم می‌خورند پس پایتون True برمی‌گرداند.

اگر مقدار car چیزی جز ‘bmw’ باشد، این آزمون False برمی‌گرداند:

1
2
3
>>> car = 'audi'
>>> car == 'bmw'
False

علامت تساوی تکّی در واقع یک حکم است، می‌توانید کد ۱ را به این صورت بخوانید: «مقدار car را برابر با ‘audi’ قرار بده.» از طرف دیگر علامت تساوی دوتایی، مانند ۲، پرسشی مطرح می‌کند: «آیا مقدار car برابر با ‘bmw’ است؟» بیشترِ زبان‌های برنامه‌نویسی از علامت‌های تساوی به همین شکل استفاده می‌کنند.

نادیده گرفتن بزرگ و کوچکی حروف هنگام بررسی تساوی

آزمودن تساوی در پایتون به بزرگی و کوچکی حروف حسّاس است. برای مثال دو کلمه با حروف یکسان امّا بزرگ و کوچکیِ متفاوت، یکسان تلقّی نمی‌شوند:

1
2
3
>>> car = 'Audi'
>>> car == 'audi'
False

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

1
2
3
>>> car = 'Audi'
>>> car.lower() == 'audi'
True

این آزمون باید صرف از شکل ‘Audi’ هم True برگرداند زیرا اکنون دیگر به بزرگ و کوچکی حروف حسّاس نیست. تابع lower() مقدار اوّلیّه‌ی ذخیره‌شده در car را تغییر نمی‌دهد، پس می‌توانید این گونه مقایسه‌ها را بدون ایجاد تغییر در مقدار متغیّر انجام دهید:

1
2
3
4
5
>>> car = 'Audi'
>>> car.lower() == 'audi'
True
>>> car
'Audi'

وب‌سایت‌ها قوانین مشخّصی مانند این را بر داده‌هایِ ورودیِ کاربران اعمال می‌کنند. برای مثال سایتی ممکن است از یک آزمون شرطی مانند این استفاده کند تا مطمئن شود کاربر یک نام کاربری واقعاً یکتا دارد، نه صرفاً نام کاربری کس دیگری با تغییری در بزرگی و کوچکی حروف. زمانی که کسی نام کاربری جدیدی را ثبت می‌کند مقدار آن به حروف کوچک تبدیل می‌شود و سپس با تمام نام‌های کاربری موجودِ تبدیل‌شده به حروف کوچک، مقایسه می‌شود. طی چنین آزمونی اگر هر ترکیبی از ‘john’ قبلاً ثبت شده باشد، نام کاربری‌ای مانند ‘John’ رد خواهد شد.

بررسی نابرابری

اگر بخواهید نابرابری دو مقدار را بررسی کنید، می‌توانید از ترکیب یک علامت تعجّب و علامت تساوی (!=) استفاده کنید. علامت تعجّب، مانند بسیاری از زبان‌های برنامه‌نویسی دیگر، بیانگر «نیست» است.

اکنون از یک عبارت if دیگر برای بررسی کاربرد عملگر نابرابری استفاده می‌کنیم. مواد درخواستی برای روی پیتزا را در متغیّری ذخیره می‌کنیم و اگر ماهی کولی سفارش نداده باشد، پیامی خطاب به او صادر می‌کنیم:

1
2
3
4
requested_topping = 'mushrooms'

if requested_topping != 'anchovies':
    print("Hold the anchovies!")

خطّ ۱ مقدار requested_topping را با مقدار ‘anchovies’ مقایسه می‌کند. اگر دو مقدار برابر نباشند پایتون True برمی‌گرداند درنتیجه کدی که در ادامه‌ی گزاره‌ی if آمده اجرا می‌کند. اگر دو مقدار برابر باشند پایتون False برمی‌گرداند و آن خط را اجرا نخواهد کرد.

چون مقدار requested_topping برابر ‘anchovies’ نیست، تابع print() اجرا می‌شود:

1
Hold the anchovies!

بیشترِ عبارت‌های شرطی‌ای که می‌نویسید برابری را بررسی می‌کنند امّا گاهی آزمودن نابرابری را موثّرتر خواهید یافت.

مقایسه‌های عددی

آزمودن مقدارهای عددی بسیار سرراست است. برای مثال کد زیر بررسی می‌کند که آیا شخص ۱۸ ساله است یا نه:

1
2
3
>>> age = 18
>>> age == 18
True

همچنین می‌توانید نابرابری دو عدد را بررسی کنید. مثلاً در کد زیر، در صورتی‌که پاسخ وارد شده درست نباشد، پیامی چاپ می‌شود:

1
2
3
4
answer = 17

if answer != 42:
    print("That is not the correct answer. Please try again!")

آزمون شرطی ۱ قبول می‌شود زیرا مقدار answer (۱۷) برابر با ۴۲ نیست و به همین دلیل کد تورفته اجرا می‌شود:

1
That is not the correct answer. Please try again!

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

1
2
3
4
5
6
7
8
9
>>> age = 19
>>> age < 21
True
>>> age <= 21
True
>>> age > 21
False
>>> age >= 21
False

هر مقایسه‌ی ریاضی می‌تواند به عنوان بخشی از یک عبارت if به کار گرفته شود که به شما برای تشخیص شرط‌های دقیق موردنظرتان کمک می‌کند.

1.5.3 - بررسی شرط‌های چندگانه

شاید بخواهید چند شرط را همزمان بررسی کنید. برای نمونه گاهی برای انجام عملی به True بودن همزمان دو شرط نیاز دارید. زمانی دیگر ممکن است ارضا شدن تنها یکی از دو شرط کافی باشد. دو کلیدواژه‌ی and و or می‌تواند شما را در این شرایط یاری کند.

به‌کارگیری and برای بررسی شرط‌های چندگانه

برای بررسی درست (True) بودنِ همزمانِ دو شرط از کلیدواژه‌ی and برای ترکیب دو آزمون شرطی استفاده کنید. اگر هر دو آزمون موفق باشند عبارت کلّی درست (True) خواهد بود. در صورتی که یکی یا هر دوی آزمون‌ها ناموفّق باشند، عبارت کلی نیز نادرست (False) خواهد بود.

برای مثال می‌توانید هنگام بررسی سن دو نفر بفهمید که آیا هر دو بالای ۲۱ سال هستند یا نه:

1
2
3
4
5
6
7
>>> age_0 = 22
>>> age_1 = 18
>>> age_0 >= 21 and age_1 >= 21
False
>>> age_1 = 22
>>> age_0 >= 21 and age_1 >= 21
True

در ۱ دو سن age_0 و age_1 را تعریف می‌کنیم. در ۲ بررسی می‌کنیم که آیا هر دو بالای ۲۱ اند یا نه؟ آزمون سمت چپ موفق است امّا راستی نه، پس کل عبارت نادرست (False) است. در ۳ age_1 را به ۲۲ تغییر می‌دهیم. اکنون مقدار age_1 بیشتر از ۲۱ است پس از دو آزمون شرطی موفّق‌اند، این باعث می‌شود که کل عبارت شرطی درست (True) تلقّی شود.

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

1
(age_0 >= 21) and (age_1 >= 21)

به‌کارگیری or برای بررسی شرط‌های چندگانه

کلیدواژه‌ی or هم امکان بررسی چند شرط را برای شما فراهم می‌آورد با این تفاوت که این بار اگر یکی یا هر دوی شرط‌ها برقرار باشند، کل عبارت پذیرفته می‌شود.

مثال مربوط به سن را در نظر بگیرید. این بار به دنبال این هستیم که بفهمیم آیا تنها یکی از دو نفر بالای ۲۱ سال هست یا نه:

1
2
3
4
5
6
7
>>> age_0 = 22
>>> age_1 = 18
>>> age_0 >= 21 or age_1 >= 21
True
>>> age_0 = 18
>>> age_0 >= 21 or age_1 >= 21
False

از تعریف دو متغیّر مربوط به سن در ۱ شروع می‌کنیم. از آن‌جایی که آزمون مربوط به age_0 درست است، کل عبارت ۲ درست (True) ارزیابی می‌شود. سپس age_0 را به ۱۸ کاهش می‌دهیم. در آزمون ۳، هر دو آزمون رد می‌شوند و عبارت کلّی نادرست (False) تعبیر خواهد شد.

بررسی وجود یک مقدار در یک لیست

گاهی مهم است که پیش از انجام کاری بررسی کنیم که یک لیست مقدار خاصّی را شامل می‌شود یا نه. برای نمونه پیش از تکمیل ثبت نام کاربر در یک وب‌سایت، شاید نیاز داشته باشید که بررسی کنید آیا نام کاربریِ انتخابیِ کاربر در لیست نام‌های کاربری قبلی وجود دارد یا نه.

برای دریافتن این که آیا مقدار خاصّی در لیست وجود دارد از کلیدواژه‌ی in استفاده می‌کنیم. فرض کنید می‌خواهید برای یک پیتزافروشی برنامه‌ای بنویسید. لیستی از مواد درخواستی مشتری برای روی پیتزا تهیّه می‌کنیم و سپس بررسی می‌کنیم که آیا آن‌ها در لیست ما وجود دارد یا نه.

1
2
3
4
5
>>> requested_toppings = ['mushrooms', 'onions', 'pineapple']
>>> 'mushrooms' in requested_toppings
True
>>> 'pepperoni' in requested_toppings
False

در ۱ و ۲، کلیدواژه‌ی in از پایتون می‌خواهد وجود ‘mushrooms’ و ‘pepperoni’ را در لیستِ requested_toppings بررسی کند. این تکنیک بسیار قدرتمند است زیرا می‌توانید به کمک آن لیستی از مقادیر ضروری درست کنید و سپس به آسانی بررسی کنید که مقداری مورد نظر شما با یکی از مقادیر داخل لیست برابری می‌کند یا نه.

بررسی وجود نداشتن یک مقدار در یک لیست

زمانی دیگر ممکن است بخواهید وجود نداشتن مقداری را در لیستی بررسی کنید. در این شرایط می‌توانید از کلیدواژه‌ی not استفاده کنید. برای مثال لیستی از کاربران را در نظر بگیرید که از ارسال نظر در یک انجمن منع شده‌اند. می‌خواهید پیش از ثبت یک نظر بررسی کنید که آیا کاربرِ نظردهنده در لیست کاربران ممنوع وجود دارد یا نه.

1
2
3
4
5
6
banned_users = ['andrew', 'carolina', 'david']

user = 'marie'

if user not in banned_users:
    print(f"{user.title()}, you can post a response if you wish.")

خط ۱ به آسانی خوانده می‌شود. اگر مقدار user در لیست banned_users نبود، پایتون True برمی‌گرداند و کد تورفته را اجرا می‌کند.

کاربر ‘marie’ در لیست ‘banned_users’ نیست پس پیامی می‌بیند که از او برای ارسال نظرش دعوت می‌کند:

1
Marie, you can post a response if you wish.

عبارت‌های بولی

در طی آموختن برنامه‌نویسی جایی به عبارت بولی برخواهید خورد. عبارت بولی تنها نامی دیگر برای آزمون شرطی است. مقدار بولی همانندِ مقدارِ یک عبارتِ شرطی پس از ارزیابی، یا درست (True) است یا نادرست (False).

مقدارهای بولی غالباً برای پیگیری و دنبال کردن شرط‌ها به کار می‌رود، مانند این که آیا بازی می‌تواند اجرا شود یا آیا کاربری می‌تواند مطلب خاصّی را روی وب‌سایت ویرایش کند یا نه:

1
2
game_active = True
can_edit = False

مقدارهای بولی برای پیگیری وضعیّت برنامه یا شرط خاصّی که در برنامه مهم است، راهی موثّر ارائه می‌کنند.

خودآزمایی

آزمون‌های شرطی

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

1
2
3
4
5
6
7
car = 'subaru'

print("Is car == 'subaru'? I predict True.")
print(car == 'subaru')

print("\nIs car == 'audi'? I predict False.")
print(car == 'audi')
  • به دقّت به نتایج خود نگاه کنید و مطمئن شوید که دلیل True یا False شدن هر خط را متوجّه می‌شوید.
  • دستِ کم ده آزمون بسازید طوری که حدّاقل پنج تای آن‌ها True و پنج‌تای دیگر False شود.

آزمون‌های شرطی بیشتر

نباید خود را به ساختن تنها ده آزمون شرطی محدود کنید. اگر می‌خواهید مقایسه‌های بیشتری بکنید، آزمون‌های بیشتری بنویسید و آن‌ها را به conditional_tests.py بیفزایید. دست کم یک نتیجه‌ی True و یک نتیجه‌ی False برای هر کدام از موارد زیر داشته باشید:

  • آزمون برابری و نابرابری برای رشته‌ها
  • آزمون‌هایی با متد lower()
  • آزمون‌های عددی شامل برابری و نابرابری، بزرگتری و کوچکتری، بزرگتری و تساوی و کوچکتری و تساوی.
  • آزمون‌هایی شامل کلیدواژه‌های and و or
  • آزمون وجود عضوی در یک لیست
  • آزمون وجود نداشتن عضوی در یک لیست

1.5.4 - گزاره‌های if

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

گزاره‌های if ساده

ساده‌ترین نوع گزاره‌ی if یک آزمون و یک عملکرد دارد:

1
2
if conditional_test:
    do something

می‌توانید هر نوع آزمون شرطی را در خط اوّل و هر آن‌چه را که می‌خواهید انجام دهید در بخش تورفته‌ی بعد از آن بنویسید. اگر آزمون شرطی True ارزیابی شود کدی که بعد از گزاره‌ی if آمده اجرا می‌شود امّا اگر False ارزیابی شود پایتون آن کد را نادیده می‌گیرد.

اکنون متغیّری را برای نمایش سن یک نفر در نظر می‌گیریم و می‌خواهیم بدانیم آیا به سنّ قانونی برای رای دادن رسیده یا نه. کد زیر امکان رای دادن شخص را بررسی می‌کند:

1
2
3
4
age = 19

if age >= 18:
    print("You are old enough to vote!")

در 1 پایتون بررسی می‌کند که آیا مقدار age بزرگتر یا مساوی ۱۸ است یا نه؟ چون این شرط برقرار است پایتون دستور print() تورفته را اجرا می‌کند:

1
You are old enough to vote!

تورفتگی در گزاره‌های if همان نقشی را بازی می‌کند که در حلقه‌های for ایفا می‌کرد. تمام خط‌های تورفته‌ی پس از گزاره‌ی if در صورتِ برقراریِ شرط اجرا خواهند شد و کل این خط‌ها در صورت عدم برقراری نادیده گرفته خواهند شد.

می‌توانید هر تعداد خط که خواستید به بخش بعد از گزاره‌ی if اضافه کنید. بیایید خطّی دیگر به خروجی اضافه کنید و بپرسیم که آیا فرد برای رای دادن ثبت نام کرده است یا نه:

1
2
3
4
5
age = 19

if age >= 18:
    print("You are old enough to vote!")
    print("Have you registered to vote yet?")

شرط برقرار است، هر دو دستور print() تورفته‌اند، پس هر دو خط چاپ می‌شود:

1
2
You are old enough to vote!
Have you registered to vote yet?

اگر مقدار age کمتر از ۱۸ باشد این برنامه هیچ خروجی‌ای نخواهد داشت.

گزاره‌های if-else

اغلب می‌خواهید هنگام برقراری یک شرط کار خاصّی انجام دهید و در صورت عدم برقراری کار دیگری. دستور if-else پایتون چنین امکانی فراهم می‌آورد. ساختار if-else مشابه گزاره‌ی if ساده است امّا گزاره‌ی else به شما اجازه می‌دهد که عملی یا مجموعه‌ای از عمل‌ها را پس از برقرار نشدن شرط اجرا کنید.

مانند مثال قبلی خطاب به کسانی که به سن رای دادن رسیده‌اند پیامی چاپ می‌کنیم امّا این بار به کسانی که به سن قانونی نرسیده‌اند هم پیامی می‌دهیم.

1
2
3
4
5
6
7
8
9
age = 17

if age >= 18:
    print("You are old enough to vote!")
    print("Have you registered to vote yet?")

else:
    print("Sorry, you are too young to vote.")
    print("Please register to vote as soon as you turn 18!")

اگر شرط 1 برقرار شود اوّلین مجموعه‌دستورهای تورفته‌ی print() اجرا می‌شود. اگر آزمون False ارزیابی شود، کد else در 2 اجرا خواهد شد. چون این بار age زیر ۱۸ است، شرط برقرار نمی‌شود و کدی اجرا می‌شود که در بخش else آمده است:

1
2
Sorry, you are too young to vote.
Please register to vote as soon as you turn 18!

این کد به این دلیل کار می‌کند که تنها دو حالت ممکن برای ارزیابی وجود دارد: یا شخص به سن قانونی برای رای دادن رسیده یا نرسیده. در چنین شرایطی که پایتون باید یکی از دو کار ممکن را انجام دهد، ساختار if-else به خوبی کار می‌کند. در یک زنجیره‌ی if-else ساده مانند این، همیشه یکی از دو کار اجرا می‌شود.

1.5.5 - زنجیره‌ی if-elif-else

اغلب نیاز دارید که بیش از دو وضعیّت را بررسی کنید. برای ارزیابی چنین شرایطی می‌توانید از دستور if-elif-else پایتون استفاده کنید. پایتون تنها یکی از بخش‌ها را در زنجیره‌ی if-elif-else اجرا می‌کند. تا زمانی که یکی از آزمون‌ها برقرار شود، پایتون آن‌ها را به ترتیب اجرا می‌کند. به محض برقراری یک شرط، کد مربوط به آن اجرا شده و باقی آزمون‌ها نادیده گرفته می‌شوند.

بسیاری از وضعیت‌ها در دنیای واقعی بیش از دو شرط ممکن دارند. برای مثال یک شهربازی را در نظر بگیرید که نرخ‌های مختلفی برای گروه‌های سنّی مختلف در نظر می‌گیرد:

  • پذیرش کودکان زیر ۴ سال رایگان است.
  • پذیرش افراد بین ۴ تا ۱۸ سال ۲۵ دلار هزینه دارد.
  • پذیرش افراد ۱۸ سال و بالاتر ۴۰ دلار هزینه دارد.

چگونه می‌توانیم از گزاره‌ی if برای تعیین هزینه‌ی پذیرش یک شخص استفاده کنیم؟ کد زیر گروه سنّی فرد را بررسی می‌کند و هزینه‌ی متناسب را چاپ می‌کند:

1
2
3
4
5
6
7
8
age = 12

if age < 4:
    print("Your admission cost is $0.")
elif age < 18:
    print("Your admission cost is $25.")
else:
    print("Your admission cost is $40.")

آزمون if در 1 بررسی می‌کند که آیا سن فرد زیر ۴ سال است یا نه. اگر این چنین باشد پیام مناسب چاپ شده و پایتون بقیه‌ی آزمون‌ها را نادیده می‌گیرد. خط elif در 2 در واقع یک if دیگر است و تنها زمانی اجرا می‌شود که آزمون قبلی موفّق نباشد. در این نقطه از زنجیره می‌دانیم که سن فرد حداقل ۴ سال است زیرا آزمون اول نتیجه نداده است. اگر سن فرد زیر ۱۸ سال باشد پیام متناسب چاپ خواهد شد و پایتون بخش else را نادیده می‌گیرد. اگر هر دو آزمون if و elif برقرار نشوند، کد مربوط به else را در 3 اجرا می‌کند.

در این مثال آزمون 1 False ارزیابی می‌شود پس این بخش از کد اجرا نمی‌شود. امّا آزمون دوم True می‌شود (۱۲ کمتر از ۱۸ است) پس این کد اجرا خواهد شد. خروجی یک جمله است که هزینه‌ی پذیرش فرد را نشان می‌دهد:

1
Your admission cost is $25.

هر سنّی بالاتر از ۱۷ موجب رد شدن هر دو آزمون اوّل می‌شود. در چنین شرایطی بخش else اجرا خواهد شد و هزینه‌ی پذیرش ۴۰ دلار است.

به جای چاپ کردن هزینه در هر بخش از زنجیره‌ی if-elif-else، مختصرتر این است که تنها قیمت را در زنجیره مشخّص کنیم و پس از پایان ارزیابی کامل زنجیره، یک بار دستور print() ساده را فراخوانی کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
age = 12

if age < 4:
    price = 0
elif age < 18:
    price = 25
else:
    price = 40

print(f"Your admission cost is ${price}.")

خط‌ها در 2،1 و 3 مقدار price را بر اساس سن فرد تعیین می‌کند. پس از تعیین شدن قیمت در زنجیره‌ی if-elif-else، فراخوانیِ جداگانه و بدونِ تورفتگی print()، این مقدار را برای گزارش هزینه‌ی پذیرش فرد نمایش می‌دهد.

خروجی این کد مشابه مثال قبل است امّا مقصود زنجیره‌ی if-elif-else اینجا دقیق‌تر و مختصرتر است. به جای تعیین و چاپ کردن قیمت، تنها قیمت پذیرش را تعیین می‌کند. علاوه بر کارآمدی بیشتر، کد بازبینی‌شده نسبت به رویکرد اولیّه راحت‌تر قابل تغییر است. برای تغییر متن پیام خروجی، به جای تغییر سه print() مجزّا، کافی است تنها یک دستور print() را عوض کنید.

استفاده از چند elif

می‌توانید هر تعداد elif که خواستید در کد خود استفاده کنید. مثلاً اگر شهربازی برای افراد مسن تخفیف قائل شده باشد، می‌توانید یک آزمون شرطی دیگر به کد اضافه کنید تا تشخیص دهید فرد مستحقّ دریافت تخفیف است یا نه. فرض کنید افراد بالای ۶۵ سال باید نصف هزینه‌ی معمول پذیرش، یعنی ۲۰ دلار، را بپردازند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
age = 12

if age < 4:
    price = 0
elif age < 18:
    price = 25
elif age < 65:
    price = 40
else:
    price = 20

print(f"Your admission cost is ${price}.")

بیشترِ کد دست نخورده است. پیش از تعیین قیمت معمولی ۴۰ دلار، بخش elif دوم در 1 بررسی می‌کند که آیا فرد زیر ۶۵ سال است یا نه. توجّه کنید که قیمت تعیین شده در else (2) باید ۲۰ دلار باشد چون تنها افرادی که به این بخش می‌رسند بالای ۶۵ سال سن دارند.

حذف کردن قسمت else

پایتون الزامی به وجود یک بخش else در پایان زنجیره‌ی if-elif نمی‌بیند. گاهی وجود قسمت else سودمند است؛ گاهی به کار‌گیری یک گزاره‌ی elif اضافی برای دریافتن یک شرط مشخّص مورد نظر، واضح‌تر است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
age = 12

if age < 4:
    price = 0
elif age < 18:
    price = 25
elif age < 65:
    price = 40
elif age >= 65:
    price = 20

print(f"Your admission cost is ${price}.")

قسمت elif افزوده شده در 1 برای افزاد بالاتر از ۶۵ سال قیمت ۲۰ دلار را تعیین می‌کند. این کار از به‌کارگیری یک قسمتِ else عمومی کمی واضح‌تر است. با این تغییر هر قسمتِ کد با گذراندن یک آزمون شرطی مشخّص اجرا خواهد شد.

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

1.5.6 - آزمودن چند شرط

زنجیره‌ی if-elif-else قدرتمند است امّا تنها زمانی کاربرد دارد که بخواهید فقط یک شرط برقرار باشد. به محض برقراری یکی از آزمون‌های شرطی پایتون باقی آزمون‌ها را نادیده می‌گیرد. این رفتار به خاطر کارآمدی سودمند است و آزمودن یک شرط مشخّص را برای شما ممکن می‌سازد.

با این حال گاهی مهم است که تمام شرط‌های مورد نظر را بررسی کنیم. در این حالت باید از تعداد گزاره‌ی if متوالی بدون elif یا else استفاده کنیم. این تکنیک زمانی معنا می‌یابد که بیش از یک شرط امکان تحقّق داشته باشند و بخواهید برای هر شرطی که True شود کاری انجام دهید.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
requested_toppings = ['mushrooms', 'extra cheese']

if 'mushrooms' in requested_toppings:
    print("Adding mushrooms.")

if 'pepperoni' in requested_toppings:
    print("Adding pepperoni.")

if 'extra cheese' in requested_toppings:
    print("Adding extra cheese.")

print("\nFinished making your pizza!")

در 1 لیستی شامل مواد درخواستی تعریف می‌کنیم. در گزاره‌ی 2 بررسی می‌کنیم که آیا مشتری برای روی پیتزای خود قارچ سفارش داده یا نه. اگر چنین باشد پیامی برای تایید چاپ می‌شود. آزمونی که در 3 برای پپرونی آورده شده یک گزاره‌ی if ساده است، نه elif یا else، بنابراین کد مربوط به آن، صرف از نظر از برقراریِ شرطِ قبلی، اجرا می‌شود. کد خط 4 بررسی می‌کند که بدون توجّه به آزمون قبلی، آیا پنیر اضافی درخواست شده یا نه. این سه آزمون مستقل با هر بار اجرای برنامه اجرا خواهند شد.

از آن‌جایی که تمام شرط‌ها در این مثال ارزیابی می‌شوند، قارچ و پنیر اضافی هر دو به پیتزا اضافه می‌شوند:

1
2
3
4
Adding mushrooms.
Adding extra cheese.

Finished making your pizza!

اگر از ساختار if-elif-else استفاده می‌کردیم این کد به درستی کار نمی‌کرد، زیرا پس از برقراری یکی از شرط‌ها اجرای آن متوقّف می‌شد. اینجا می‌بینیم که خروجی به چه شکل خواهد بود:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
requested_toppings = ['mushrooms', 'extra cheese']

if 'mushrooms' in requested_toppings:
    print("Adding mushrooms.")
elif 'pepperoni' in requested_toppings:
    print("Adding pepperoni.")
elif 'extra cheese' in requested_toppings:
    print("Adding extra cheese.")

print("\nFinished making your pizza!")

آزمون مربوط به ‘mushrooms’ اولین موردی است که تحقّق می‌یابد پس قارچ به پیتزا اضافه می‌شود. با این حال مقادیر ’extra cheese’ و ‘pepperoni’ هرگز بررسی نمی‌شوند زیرا پس از موفقّیت‌آمیز بودن آزمون اوّل، پایتون کدهای دیگر در زنجیره‌ی if-elif-else را اجرا نمی‌کند. اوّلین مورد درخواستی مشتری اضافه می‌شود امّا دوتای دیگر از قلم خواهند افتاد:

1
2
3
Adding mushrooms.

Finished making your pizza!

به طور مختصر، اگر بخواهید تنها یک قسمت از کد اجرا شود از یک زنجیره‌ی if-elif-else استفاده کنید. اگر قسمت‌های مختلف کد باید اجرا شوند از تعدادی گزاره‌ی if مستقل بهره بگیرید.

خودآزمایی

رنگ‌های موجودات بیگانه ۱

فرض کنید که در یک بازی به یک موجود بیگانه شلیک کرده‌ایم. متغیّری به نام alien_color تعریف کنید و به آن مقدار ‘green’، ‘yellow’ یا ‘red’ را نسبت دهید.

  • یک گزاره‌ی if بنویسید تا سبز بودن رنگ موجود بیگانه را بررسی کند. اگر چنین بود پیامی چاپ کند که بازیکن ۵ امتیاز گرفته است.
  • یک نسخه از این برنامه را به نحوی بنویسید که در آن شرط برقرار می‌شود. نسخه‌ی دیگر را طوری بنویسید که در آن شرط تحقّق نیابد. (در این نسخه برنامه هیچ خروجی‌ای ندارد.)

رنگ‌های موجودات بیگانه ۲

همانند تمرین ۵-۳ رنگی برای موجودات بیگانه انتخاب کنید و یک زنجیره‌ی if-else بنویسید.

  • اگر رنگ موجود بیگانه سبز باشد، پیامی چاپ کند مبنی بر این که بازیکن ۵ امتیاز کسب کرده است.
  • اگر رنگ سبز نبود اعلام کند که بازیکن ۱۰ امتیاز گرفته است.
  • برنامه را یک بار طوری بنویسید که شرط if برقرار شود و بار دیگر شرط else.

رنگ‌های موجودات بیگانه ۳

زنجیره‌ی if-else مثال ۵-۴ را به if-elif-else تبدیل کنید.

  • اگر رنگ موجود بیگانه سبز بود، پیامی چاپ کند که بازیکن ۵ امتیاز گرفته است.
  • اگر زرد بود، پیامی چاپ کند که بازیکن ۱۰ امتیاز گرفته است.
  • اگر قرمز بود، پیامی چاپ کند که بازیکن ۱۵ امتیاز گرفته است.
  • سه نسخه از این برنامه را به نحوی بنویسید که هر بار یکی از شرط‌ها برقرار شود.

مراحل زندگی

یک زنجیره‌ی if-elif-else بنویسید که مشخّص کند فرد در کدام مرحله‌ی زندگی است. مقداری برای متغیّر age تعیین کنید، سپس:

  • اگر فرد زیر ۲ سال سن داشت، پیامی چاپ کند که فرد نوزاد است.
  • اگر سن بین ۲ تا ۴ سال بود پیامی چاپ کند که فرد نوپاست.
  • اگر فرد بین ۴ و ۱۳ سال سن دارد، پیامی چاپ کند که فرد کودک است.
  • اگر سن بین ۱۳ و ۲۰ سال است پیامی چاپ کند که فرد نوجوان است.
  • اگر سن بین ۲۰ و ۶۵ باشد پیامی چاپ کند که فرد بالغ است.
  • اگر سن ۶۵ یا بیشتر بود پیامی چاپ کند که فرد کهنسال است.

میوه‌ی مورد علاقه

لیستی از میوه‌های مورد علاقه‌ی خود درست کنید و تعدادی گزاره‌ی if مستقل بنویسید که وجود میوه‌ی خاصّی را در لیست بررسی کند.

  • لیستی از سه میوه‌ی مورد علاقه‌ی خود بسازید و آن را favorite_fruits بنامید.
  • پنج گزاره‌ی if بنویسید. هر کدام از آن‌ها بررسی می‌کنند که آیا میوه‌ی خاصّی در لیست وجود دارد یا نه. اگر میوه در لیست بود کد داخل گزاره‌ی if باید جمله‌ای مانند You really like bananas چاپ کند.

1.5.7 - به‌کارگیری گزاره‌های if در لیست‌ها

با ترکیب کردن لیست‌ها و گزاره‌های if می‌توانید کارهای جالبی بکنید. می‌توان مقادیر ویژه‌ای را تحت نظر گرفت که نسبت به دیگر مقادیر موجود در لیست نیاز به عمل متفاوتی دارند، می‌توانید شرایطی را که در حال تغییرند به طور موثری مدیریّت کنید. به تدریج ثابت می‌کنید که کد شما همان‌طور که انتظار دارید تحت هر شرایطی کار می‌کند.

بررسی موارد خاص

این فصل با مثالی ساده آغاز شد که در آن نشان دادیم چگونه مقداری ویژه‌ای مانند ‘bmw’ را مدیریت کنیم به نحوی که در مقایسه با دیگر اعضای لیست به شکل دیگری چاپ شود. اکنون که درکی پایه‌ای از آزمون‌های شرطی و گزاره‌های if کسب کرده‌اید، نگاهی دقیق‌تر به نحوه‌ی تحت نظر گرفتن مقدارهای خاصی در یک لیست می‌اندازیم و این که چگونه آن‌ها را به درستی مدیریت کنیم.

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

1
2
3
4
5
6
requested_toppings = ['mushrooms', 'green peppers', 'extra cheese']

for requested_topping in requested_toppings:
    print(f"Adding {requested_topping}.")

print("\nFinished making your pizza!")

خروجی سرراست است زیرا کد تنها یک حلقه‌ی ساده‌ی for است:

1
2
3
4
5
Adding mushrooms.
Adding green peppers.
Adding extra cheese.

Finished making your pizza!

اگر پیتزافروشی فلفل سبز تمام کرده باشد چه اتّفاقی می‌افتد؟ یک گزاره‌ی if در داخل حلقه می‌تواند این وضعیّت را به خوبی مدیریّت کند:

1
2
3
4
5
6
7
8
9
requested_toppings = ['mushrooms', 'green peppers', 'extra cheese']

for requested_topping in requested_toppings:
    if requested_topping == 'green peppers':
        print("Sorry, we are out of green peppers right now.")
    else:
        print(f"Adding {requested_topping}.")

print("\nFinished making your pizza!")

این بار هر مورد درخواستی را پیش از افزودن به پیتزا بررسی می‌کنیم. کد در 1 بررسی می‌کند که آیا مشتری فلفل سبز درخواست کرده یا نه. اگر چنین باشد پیامی چاپ می‌کند و به او اعلام می‌کند که چرا نمی‌توانند چنین موردی را به پیتزای خود اضافه کنند. قسمت else در 2 اطمینان حاصل می‌کند که تمام مواد دیگر به پیتزا افزوده شوند.

خروجی نشان می‌دهد که هر کدام از موارد درخواستی به درستی مدیریّت شد‌ه‌اند:

1
2
3
4
5
Adding mushrooms.
Sorry, we are out of green peppers right now.
Adding extra cheese.

Finished making your pizza!

بررسی تهی نبودن یک لیست

تا اینجا در مورد لیست‌هایی که با آن‌ها کار کرده‌ایم یک فرض ساده داشته‌ایم؛ فرض کرده‌ایم که هر لیست حداقل یک عضو دارد. به زودی به کاربران امکان وارد کردن اطّلاعاتی را خواهیم داد که باید در لیست‌ها ذخیره شود، بنابراین نمی‌توانیم فرض کنیم که در هر بار اجرای حلقه لیست عضوی در خود دارد. در چنین شرایط بررسی تهی بودنِ یک لیست پیش از اجرای حلقه‌ی for کاری سودمند است.

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

1
2
3
4
5
6
7
8
requested_toppings = []

if requested_toppings:
    for requested_topping in requested_toppings:
        print(f"Adding {requested_topping}.")
    print("\nFinished making your pizza!")
else:
    print("Are you sure you want a plain pizza?")

این بار از یک لیست خالی برای مواد درخواستی در 1 شروع کردیم. به جای پریدن داخل حلقه‌ی for، یک آزمون سریع در 2 انجام دادیم. اگر در گزاره‌ی if نام لیست را بیاوریم، در صورتی که لیست دستِ کم یک عضو داشته باشد، پایتون این گزاره را True ارزیابی می‌کند و اگر لیست خالی باشد آن را False در نظر می‌گیرد. اگر requested_toppings در آزمون شرطی قبول شود، حلقه‌ی for مانند مثال قبل اجرا خواهد شد. در غیر این صورت در 3 پیامی چاپ می‌کنیم و از مشتری می‌پرسیم که آیا واقعاً یک پیتزای ساده می‌خواهد.

در این مورد لیست خالی است پس خروجی از کاربر می‌پرسد آیا واقعاً پیتزای ساده می‌خواهد؟

1
Are you sure you want a plain pizza?

اگر لیست خالی نباشد خروجی همه‌ی چیزهایی را که به پیتزا اضافه می‌شود نشان می‌دهد.

به کارگیری چند لیست

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
available_toppings = ['mushrooms', 'olives', 'green peppers',
                      'pepperoni', 'pineapple', 'extra cheese']

requested_toppings = ['mushrooms', 'french fries', 'extra cheese']

for requested_topping in requested_toppings:
    if requested_topping in available_toppings:
        print(f"Adding {requested_topping}.")
    else:
        print(f"Sorry, we don't have {requested_topping}.")

print("\nFinished making your pizza!")

در 1 لیستی از مواد موجود در پیتزافروشی تعریف کردیم. توجّه داشته باشید که اگر انتخاب‌های موجود برای مواد روی پیتزا ثابت باشد می‌توان به جای لیست از چندتایی استفاده کرد. در 2 لیستی از موادِ موردِ درخواستِ مشتری درست کردیم. به درخواست نامعمول ‘french fries’ توجّه کنید. در خط 3 داخل لیست مواد درخواستی حلقه می‌زنیم. درون حلقه (4) ابتدا بررسی می‌کنیم که آیا ماده‌ی درخواستی در لیست مواد موجود وجود دارد یا نه. اگر چنین است مواد را به پیتزا اضافه می‌کنیم. امّا اگر ماده‌ی درخواستی در لیست موجود نبود، بخش else خط 5 را اجرا خواهد کرد. این بخش پیامی چاپ می‌کند و به مشتری می‌گوید که مواد مورد نظر او موجود نیست.

این کد یک خروجی مرتّب و پرمعنی تولید می‌کند:

1
2
3
4
5
Adding mushrooms.
Sorry, we don't have french fries.
Adding extra cheese.

Finished making your pizza!

در تنها چند خط کد یک مساله‌ی دنیای واقعی را به روشی بسیار موثّر مدیریت کردیم!

خودآزمایی

سلامْ مدیر

لیستی از پنج‌تا یا بیشتر نام کاربری درست کنید که شامل ‘admin’ باشد. فرض کنید کدی می‌نویسید که پس از ورود هر کاربر به سایت پیام تبریکی خطاب به او چاپ کند. در لیست حلقه بزنید و پیام تبریکی برای هر کاربر چاپ کنید:

  • اگر نام کاربری ‘admin’ است تبریکی خاص چاپ کنید مانند Hello admin, would you like to see a status report?.
  • در غیر این صورت یک پیام تبریک عمومی چاپ کنید. مانند Hello Jaden, thank you for logging in again.

فقدان کاربران

یک آزمون if به hello_admin.py اضافه کنید تا مطمئن شوید که لیست کاربران خالی نیست.

  • اگر لیست خالی است پیام We need to find some users را چاپ کند.
  • تمام نام‌های کاربری را از لیست خود حذف کنید و مطمئن شوید که پیام درست چاپ می‌شود.

بررسی نام‌های کاربری

کارهای زیر را برای ساختن برنامه‌ای انجام دهید که چگونگیِ عملکردِ وب‌سایت‌ها را هنگامِ بررسیِ یکتا بودنِ نام‌ِ کاربریِ هر کس شبیه‌سازی کند.

  • لیستی به نام current_users شامل پنج‌تا یا بیشتر نام کاربری بسازید.
  • لیست دیگری به نام new_users شامل پنج نام کاربری دیگر درست کنید. مطمئن شوید یک یا دو نام این لیست با لیست current_users مشترک است.
  • در لیست new_users حلقه بزنید تا ببینید که نام کاربری جدید قبلاً استفاده شده یا نه. اگر چنین بود پیامی چاپ کنید که به کاربر بگوید باید نام کاربر جدیدی انتخاب کند. اگر نام کاربری قبلاً استفاده نشده بود پیامی چاپ کند که اعلام کند نام کاربری موجود است.
  • مطمئن شوید که مقایسه‌ی شما به بزرگ و کوچکی حروف حساس نیست. اگر ‘John’ قبلاً انتخاب شده باشد، ‘JOHN’ نباید پذیرفته شود. (برای این کار باید یک کپی از current_users شامل نام کاربران با حروف کوچک درست کنید.)

اعداد ترتیبی

اعداد ترتیبی جایگاه خود را در یک لیست نمایش می‌دهند مثل یکم، دوم و … . در زبان انگلیسی به جز ۱، ۲ و ۳، باقی اعداد ترتیبی به th ختم می‌شوند.

  • اعداد ۱ تا ۹ را در لیستی ذخیره کنید.
  • در لیست حلقه بزنید.
  • از یک زنجیره‌ی if-elif-else در داخل حلقه استفاده کنید تا پسوند مناسب ترتیبی را برای هر عدد چاپ کند. خروجی شما باید به صورت “1st 2nd 3rd 4th 5th 6th 7th 8th 9th” خوانده شود و هر نتیجه باید در یک خط جدا چاپ شود.

1.5.8 - شکل‌دهی به عبارت‌های if

در هر کدام از مثال‌های این فصل نمونه‌هایی از عادت‌های خوب شکل‌دهی را دیده‌اید. تنها توصیه‌ای که PEP 8 برای شکل‌دهی آزمون‌های شرطی ارائه کرده این است که در دو طرف عملگرهای مقایسه‌ای مانند ==، >= و <= یک فاصله‌ی خالی قرار دهید.

1
if age < 4:

از:

1
if age<4:

بهتر است.

این فاصله‌گذاری بر روشی که پایتون کد شما را تفسیر می‌کند، تاثیری نمی‌گذارد؛ تنها خواندن کد شما را برای خودتان و دیگران ساده‌تر می‌کند.

خودآزمایی

شکل‌دهی به گزاره‌های if

برنامه‌هایی که در این فصل نوشته‌اید مرور کنید و مطمئن شوید که آزمون‌های شرطی خود را به خوبی شکل داده‌اید.

ایده‌های شما

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

1.5.9 - خلاصه

در این فصل نوشتن آزمون‌های شرطی را آموختید که همیشه True یا False ارزیابی می‌شوند. یاد گرفتید که چگونه عبارت‌های ساده‌ی if، زنجیره‌های if-else و if-elif-else بنویسید. برای تشخیص شرط‌های خاصی که نیاز به بررسی دارند، به‌کارگیری این ساختارها را شروع کردید. همچنین فهمیدید که آن شرط‌ها چه زمانی در برنامه‌های شما برآورده می‌شوند. برخورد متفاوت با برخی اعضای خاص یک لیست را همزمان با به‌کارگیری کارآمدِ یک حلقه‌ی for آموختید. دوباره به توصیه‌ها و شیوه‌نامه‌های پایتون برخوردید تا مطمئن شوید با پیچیده‌تر شدن برنامه‌ها، خوانایی و درک آن‌ها نسبتاً ساده باقی می‌ماند.

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


1.6 - دیکشنری‌ها

1.6.1 - یک دیکشنری ساده

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

درک کردن دیکشنری‌ها به شما امکان می‌دهد که گستره‌ای از مسائل دنیای واقعی را به طور دقیق‌تر مدل کنید.

یک دیکشنری ساده

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

1
2
3
4
alien_0 = {'color': 'green', 'points': 5}

print(alien_0['color'])
print(alien_0['points'])

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

1
2
green
5

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

1.6.2 - کار کردن با دیکشنری

دیکشنری در پایتون مجموعه‌ای از جفت‌های کلید-مقدار است. هر کلید با یک مقدار مرتبط است و به کمک آن می‌توانید به این مقدارِ مرتبط دسترسی پیدا کنید. مقدار یک کلید می‌تواند عدد، رشته، لیست یا حتّی یک دیکشنری دیگر باشد.

در پایتون یک دیکشنری با دنباله‌ای از کلید-مقدارها، داخل آکُلاد {} قرار می‌گیرد:

1
alien_0 = {'color': 'green', 'points': 5}

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

دسترسی به مقادیر یک دیکشنری

برای گرفتن مقدار مرتبط با یک کلید، نام دیکشنری را بنویسید و سپس کلید را داخل یک جفت کروشه جلوی آن قرار دهید:

1
2
3
alien_0 = {'color': 'green'}

print(alien_0['color'])

این کد مقدار مربوط به کلید ‘color’ را از دیکشنری alien_0 برمی‌گرداند:

1
green

شما می‌توانید تعداد نامحدودی جفتِ کلید-مقدار در یک دیکشنری بگنجانید. اگر بازیکنی این موجود بیگانه را بکشد، امتیازِ کسب‌شده‌ی او با کدی به صورت زیر به دست می‌آید:

1
2
3
4
5
alien_0 = {'color': 'green', 'points': 5}

new_points = alien_0['points']

print(f"You just earned {new_points} points!")

کد 1 می‌تواند بلافاصله بعد از تعریفِ دیکشنری، مقدار مرتبط با کلید ‘points’ را از آن بیرون بکشد. این مقدار در ادامه به متغیّر new_points منسوب می‌شود. خط 2 درباره‌ی تعداد امتیازی که بازیکن دریافت کرده عبارتی چاپ می‌کند:

1
You just earned 5 points!

افزودن جفت‌های کلید-مقدار جدید

دیکشنری‌ها ساختارهایی پویا هستند و همیشه می‌توان جفت‌های کلید-مقدارِ جدید را به آن‌ها افزود. برای افزودن یک جفت کلید-مقدار، نام دیکشنری را همراه با کلید جدید، که در کروشه قرار گرفته، در کنارِ مقدار جدید می‌نویسید.

بیایید دو دانسته‌ی جدید به دیکشنری alien_0 بیفزاییم: مختصّات x و y آن:

1
2
3
4
5
6
7
8
alien_0 = {'color': 'green', 'points': 5}

print(alien_0)

alien_0['x_position'] = 0
alien_0['y_position'] = 25

print(alien_0)

مطابق قبل از تعریف یک دیکشنری شروع می‌کنیم. سپس آن را چاپ می‌کنیم. در 1 یک جفت کلید-مقدار جدید به آن اضافه می‌کنیم: کلید ‘x_position’ و مقدار 0. در 2 همین کار را برای کلید ‘y_position’ تکرار می‌کنیم:

1
2
3
{'color': 'green', 'points': 5}

{'color': 'green', 'points': 5, 'y_position': 25, 'x_position': 0}
نکته
از پایتون ۳.۷ به بعد، دیکشنری‌ها ترتیبی را که مطابق آن تعریف شده‌اند، حفظ می‌کنند. زمانی که یک دیکشنری را چاپ می‌کنید یا در آن حلقه می‌زنید، اعضای آن را به همان ترتیبِ افزوده شدن خواهید دید.

شروع کردن از یک دیکشنری خالی

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

1
2
3
4
5
6
alien_0 = {}

alien_0['color'] = 'green'
alien_0['points'] = 5

print(alien_0)

نتیجه همان دیکشنری‌ای است که در مثال‌های قبلی به کار می‌بردیم:

1
{'color': 'green', 'points': 5}

معمولاً هنگام ذخیره‌سازی داده‌هایی که کاربران ارائه می‌کنند یا نوشتن کدی که تعداد زیادی جفت کلید-مقدار را به طور خودکار تولید می‌کند، از دیکشنری‌های خالی استفاده می‌کنیم.

تغییر دادن مقدارها در یک دیکشنری

برای تغییر دادن مقداری در یک دیکشنری، نام آن را به همراه کلید در داخل کروشه بنویسید و سپس مقدار جدیدی را بیاورید:

1
2
3
4
5
6
7
alien_0 = {'color': 'green'}

print(f"The alien is {alien_0['color']}.")

alien_0['color'] = 'yellow'

print(f"The alien is now {alien_0['color']}.")

ابتدا دیکشنری alien_0 را تعریف می‌کنیم که تنها شامل رنگ آن است، سپس مقدار مرتبط با کلید ‘color’ را به ‘yellow’ تغییر می‌دهیم:

1
2
3
The alien is green.

The alien is now yellow.

به عنوان یک مثال جذّاب‌تر بیاید موقعیّت یک موجود بیگانه را که با سرعت‌های مختلف حرکت می‌کند ردگیری کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
alien_0 = {'x_position': 0, 'y_position': 25, 'speed': 'medium'}

print(f"Original position: {alien_0['x_position']}")

# Move the alien to the right.
# Determine how far to move the alien based on its current speed.
if alien_0['speed'] == 'slow':
    x_increment = 1

elif alien_0['speed'] == 'medium':
    x_increment = 2

else:
    # This must be a fast alien.
    x_increment = 3

# The new position is the old position plus the increment.
alien_0['x_position'] = alien_0['x_position'] + x_increment

print(f"New position: {alien_0['x_position']}")

در 1 یک زنجیره‌ی if-elif-else مشخّص می‌کند که موجود بیگانه چه‌قدر باید به سمت راست حرکت کند. پس از محاسبه شدن مقدارِ افزایشِ جابه‌جایی، آن را در 2 به مقدار x_position اضافه می‌کنیم:

1
2
3
Original x-position: 0

New x-position: 2

حذف کردن جفت‌های کلید-مقدار

زمانی که دیگر به اطّلاعاتِ ذخیره‌شده در دیکشنری نیاز نداشتید می‌توانید به کمک عبارت del، یک جفت کلید-مقدار را به کلّی حذف کنید:

1
2
3
4
5
6
7
alien_0 = {'color': 'green', 'points': 5}

print(alien_0)

del alien_0['points']

print(alien_0)

خروجی نشان می‌دهد که کلید ‘points’ و مقدار آن، ۵، از دیکشنری حذف شده:

1
2
3
{'color': 'green', 'points': 5}

{'color': 'green'}
نکته
توجّه داشته باشید که جفت کلید-مقدار به صورت همیشگی حذف خواهد شد.

1.6.3 - یک دیکشنری شامل اشیای مشابه

مثال قبلی به ذخیره‌سازی اطّلاعاتِ مختلفِ یک شیء می‌پرداخت که موجودی بیگانه‌ای در یک بازی بود. علاوه بر این حالت شما می‌توانید از دیکشنری برای ذخیره کردن یک نوع اطّلاعات در مورد اشیاء مختلف استفاده کنید. مثلاً فرض کنید می‌خواهید از افراد مختلف در مورد زبان برنامه‌نویسی موردعلاقه‌شان نظرسنجی کنید:

1
2
3
4
5
6
favorite_languages = {
    'jen': 'python',
    'sarah': 'c',
    'edward': 'ruby',
    'phil': 'python',
}

هر کدام از کلیدها نام یکی از افراد پاسخ‌دهنده و هر مقدارْ زبان برنامه‌نویسی مورد علاقه‌ی اوست.

برای استفاده از این دیکشنری، با داشتن نام فرد شرکت‌کننده در نظرسنجی، زبان برنامه‌نویسی دلخواه او به سادگی به دست می‌آید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
favorite_languages = {
    'jen': 'python',
    'sarah': 'c',
    'edward': 'ruby',
    'phil': 'python',
}

language = favorite_languages['sarah'].title()

print(f"Sarah's favorite language is {language}.")

خروجی زبان مورد علاقه‌ی سارا را نمایش می‌دهد:

1
Sarah's favorite language is C.

به‌کارگیری get() برای دسترسی به مقدارها

به‌کارگیری کلید‌ها در کروشه برای بازیابی مقدار مورد نظر از دیکشنری یک خطر بالقوّه دارد: اگر کلید درخواستی شما وجود نداشته باشد به خطا برمی‌خورید:

1
2
3
alien_0 = {'color': 'green', 'speed': 'slow'}

print(alien_0['points'])
1
2
3
4
Traceback (most recent call last):
  File "alien_no_points.py", line 2, in <module>
    print(alien_0['points'])
KeyError: 'points'

برای دیکشنری‌ها به طور خاص می‌توانید از متد get() استفاده کنید تا برای مواقعی که کلید درخواستی وجود ندارد مقدارِ پیش‌فرضی برای بازگرداندن تعیین کنید:

1
2
3
4
5
alien_0 = {'color': 'green', 'speed': 'slow'}

point_value = alien_0.get('points', 'No point value assigned.')

print(point_value)

اگر کلید ‘points’ در دیکشنری وجود نداشته باشد، مقدار متناظر آن برگردانده می‌شود. در غیر این صورت مقدار پیش‌فرض تعیین‌شده را خواهید گرفت:

1
No point value assigned.
نکته
اگر نشان‌وند دوم را هنگام فراخوانی get() تعیین نکنید و کلید مورد نظر وجود نداشته باشد، پایتون مقدار None را برمی‌گرداند. مقدار ویژه‌ی None به معنای «وجود نداشتنِ هیچ مقداری» است. این خطا نیست بلکه مقداری ویژه است که فقدان یک مقدار را مشخّص می‌کند.

خودآزمایی

فرد

از یک دیکشنری برای ذخیره‌سازی اطّلاعات مربوط به فردی استفاده کنید که او را می‌شناسید. نام و نام خانوادگی، سن و شهرِ محل زندگی او را ضبط کنید. باید کلیدهایی مانند first_name، last_name، age و city داشته باشید. هر بخشِ اطّلاعاتی را که ذخیره کرده‌اید چاپ کنید.

اعداد محبوب

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

واژه‌نامه

دیکشنری پایتون می‌تواند به عنوان دیکشنری (واژه‌نامه‌ی) واقعی به کار رود.

  • پنج واژه‌ی مربوط به برنامه‌نویسی را در نظر بگیرید که در فصل‌های گذشته معنی آن‌ها را آموخته‌اید. از واژه‌ها به عنوان کلید و از معنی آن‌ها به جای مقدار استفاده کرده و آن‌ها را در دیکشنری ذخیره کنید.
  • هر واژه و معنای آن را به شکلی منظّم در خروجی چاپ کنید.

1.6.4 - حلقه زدن در یک دیکشنری

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

حلقه زدن در تمام جفت‌های کلید-مقدار

دیکشنری زیر نام کاربری، نام و نام خانوادگی یک فرد را ذخیره می‌کند:

1
2
3
4
5
6
7
8
9
user_0 = {
    'username': 'efermi',
    'first': 'enrico',
    'last': 'fermi',
}

for key, value in user_0.items():
    print(f"\nKey: {key}")
    print(f"Value: {value}")

برای نوشتن یک حلقه‌ی for برای دیکشنری، دو متغیّر را نام‌گذاری می‌کنیم که کلید و مقدار یک جفت کلید-مقدار را نگهداری می‌کند. بخش دوم عبارت for در 1، نام دیکشنری را در بر می‌گیرد که متد items() پس از آن آمده است. این متد لیستی از جفت‌های کلید-مقدار برمی‌گرداند:

1
2
3
4
5
6
7
8
Key: last
Value: Fermi

Key: first
Value: enrico

Key: username
Value: efermi

برای مثال favorite_languages.py که در صفحه‌ی قبل دیدیم، اگر داخل دیکشنری favorite_languages حلقه بزنید نام هر فرد و زبان برنامه‌نویسی مورد علاقه‌ی او را به دست می‌آورید:

1
2
3
4
5
6
7
8
9
favorite_languages = {
    'jen': 'python',
    'sarah': 'c',
    'edward': 'ruby',
    'phil': 'python',
}

for name, language in favorite_languages.items():
    print(f"{name.title()}'s favorite language is {language.title()}.")
1
2
3
4
5
6
7
Jen's favorite language is Python.

Sarah's favorite language is C.

Edward's favorite language is Ruby.

Phil's favorite language is Python.

حلقه زدن در تمام کلیدهای یک دیکشنری

در صورتی که مقدارهای یک دیکشنری مورد نظر نباشد، استفاده از متد keys() سودمند است:

1
2
3
4
5
6
7
8
9
favorite_languages = {
    'jen': 'python',
    'sarah': 'c',
    'edward': 'ruby',
    'phil': 'python',
}

for name in favorite_languages.keys():
    print(name.title())
1
2
3
4
5
6
7
Jen

Sarah

Edward

Phil

در واقع هنگام حلقه زدن در یک دیکشنری، رفتار پیش‌فرض همان حلقه زدن در کلیدها است، پس اگر به جای for name in favorite_languages.keys(): این کد را می‌نوشتید: for name in favorite_languages: نتیجه یکسان می‌شد.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
favorite_languages = {
    'jen': 'python',
    'sarah': 'c',
    'edward': 'ruby',
    'phil': 'python',
}

friends = ['phil', 'sarah']

for name in favorite_languages.keys():
    print(name.title())

    if name in friends:
        language = favorite_languages[name].title()
        print(f"\t{name.title()}, I see you love {language}!")
1
2
3
4
5
6
7
8
9
Hi Jen.

Hi Sarah.
    Sarah, I see you love C!

Hi Edward.

Hi Phil.
    Phil, I see you love Python!

همچنین برای این که بفهمید فردی در نظرسنجی شرکت کرده یا نه، می‌توانید از متد keys() استفاده کنید:

1
2
if 'erin' not in favorite_languages.keys():
    print("Erin, please take our poll!")
1
Erin, please take our poll!

حلقه زدن در کلید‌های دیکشنری با یک ترتیب خاص

از پایتون ۳.۷ به بعد حلقه زدن در دیکشنری اعضای آن را به همان ترتیبِ ذخیره شدن باز می‌گرداند. با این حال گاهی به حلقه زدن در دیکشنری با ترتیبی متفاوت نیاز دارید:

1
2
3
4
5
6
7
8
9
favorite_languages = {
    'jen': 'python',
    'sarah': 'c',
    'edward': 'ruby',
    'phil': 'python',
}

for name in sorted(favorite_languages.keys()):
    print(f"{name.title()}, thank you for taking the poll.")
1
2
3
4
5
6
7
Edward, thank you for taking the poll.

Jen, thank you for taking the poll.

Phil, thank you for taking the poll.

Sarah, thank you for taking the poll.

حلقه زدن در تمام مقدارهای یک دیکشنری

اگر بررسی مقدارهای یک دیکشنری برای شما اولویّت بیشتری دارد، می‌توانید از متد values() استفاده کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
favorite_languages = {
    'jen': 'python',
    'sarah': 'c',
    'edward': 'ruby',
    'phil': 'python',
}

print("The following languages have been mentioned:")

for language in favorite_languages.values():
    print(language.title())
1
2
3
4
5
6
7
8
9
The following languages have been mentioned:

Python

C

Python

Ruby

این رویکرد تمام مقدارهای دیکشنری را بیرون می‌کشد بدون این که به تکراری بودن یا نبودن آن‌ها اهمّیّتی دهد. برای این که زبان‌های برنامه‌نویسی انتخابی را بدون تکرار ببینیم، از مجموعه استفاده می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
favorite_languages = {
    'jen': 'python',
    'sarah': 'c',
    'edward': 'ruby',
    'phil': 'python',
}

print("The following languages have been mentioned:")

for language in set(favorite_languages.values()):
    print(language.title())
1
2
3
4
5
6
7
The following languages have been mentioned:

Python

C

Ruby
نکته

برای ساختن مستقیم یک مجموعه می‌توانید از آکُلاد استفاده کرده و اعضا را با کاما از هم جدا کنید:

1
2
3
>>> languages = {'python', 'ruby', 'python', 'c'}
>>> languages
{'ruby', 'python', 'c'}

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

خودآزمایی

واژه‌نامه ۲

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

رودخانه‌ها

یک دیکشنری بسازید که شامل سه رودخانه‌ی اصلی و کشوری که آن رودخانه‌ها در آن جریان دارند، باشد.

  • از یک حلقه برای چاپ کردن جمله‌ای در مورد رودخانه استفاده کنید، مثلاً: The Nile runs through Egypt.
  • از حلقه‌ای برای چاپ نام تک‌تکِ رودخانه‌های داخل دیکشنری استفاده کنید.
  • از حلقه‌ای برای چاپ نام تک‌تکِ کشورهای داخل دیکشنری استفاده کنید.

نظرسنجی

از کد favorite_languages.py استفاده کنید.

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

1.6.5 - تودرتوکردن

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

لیستی از دیکشنری‌ها

دیکشنری alien_0 شامل اطّلاعات مختلفی از یک موجود بیگانه است امّا دیگر برای ذخیره کردنِ اطّلاعاتِ موجودِ بیگانه‌ی دوم جا ندارد. یک راه ساختن لیستی از دیکشنری‌هاست:

1
2
3
4
5
6
7
8
alien_0 = {'color': 'green', 'points': 5}
alien_1 = {'color': 'yellow', 'points': 10}
alien_2 = {'color': 'red', 'points': 15}

aliens = [alien_0, alien_1, alien_2]

for alien in aliens:
    print(alien)
1
2
3
4
5
{'color': 'green', 'points': 5}

{'color': 'yellow', 'points': 10}

{'color': 'red', 'points': 15}

یک مثال واقعی‌تر ساختن بیشتر از سه موجود به کمک کدی است که تولید موجودات بیگانه را به طور خودکار انجام می‌دهد. در مثال زیر از range() برای ساختن ناوگانی از ۳۰ موجود بیگانه استفاده می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Make an empty list for storing aliens.
aliens = []

# Make 30 green aliens.
for alien_number in range(30):
    new_alien = {'color': 'green', 'points': 5, 'speed': 'slow'}
    aliens.append(new_alien)

# Show the first 5 aliens.
for alien in aliens[:5]:
    print(alien)

print("...")

# Show how many aliens have been created.
print(f"Total number of aliens: {len(aliens)}")
1
2
3
4
5
6
7
8
{'speed': 'slow', 'color': 'green', 'points': 5}
{'speed': 'slow', 'color': 'green', 'points': 5}
{'speed': 'slow', 'color': 'green', 'points': 5}
{'speed': 'slow', 'color': 'green', 'points': 5}
{'speed': 'slow', 'color': 'green', 'points': 5}
...

Total number of aliens: 30

این موجودهای بیگانه همگی خصوصیّاتی مشترک دارند ولی پایتون هر کدام از آن‌ها را یک شیء جدا در نظر می‌گیرد که امکان تغییر دادن جداگانه‌ی هر کدام از آن‌ها در آینده فراهم است.

چگونه می‌توانید با چنین گروهی از موجودات بیگانه کار کنید؟ تصوّر کنید که بخشی از بازی موجوداتی دارد که رنگ آن‌ها عوض می‌شود:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
aliens = []

for alien_number in range(30):
    new_alien = {'color': 'green', 'points': 5, 'speed': 'slow'}
    aliens.append(new_alien)

for alien in aliens[:3]:
    if alien['color'] == 'green':
        alien['color'] = 'yellow'
        alien['speed'] = 'medium'
        alien['points'] = 10

# Show the first 5 aliens.
for alien in aliens[:5]:
    print(alien)

print("...")
1
2
3
4
5
6
{'speed': 'medium', 'color': 'yellow', 'points': 10}
{'speed': 'medium', 'color': 'yellow', 'points': 10}
{'speed': 'medium', 'color': 'yellow', 'points': 10}
{'speed': 'slow', 'color': 'green', 'points': 5}
{'speed': 'slow', 'color': 'green', 'points': 5}
...

این حلقه را می‌توان با افزودن یک بخش elif گسترش داد تا موجودهای زردرنگ را قرمزرنگ، سریع و ۱۵ امتیازی کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
for alien in aliens[0:3]:
    if alien['color'] == 'green':
        alien['color'] = 'yellow'
        alien['speed'] = 'medium'
        alien['points'] = 10

    elif alien['color'] == 'yellow':
        alien['color'] = 'red'
        alien['speed'] = 'fast'
        alien['points'] = 15

لیستی در یک دیکشنری

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Store information about a pizza being ordered.
pizza = {
    'crust': 'thick',
    'toppings': ['mushrooms', 'extra cheese'],
}

# Summarize the order.
print(f"You ordered a {pizza['crust']}-crust pizza "
      "with the following toppings:")

for topping in pizza['toppings']:
    print("\t" + topping)
1
2
3
You ordered a thick-crust pizza with the following toppings:
	mushrooms
	extra cheese

هر گاه به این نیاز داشتید که در یک دیکشنری بیش از یک مقدار را به یک کلید نسبت دهید، می‌توانید لیست را در دیکشنری به کار ببرید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
favorite_languages = {
    'jen': ['python', 'ruby'],
    'sarah': ['c'],
    'edward': ['ruby', 'go'],
    'phil': ['python', 'haskell'],
}

for name, languages in favorite_languages.items():
    print(f"\n{name.title()}'s favorite languages are:")

    for language in languages:
        print(f"\t{language.title()}")
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Jen's favorite languages are:
	Python
	Ruby

Sarah's favorite languages are:
	C

Phil's favorite languages are:
	Python
	Haskell

Edward's favorite languages are:
	Ruby
	Go
نکته
نباید بیش از اندازه لیست‌ها و دیکشنری‌ها را در هم به کار ببرید. اگر بیش از آن‌که در مثال‌های این فصل دیدید، نیاز به داخل کردن این موارد در یکدیگر پیدا کردید، احتمالاً راه حل ساده‌تری برای حل آن مساله وجود دارد.

یک دیکشنری در یک دیکشنری دیگر

امکان قرار دادن یک دیکشنری درون یک دیکشنری دیگر وجود دارد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
users = {
    'aeinstein': {
        'first': 'albert',
        'last': 'einstein',
        'location': 'princeton',
    },

    'mcurie': {
        'first': 'marie',
        'last': 'curie',
        'location': 'paris',
    },
}

for username, user_info in users.items():
    print(f"\nUsername: {username}")
    full_name = f"{user_info['first']} {user_info['last']}"
    location = user_info['location']
    print(f"\tFull name: {full_name.title()}")
    print(f"\tLocation: {location.title()}")
1
2
3
4
5
6
7
Username: aeinstein
	Full name: Albert Einstein
	Location: Princeton

Username: mcurie
	Full name: Marie Curie
	Location: Paris
نکته
توجّه کنید که ساختار دیکشنری هر کاربر یکسان است. هر چند چنین چیزی در پایتون اجباری نیست، امّا این ساختار یکسان تودرتو کردن دیکشنری‌ها را ساده‌تر می‌کند.

خودآزمایی

مردم

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

حیوانات خانگی

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

مکان‌های مورد علاقه

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

نام‌های مورد علاقه

برنامه‌ی تمرین ۶-۲ را تغییر دهید به طوری‌که هر فرد بتواند بیش از یک عدد دلخواه داشته باشد. سپس نام هر فرد و اعداد مورد علاقه‌ی او را چاپ کنید.

شهرها

یک دیکشنری به نام cities بسازید. نام سه شهر را به عنوان کلید‌های آن در نظر بگیرید. برای اطّلاعات هر شهر یک دیکشنری بسازید که شامل این موارد باشد: کشوری که آن شهر در آن است، جمعیّت تقریبی و یک دانستنی در مورد آن.

توسعه‌ها

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

1.6.6 - خلاصه

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

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


1.7 - ورودی کاربر و حلقه‌های while

1.7.1 - تابع input() چگونه کار می‌کند؟

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

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

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

با وجودِ دو قابلیّتِ کار کردن با ورودی کاربر و امکان کنترل مدّت‌زمان اجرای برنامه، می‌توانید برنامه‌های کاملاً تعاملی بنویسید.

تابع input() چگونه کار می‌کند؟

تابع input() برنامه‌ی شما را متوقّف می‌کند و منتظر می‌ماند تا کاربر متنی وارد کند. پایتون به محضِ دریافت ورودیِ کاربر، آن را به متغیّری منسوب می‌کند تا کار کردن با آن ساده شود.

برای نمونه برنامه‌ی زیر از کاربر می‌خواهد تا متنی را وارد کند و سپس همان را به کاربر نمایش می‌دهد:

1
2
3
message = input("Tell me something, and I will repeat it back to you: ")

print(message)

تابع input() تنها یک نشان‌وند می‌پذیرد: اعلان یا دستور‌العمل‌هایی، که می‌خواهیم به کاربر نشان دهیم تا بداند چه باید بکند. در این مثال، هنگامی که پایتون خطّ اوّل را اجرا می‌کند کاربر اعلانِ Tell me something, and I will repeat it back to you را می‌بیند. برنامه منتظر می‌ماند تا کاربر پاسخ خود را وارد کند و پس از فشرده شدن دکمه‌ی ENTER کار را ادامه می‌دهد. پاسخ به متغیّر message منسوب می‌شود و سپس print(message) ورودی را به کاربر برمی‌گرداند:

1
2
3
Tell me something, and I will repeat it back to you: Hello everyone!

Hello everyone!
نکته
Sublime Text و بسیاری دیگر از ویرایشگرها برنامه‌هایی را که از کاربر ورودی می‌خواهند، اجرا نمی‌کنند. می‌توانید از این ویرایشگرها برای نوشتن چنین برنامه‌هایی استفاده کنید امّا باید آن‌ها را در یک ترمینال اجرا کنید.

1.7.2 - نوشتن اعلان‌های شفّاف

هر بار که از تابع input() استفاده می‌کنید باید پیامی شفاّف و قابل درک را ضمیمه کنید تا به کاربر بگوید دقیقاً به دنبال چه اطّلاعاتی هستید. هر عبارتی که به کاربر بگوید که چه باید وارد کند، مناسب است. برای مثال:

1
2
3
name = input("Please enter your name: ")

print(f"\nHello, {name}!")

یک فاصله‌ی خالی بعد از اعلان‌های خود بگذارید (در این مثال بعد از دونقطه) تا اعلان را از پاسخ کاربر جدا کند و محلِّ وارد کردن متن را برای او روشن کند. برای مثال:

1
2
3
Please enter your name: Eric

Hello, Eric!

گاهی نیاز دارید اعلانی طولانی‌تر از یک خط بنویسید. مثلاً می‌خواهید به کاربر بگویید که چرا به دنبال چنین ورودی‌ای هستید. می‌توانید اعلان خود را به یک متغیّر نسبت دهید و آن متغیّر را به تابع input() بدهید. با این کار ساختن اعلان‌هایی در چند خط و سپس نوشتن یک عبارت input() مرتّب، ممکن می‌شود.

1
2
3
4
5
6
prompt = "If you tell us who you are, we can personalize the messages you see."
prompt += "\nWhat is your first name? "

name = input(prompt)

print(f"\nHello, {name}!")

این مثال یکی از روش‌های ساختن رشته‌ی چندخطّی را نشان می‌دهد. خطّ اوّل بخش اوّل پیام را به متغیّر prompt نسبت می‌دهد. در خطّ دوم عملگر += رشته‌ی متغیّر prompt را گرفته و رشته‌ی جدید را به انتهای آن اضافه می‌کند.

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

1
2
3
4
5
If you tell us who you are, we can personalize the messages you see.

What is your first name? Eric

Hello, Eric!

استفاده از int() برای پذیرفتن ورودی‌های عددی

هنگامی که از تابع input() استفاده می‌کنید، پایتون هر آن چه را که کاربر وارد کرده به عنوان رشته تفسیر می‌کند. این نشستِ مفسّر را در نظر بگیرید که از کاربر سنّ او را می‌پرسد:

1
2
3
4
>>> age = input("How old are you? ")
How old are you? 21
>>> age
'21'

کاربر عدد ۲۱ را وارد می‌کند امّا زمانی که از پایتون مقدار age را می‌خواهیم ‘21’ را برمی‌گرداند که بیانِ رشته‌ایِ مقدارِ عددی است که وارد شده. ما می‌دانیم که پایتون ورودی را به عنوان رشته تفسیر کرده زیرا عدد را داخل علامت نقل قول قرار داده است. اگر چاپ کردنِ ورودی همه‌ی کاری باشد که می‌خواهید انجام دهید، این کد به درستی کار می‌کند. امّا اگر بخواهید از آن به عنوان عدد استفاده کنید به خطا می‌خورید:

1
2
3
4
5
6
>>> age = input("How old are you? ")
How old are you? 21
>>> age >= 18
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unorderable types: str() >= int()

زمانی که در 1 تلاش می‌کنید ورودی را با عددی مقایسه کنید، پایتون اعلام خطا می‌کند زیرا نمی‌تواند رشته را با یک عدد صحیح مقایسه کند: رشته‌ی ‘21’ که به age منسوب شده قابلِ مقایسه با مقدارِ عددیِ ۱۸ نیست.

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

1
2
3
4
5
>>> age = input("How old are you? ")
How old are you? 21
>>> age = int(age)
>>> age >= 18
True

در این مثال زمانی که در اعلانِ ورودی ۲۱ را وارد می‌کنیم، پایتون آن را رشته در نظر می‌گیرد امّا در ادامه در 1 این مقدار به کمک int()، به نمایش عددی تبدیل می‌شود. اکنون پایتون می‌تواند مقایسه‌ی عددی را اجرا کند.

چگونه از int() در یک برنامه‌ی واقعی استفاده کنیم؟ برنامه‌ای را در نظر بگیرید که تعیین می‌کند آیا قدّ افراد برای سوار شدن به یک ترن هوایی به اندازه‌ی کافی بلند است یا نه:

1
2
3
4
5
6
7
8
height = input("How tall are you, in inches? ")
height = int(height)

if height >= 48:
    print("\nYou're tall enough to ride!")

else:
    print("\nYou'll be able to ride when you're a little older.")

این برنامه می‌تواند height را با ۴۸ مقایسه کند زیرا height = int(height) مقدار ورودی را پیش از انجامِ مقایسه به یک نمایش عددی تبدیل می‌کند. اگر عددِ واردشده بزرگتر از یا مساوی با ۴۸ باشد به کاربر می‌گوییم که قد او مناسب است:

1
2
3
How tall are you, in inches? 71

You're tall enough to ride!

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

1.7.3 - عملگر پیمانه

یک ابزار سودمند برای کار کردن با داده‌های عددی عملگر پیمانه (%) است که عددی را بر یک عدد دیگر تقسیم کرده و باقیمانده‌ی آن را برمی‌گرداند:

1
2
3
4
5
6
7
8
>>> 4 % 3
1
>>> 5 % 3
2
>>> 6 % 3
0
>>> 7 % 3
1

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

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

1
2
3
4
5
6
7
8
number = input("Enter a number, and I'll tell you if it's even or odd: ")
number = int(number)

if number % 2 == 0:
    print(f"\nThe number {number} is even.")

else:
    print(f"\nThe number {number} is odd.")

اعدادِ زوج همواره برای ۲ بخش‌پذیرند پس اگر باقیمانده‌ی تقسیم یک عدد بر ۲، ۰ باشد (در اینجا if number % 2 == 0) آن عدد زوج است. در غیر این صورت فرد خواهد بود:

1
2
3
Enter a number, and I'll tell you if it's even or odd: 42

The number 42 is even.

خودآزمایی

برنامه‌ی ماشین کرایه‌ای

برنامه‌ای بنویسید که از کاربر بپرسد چه‌جور ماشین کرایه‌ای دوست دارد. پیامی درباره‌ی آن ماشین چاپ کنید، مثلاً: “Let me see if I can find you a Subaru.”.

جاگرفتن در رستوران

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

مضرب‌های ده

از کاربر عددی بپرسید و سپس گزارش دهید که آیا آن عدد مضربی از ۱۰ است یا نه؟

1.7.4 - آشنایی با حلقه‌های while

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

حلقه‌ی while در عمل

می‌توانید از یک حلقه‌ی while برای شمردن دنباله‌ای از اعداد استفاده کنید. برای مثال حلقه‌ی while زیر از ۱ تا ۵ می‌شمرد:

1
2
3
4
5
current_number = 1

while current_number <= 5:
    print(current_number)
    current_number += 1

در خطّ اوّل شمردن از ۱ را با انتساب مقدار ۱ به متغیّر current_number شروع می‌کنیم. سپس حلقه‌ی while طوری تنظیم می‌شود که تا زمانِ کمتر یا مساوی بودنِ current_number از ۵، اجرا شود. کدِ داخلِ حلقه مقدارِ current_number را چاپ کرده و سپس با += آن را یکی زیاد می‌کند. (عملگر += کوتاه‌شده‌ی current_number = current_number + 1 است.)

پایتون تا زمانی حلقه را اجرا می‌کند که شرطِ current_number <= 5 برقرار باشد. پایتون ۱ را چاپ می‌کند سپس یکی به آن اضافه می‌کند تا عدد فعلی ۲ شود. چون ۲ کمتر از ۵ است، آن را چاپ کرده و مجدّداً یکی به آن اضافه می‌کند تا عدد فعلی ۳ شود و به همین ترتیب ادامه می‌دهد. به محض بیشتر شدنِ مقدارِ current_number از ۵، اجرای حلقه متوقّف شده و برنامه پایان می‌یابد:

1
2
3
4
5
1
2
3
4
5

بیشتر برنامه‌هایی که هرروزه استفاده می‌کنید شامل حلقه‌های while‌اند. برای مثال، یک بازی به حلقه‌ی while احتیاج دارد که تا زمانی که می‌خواهید به بازی ادامه دهید، به اجراشدن ادامه دهد و به محض رسیدنِ درخواستِ خروج اجرا شدن را متوقّف کند.

اجازه دادن به کاربر برای انتخاب زمان خروج

برای این که برنامه‌ی parrot.py تا زمانی که کاربر می‌خواهد، اجرا شود می‌توانیم بیشتر خط‌های آن را در یک حلقه‌ی while قرار دهیم. یک مقدارِ خروج تعریف می‌کنیم و تا زمانی که کاربر آن را وارد نکرده باشد، اجرای برنامه ادامه می‌یابد:

1
2
3
4
5
6
7
8
prompt = "\nTell me something, and I will repeat it back to you:"
prompt += "\nEnter 'quit' to end the program. "

message = ""

while message != 'quit':
    message = input(prompt)
    print(message)

در 1 اعلانی تعریف می‌کنیم تا به کاربران بگوییم که دو گزینه پیشِ رو دارند: وارد کردن یک پیام یا وارد کردن مقدارِ خروج (در اینجا، ‘quit’). سپس در 2 متغیّر message را مقداردهی می‌کنیم تا هر مقداری را که کاربر وارد می‌کند، پیگیری کنیم. متغیّر message را یک رشته‌ی خالی "" قرار می‌دهیم تا پایتون در اوّلین اجرای حلقه چیزی برای مقایسه کردن داشته باشد. حلقه‌ی while (3) تا زمانی که مقدار message برابر با ‘quit’ نباشد اجرا می‌شود.

در اوّلین ورود به حلقه، message تنها یک رشته‌ی خالی است، پس پایتون می‌تواند وارد حلقه شود. در message = input(prompt) پایتون اعلان را نمایش می‌دهد و منتظر پاسخ کاربران می‌ماند. هر آن‌چه وارد کنند به message نسبت داده شده و چاپ می‌شود؛ سپس پایتون شرطِ عبارتِ while را دوباره ارزیابی می‌کند. سرانجام زمانی که کاربران ‘quit’ را وارد می‌کنند، پایتون اجرای حلقه‌ی while را متوقّف کرده و برنامه پایان می‌یابد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Tell me something, and I will repeat it back to you:
Enter 'quit' to end the program. Hello everyone!

Hello everyone!

Tell me something, and I will repeat it back to you:
Enter 'quit' to end the program. Hello again.

Hello again.

Tell me something, and I will repeat it back to you:
Enter 'quit' to end the program. quit

quit

این برنامه به خوبی کار می‌کند، تنها ایراد آن این است که واژه‌ی ‘quit’ را مثل یک پیام واقعی چاپ می‌کند. یک آزمون if ساده این مشکل را حل می‌کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
prompt = "\nTell me something, and I will repeat it back to you:"
prompt += "\nEnter 'quit' to end the program. "

message = ""

while message != 'quit':
    message = input(prompt)

    if message != 'quit':
        print(message)

اکنون برنامه پیش از نمایش پیام یک بررسی سریع انجام می‌دهد و تنها در صورتی آن را چاپ می‌کند که با مقدارِ خروج برابر نباشد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Tell me something, and I will repeat it back to you:
Enter 'quit' to end the program. Hello everyone!

Hello everything!

Tell me something, and I will repeat it back to you:
Enter 'quit' to end the program. Hello again.

Hello again.

Tell me something, and I will repeat it back to you:
Enter 'quit' to end the program. quit

1.7.5 - به‌کارگیری پرچم

در مثالِ پیشین برنامه را واداشتیم که تا زمانِ برقرار بودنِ شرطی مشخّص، به انجام وظایفی بپردازد. امّا در برنامه‌های پیچیده که در‌آنها رخدادهای مختلفی ممکن است باعث توقّف اجرا شوند، چه باید کرد؟

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

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

بیایید پرچمی به parrot.py از بخش قبل، اضافه کنیم. این پرچم که آن را active می‌نامیم (و هر نامی که بخواهید می‌توانید به آن بدهید) نگاه می‌کند که آیا اجرای برنامه باید ادامه یابد یا نه:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
prompt = "\nTell me something, and I will repeat it back to you:"
prompt += "\nEnter 'quit' to end the program. "

active = True

while active:
    message = input(prompt)

    if message == 'quit':
        active = False

    else:
        print(message)

در 1 متغیّر active را برابر True می‌گذاریم تا برنامه در وضعیّت فعّال قرار گیرد. انجام چنین کاری عبارت while را ساده‌تر می‌کند زیرا هیچ مقایسه‌ای در خود عبارت صورت نمی‌پذیرد؛ منطق برنامه در دیگر بخش‌های آن مورد نظر قرار می‌گیرد. تا زمانی که متغیّر active مقدار True دارد اجرای حلقه ادامه می‌یابد (2).

به محض این که کاربران ورودی خود را اعمال کنند مقدار message را در گزاره‌ی if داخل حلقه‌ی while بررسی می‌کنیم. اگر در 3 کاربر ‘quit’ را وارد کرد، active را False می‌کنیم و حلقه‌ی while متوقّف می‌شود. اگر کاربر چیزی جز ‘quit’ وارد کند، 4، ورودی او را در قالب پیامی چاپ می‌کنیم.

خروجی این برنامه مشابه خروجی مثال قبلی است. حال که ما پرچمی داریم که نشان می‌دهد کل برنامه در وضعیّت فعّال است یا نه، افزودن آزمون‌های بیشتر (مثلاً با عبارت‌های elif) برای رخ‌دادهایی که باید active را False کنند، ساده خواهد بود.

1.7.6 - استفاده از break و continue

استفاده از break برای خروج از حلقه

برای این که بدون اجرای کدهای باقیمانده در حلقه‌ی while و صرفِ نظر از نتایج هر آزمون شرطی‌ای، فوراً از آن خارج شوید از break استفاده کنید. عبارت break جریان برنامه‌ی شما را هدایت می‌کند؛ می‌توانید از آن برای مدیریّت اجرا شدن یا نشدن خط‌هایی از کد استفاده کنید.

برای نمونه برنامه‌ای را در نظر بگیرید که از کاربران می‌پرسد از چه مکان‌هایی دیدن کرده‌اند. در این برنامه به محضی که کاربر مقدار ‘quit’ را وارد می‌کند، حلقه‌ی while را با فراخوانی break متوقّف می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
prompt = "\nPlease enter the name of a city you have visited:"
prompt += "\n(Enter 'quit' when you are finished.) "

while True:
    city = input(prompt)

    if city == 'quit':
        break

    else:
        print(f"I'd love to go to {city.title()}!")

حلقه‌ای که مانند 1 با while True آغاز شود تا ابد اجرا می‌شود مگر این که به عبارت break برسد. پرسیدنِ نام شهرهایی که کاربران در آن‌ها بوده‌اند تا زمانی که ‘quit’ را وارد کنند در حلقه‌ی این برنامه ادامه می‌یابد. هنگامی که کاربران ‘quit’ را وارد می‌کنند عبارت break اجرا می‌شود که باعث می‌شود پایتون از حلقه خارج شود:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Please enter the name of a city you have visited:
(Enter 'quit' when you are finished.) New York

I'd love to go to New York!

Please enter the name of a city you have visited:
(Enter 'quit' when you are finished.) San Francisco

I'd love to go to San Francisco!

Please enter the name of a city you have visited:
(Enter 'quit' when you are finished.) quit
نکته
می‌توانید از break در تمام حلقه‌های پایتون استفاده کنید. برای مثال با به‌کارگیری break می‌توانید از حلقه‌ی for‌ای که داخل یک لیست یا دیکشنری در حال کار کردن است، خارج شوید.

به‌کارگیری continue در یک حلقه

به جای این که بدونِ اجرای باقیمانده‌ی کد، از حلقه‌ای به کلّی خارج شوید، می‌توانید از عبارت continue استفاده کنید تا بر اساس نتیجه‌ی یک آزمون شرطی به ابتدای حلقه برگردید. برای نمونه حلقه‌ای را در نظر بگیرید که از ۱ تا ۱۰ می‌شمرد امّا تنها اعداد فردِ این بازه را چاپ می‌کند:

1
2
3
4
5
6
7
8
9
current_number = 0

while current_number < 10:
    current_number += 1

    if current_number % 2 == 0:
        continue

    print(current_number)

ابتدا مقدار current_number را برابر با ۰ می‌گذاریم. چون این مقدار کمتر از ۱۰ است پایتون وارد حلقه می‌شود. با ورود به حلقه در 1 مقدار را یکی افزایش می‌دهیم، پس current_number برابر با ۱ خواهد بود. سپس گزاره‌ی if باقیمانده‌ی current_number بر ۲ را بررسی می‌کند. اگر باقیمانده ۰ باشد (یعنی current_number بر ۲ بخش‌پذیر باشد)، عبارت continue از پایتون می‌خواهد که باقیِ حلقه‌ی را نادیده بگیرد و به ابتدای آن برگردد. اگر عدد فعلی برای ۲ بخش‌پذیر نباشد ادامه‌ی حلقه اجرا شده و پایتون آن را چاپ می‌کند:

1
2
3
4
5
1
3
5
7
9

1.7.7 - پرهیز از حلقه‌های بی‌پایان

هر حلقه‌ی while به روشی برای توقّف اجرا نیاز دارد تا اجرای آن برای همیشه ادامه نیابد. برای مثال این حلقه‌ی شمارنده باید از ۱ تا ۵ بشمارد:

1
2
3
4
5
x = 1

while x <= 5:
    print(x)
    x += 1

امّا اگر همان‌طور که در ادامه می‌آید، سهواً خط x += 1 را حذف کنید حلقه تا ابد اجرا می‌شود:

1
2
3
4
5
# This loop runs forever!
x = 1

while x <= 5:
    print(x)

اکنون مقدار x از ۱ شروع می‌شود امّا هرگز تغییر نمی‌کند. در نتیجه آزمون شرطی x <= 5 همواره True ارزیابی می‌شود و حلقه‌ی while برای همیشه اجرا شده و دنباله‌ای از ۱ها چاپ می‌کند:

1
2
3
4
5
1
1
1
1
--snip--

هر برنامه‌نویسی گاه به گاه و به صورت اتّفاقی حلقه‌ی while بی‌پایان می‌نویسد، به ویژه زمانی که حلقه‌های برنامه شرط‌های خروج ظریف و دیرفهمی دارند. اگر برنامه‌ی شما در یک حلقه‌ی بی‌پایان گیر کرد CTRL-C را فشار دهید یا به سادگی پنجره‌ی ترمینالی را که خروجی‌ برنامه را نشان می‌دهد ببندید.

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

نکته
Sublime Text و برخی دیگر از ویرایشگرها یک پنجره‌ی خروجی درونی دارند که ممکن است متوقّف کردن یک حلقه‌ی بی‌پایان را مشکل کند و شما را مجبور کند که خود ویرایشگر را برای پایان دادن به حلقه ببندید. سعی کنید پیش از فشردن CTRL+C روی ناحیه‌ی پنجره‌ی خروجی کلیک کنید تا قادر باشید حلقه‌ی بی‌پایان را متوقّف کنید.

خودآزمایی

مواد روی پیتزا

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

بلیت‌های سینما

یک سالن سینما قیمت بلیت‌ها را بر اساس سنّ فرد تعیین می‌کند. اگر فرد کمتر از ۳ سال سن داشته باشد، بلیت او رایگان است؛ اگر بین ۳ تا ۱۲ سال داشته باشند باید ۱۰ دلار بپردازند و اگر بیش از ۱۲ سال سن داشته باشند بلیت آن‌ها ۱۵ دلار خواهد بود. حلقه‌ای بنویسید که در آن سن کاربران پرسیده شده و سپس قیمت بلیت آن‌ها اعلام می‌شود.

سه خروجی

نسخه‌های متفاوتی از تمرین ۷-۴ یا ۷-۵ بنویسید که کارهای زیر را حدّاقل یک بار انجام دهد:

  • در عبارت while یک آزمون شرطی بیاورید که اجرای حلقه را متوقّف کند.
  • از یک متغیّر فعّال بودن استفاده کنید تا مدّت زمان اجرای حلقه را کنترل کنید.
  • هنگامی که کاربر مقدار ‘quit’ را وارد می‌کند، از یک عبارت break برای خروج از حلقه استفاده کنید.

بی‌پایانی

حلقه‌ای بنویسید که هیچ‌گاه پایان نمی‌یابد و آن را اجرا کنید. (برای پایان دادن به این حلقه CTRL+C را فشار دهید یا پنجره‌ای که خروجی را نشان می‌دهد ببندید.)

1.7.8 - به‌کارگیری حلقه‌ی while در لیست‌ها و دیکشنری‌ها

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

به‌کارگیری یک حلقه‌ی for برای حلقه زدن در یک لیست موثّر است امّا نباید لیست را در داخل این حلقه تغییر داد چون پایتون در پیگیری موارد داخل لیست دچار مشکل می‌شود. اگر می‌خواهید هنگام کار کردن با یک لیست آن را تغییر دهید، از حلقه‌ی while استفاده کنید.

انتقال دادن اعضا از یک لیست به لیستِ دیگر

لیستی از کاربران تازه ثبت‌نام شده‌ی یک وب‌سایت را در نظر بگیرید که هنوز تایید نشده‌اند. چگونه می‌توانیم پس از تایید این کاربران، آن‌ها را به لیستی مجزّا شامل کاربران تاییدشده منتقل کنیم؟ یک روش استفاده کردن از حلقه‌ی while است تا به کمک آن کاربران را از لیست کاربران تاییدنشده بیرون بکشیم، تایید کنیم و سپس به لیستی جداگانه از کاربران تاییدشده اضافه کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Start with users that need to be verified,
# and an empty list to hold confirmed users.
unconfirmed_users = ['alice', 'brian', 'candace']
confirmed_users = []

# Verify each user until there are no more unconfirmed users.
# Move each verified user into the list of confirmed users.
while unconfirmed_users:
    current_user = unconfirmed_users.pop()

    print(f"Verifying user: {current_user.title()}")
    confirmed_users.append(current_user)

# Display all confirmed users.
print("\nThe following users have been confirmed:")
for confirmed_user in confirmed_users:
    print(confirmed_user.title())

در 1 با لیستِ کاربرانِ تاییدنشده (Alice، Brian و Candace) و لیستی خالی برای نگهداری کاربران تاییدشده شروع می‌کنیم. حلقه‌ی while در 2 تا زمانی که لیست unconfirmed_users خالی نباشد به کار ادامه می‌دهد. داخل این حلقه و در 3، تابع pop() هر بار کاربران تاییدنشده را از انتهای unconfirmed_users برمی‌دارد. اینجا چون Candace آخرین کاربر در unconfirmed_users است، نامش اوّلین نامی خواهد بود که از لیست حذف، به متغیّر current_user منسوب و به لیست confirmed_users اضافه می‌شود (4). بعدی Brian و سپس Alice خواهد بود.

فرآیند تایید کاربران را با چاپ کردن یک پیام تایید شبیه‌سازی می‌کنیم و سپس کاربران را به لیست تاییدشده‌ها می‌افزاییم. همزمان با کوچک شدن لیست کاربران تاییدنشده، لیست تایید‌شده‌ها بزرگ‌تر می‌شود:

1
2
3
4
5
6
7
8
Verifying user: Candace
Verifying user: Brian
Verifying user: Alice

The following users have been confirmed:
Candace
Brian
Alice

حذف کردن مقدارهای مشخّصی از یک لیست

در فصل ۳ از remove() برای حذف کردن یک مقدار مشخّص از لیست استفاده کردیم. این تابع به این دلیل کار می‌کرد که مقدار مورد نظر ما تنها یک بار در لیست ظاهر شده بود. امّا اگر بخواهیم تمام نمونه‌های یک مقدار را از لیستی حذف کنیم، چه؟

فرض کنید لیستی از حیوانات خانگی دارید که ‘cat’ در آن بارها تکرار شده است. برای حذف تمام مشابه‌های این مقدار می‌توانید یک حلقه‌ی while را تا زمانی اجرا کنید که هیچ موردی از ‘cat’ در لیست باقی نمانده باشد:

1
2
3
4
5
6
7
8
pets = ['dog', 'cat', 'dog', 'goldfish', 'cat', 'rabbit', 'cat']

print(pets)

while 'cat' in pets:
    pets.remove('cat')

print(pets)

با لیستی شامل چند عضو مشابه ‘cat’ شروع می‌کنیم. پس از چاپ کردن این لیست، پایتون وارد حلقه‌ی while می‌شود زیرا مقدار ‘cat’ را دستِ کم یک بار در لیست می‌یابد. پس از ورود به حلقه، پایتون اوّلین نمونه‌ی ‘cat’ را حذف کرده و به خطّ while برمی‌گردد. سپس از آن‌جا که هنوز ‘cat’ در لیست وجود دارد، مجدّداً وارد حلقه می‌شود. تا زمانی که دیگر هیچ مقداری مشابه ‘cat’ در حلقه موجود نباشد، هر نمونه‌ی آن را حذف می‌کند:

1
2
3
['dog', 'cat', 'dog', 'goldfish', 'cat', 'rabbit', 'cat']

['dog', 'dog', 'goldfish', 'rabbit']

پر کردن یک دیکشنری با ورودی کاربر

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
responses = {}

# Set a flag to indicate that polling is active.
polling_active = True

while polling_active:
    # Prompt for the person's name and response.
    name = input("\nWhat is your name? ")
    response = input("Which mountain would you like to climb someday? ")

    # Store the response in the dictionary.
    responses[name] = response

    # Find out if anyone else is going to take the poll.
    repeat = input("Would you like to let another person respond? (yes/ no) ")

    if repeat == 'no':
        polling_active = False

# Polling is complete. Show the results.
print("\n--- Poll Results ---")
for name, response in responses.items():
    print(f"{name} would like to climb {response}.")

این برنامه ابتدا یک دیکشنری خالی (responses) تعریف می‌کند و پرچمی (polling_active) را برای نمایش فعّال بودن نظرسنجی مقداردهی می‌کند. تا زمانی که polling_active برابر با True باشد، پایتون کد داخل حلقه‌ی while را اجرا می‌کند.

در داخل حلقه به کاربر اعلام می‌شود که نام و کوه مورد علاقه‌ی خود برای نوردیدن را وارد کند. این اطّلاعات در دیکشنری responses ذخیره می‌شود و از کاربر پرسیده می‌شود که آیا نظرسنجی ادامه یابد یا نه. اگر yes وارد شود، برنامه بار دیگر وارد حلقه‌ی while می‌شود. اگر no وارد شود پرچم polling_active به False تغییر می‌یابد و اجرای حلقه‌ی while متوقّف می‌شود. سرانجام بخش آخر کد نتایج نظرسنجی را نشان می‌دهد.

اگر برنامه را اجرا کرده و چند پاسخ نمونه وارد کنید، باید خروجی‌ای مانند زیر ببینید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
What is your name? Eric
Which mountain would you like to climb someday? Denali
Would you like to let another person respond? (yes/ no) yes

What is your name? Lynn
Which mountain would you like to climb someday? Devil's Thumb
Would you like to let another person respond? (yes/ no) no

--- Poll Results ---
Lynn would like to climb Devil's Thumb.
Eric would like to climb Denali.

خودآزمایی

غذافروشی

لیستی به نام sandwich_orders بسازید و آن را با نام چند نوع ساندویچ پر کنید. سپس لیستی خالی به نام finished_sandwiches بسازید. در لیست سفارش‌های ساندویچ حلقه بزنید و برای هر کدام پیامی چاپ کنید، مثلاً I made your tuna sandwich.. با درست شدن هر ساندویچ، سفارش آن را به لیست‌های ساندویچ‌های تحویل‌داده‌شده منتقل کنید. پس از آماده شدن تمام ساندویچ‌ها پیامی چاپ کنید و تمام ساندویچ‌های آماده‌شده را فهرست کنید.

تمام شدنِ پاسترامی

لیستِ sandwich_orders در تمرین ۷-۸ را دوباره به کار بگیرید و مطمئن شوید ساندویچ ‘pastrami’ دستِ کم ۳ بار در آن تکرار شده باشد. در ابتدای برنامه‌ی خود پیامی چاپ کنید حاکی از آن‌که موجودیِ پاسترامی در این غذاخوری به پایان رسیده است، سپس به کمک یک حلقه‌ی while تمام موارد ‘pastrami’ را از sandwich_orders حذف کنید. مطمئن شوید که در لیست ساندویچ‌های آماده هیچ نمونه‌ای از ساندویچ پاسترامی نباشد.

سفر رویایی

برنامه‌ای بنویسید که از کاربران درباره‌ی سفر رویایی آن‌ها نظرسنجی کند. اعلانی مانند این چاپ کنید: If you could visit one place in the world, where would you go?. بخشی از کد را به چاپ کردن نتایج نظرسنجی اختصاص دهید.

1.7.9 - خلاصه

در این فصل آموختید که چگونه input() را به کار بگیرید تا امکان وارد کردن اطّلاعات را برای کاربران فراهم کنید. یاد گرفتید که چگونه با هر دو نوع ورودی رشته و عددی کار کنید و با حلقه‌های while اجرای برنامه‌های خود را تا زمانی که کاربر بخواهد ادامه دهید. چندین روش را برای کنترل کردن جریان یک حلقه‌ی while آموختید، مانند تعریف یک پرچم active، استفاده از عبارت‌های break و continue. یاد گرفتید که چگونه برای انتقال اعضای یک لیست به لیستی دیگر و همچنین حذفِ تمامِ موارد مشابه با یک مقدار از یک لیست، از حلقه‌ی while استفاده کنید. همچنین دانستید که می‌توان حلقه‌های while را با دیکشنری‌ها هم استفاده کرد.

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


1.8 - تابع‌ها

1.8.1 - تعریف کردن تابع

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

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

اینجا تابعی ساده به نام greet_user() می‌بینید که یک پیام احوال‌پرسی را چاپ می‌کند:

1
2
3
4
5
def greet_user():
    """Display a simple greeting."""
    print("Hello!")

greet_user()

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

هر خطّ تورفته‌ای که پس از def greet_user(): بیاید بدنه‌ی تابع را تشکیل می‌دهد. متنِ ۲ نوعی توضیح است که docstring نامیده می‌شود و عملکرد تابع را شرح می‌دهد. Docstringها در علامت‌های نقل قول سه‌تایی قرار می‌گیرند و پایتون هنگام تولید مستندات برای تابع‌های موجود در برنامه‌ی شما به دنبال آن‌ها می‌گردد.

خط print("Hello!") در ۳ تنها خطِ برنامه در بدنه‌ی این تابع است، بنابراین greet_user() تنها یک وظیفه دارد: print("Hello").

زمانی که می‌خواهید از این تابع استفاده کنید، آن را فراخوانی می‌کنید. فراخوانی تابع از پایتون می‌خواهد که کد داخل آن را اجرا کند. برای فراخوانی یک تابع نام آن را می‌نویسید و پس از آن اطّلاعات ضروری را در پرانتز بعد از آن می‌آورید، چنان‌که در ۴ نشان داده شده است. چون اینجا به هیچ اطّلاعاتی نیاز نیست، فراخوانی تابع ما به سادگی و تنها با وارد کردن greet_user() انجام می‌پذیرد. همان‌طور که انتظار داشتیم، این تابع Hello! را چاپ می‌کند:

1
Hello!

1.8.2 - انتقال اطّلاعات به یک تابع

با کمی تغییر، تابعِ greet_user() نه تنها به کاربر سلام می‌کند، بلکه می‌تواند نام او را هم ذکر کند. برای این که تابع چنین کند username را در پرانتزهای تعریفِ تابع در def greet_user() می‌نویسید. با افزودن username به آن‌جا، تابع را قادر می‌سازید که هر مقدار usernameای که تعیین می‌کنید، بپذیرد. اکنون پایتون انتظار دارد که در هر بار فراخوانیِ تابع، مقداری برای username ارائه دهید. هنگامی که greet_user() را فرامی‌خوانید می‌توانید در پرانتزهای آن، نامی مانند 'jesse' را به این تابع بدهید:

1
2
3
4
5
def greet_user(username):
    """Display a simple greeting."""
    print(f"Hello, {username.title()}!")

greet_user('jesse')

وارد کردن greet_user('jesse') تابع greet_user() را فراخوانی کرده و به آن اطّلاعات مورد نیاز را برای اجرا کردن تابع print() ارائه می‌دهد. این تابع نامی را که به آن داده‌اید گرفته و پیامی خطاب به او نمایش می‌دهد:

1
Hello, Jesse!

مشابه این، وارد کردن greet_user('sarah') تابع greet_user() را فراخوانده، 'sarah' را به آن منتقل کرده و Hello, Sarah! را چاپ می‌کند. می‌توانید greet_user() را هر زمان که بخواهید فرابخوانید و هر نامی را که خواستید به آن بدهید تا هر بار یک خروجی قابل پیش‌بینی تولید کند.

پارامترها و نشان‌وندها

تابع greet_user() قبلی را طوری تعریف کردیم که مقداری برای متغیّر username درخواست کند. با فراخوانی آن و ارائه‌ی اطّلاعاتی که نیاز دارد (نام فرد)، این تابع پیام مناسب را چاپ می‌کرد.

متغیّر username در تعریفِ تابع greet_user() نمونه‌ای از یک پارامتر 70 است، اطّلاعاتی که تابع برای انجام کار خود نیاز دارد. مقدار 'jesse' در greet_user('jesse') نمونه‌ای از نشان‌وند یا آرگومان است. نشان‌ون 71 د اطّلاعاتی است که هنگام فراخوانی یک تابع، به آن ارائه می‌شود. زمانی که تابع را فراخوانی می‌کنیم، مقدار مورد نظر خود را در داخل پرانتزهای آن قرار می‌دهیم. در این مورد نشان‌وند 'jesse' به تابع greet_user() منتقل شده و این مقدار به پارامتر username منسوب شده است.

نکته
افراد گاهی نشان‌وند و پارامتر را به جای هم به کار می‌برند. اگر جایی دیدید که به متغیّرهای تعریف‌شده در تعریفِ تابع نشان‌وند یا به متغیرّهای فراخوانی تابع پارامتر گفته‌اند، تعجّب نکنید.

خودآزمایی

۸-۱. پیام: تابعی به نام display_message() بنویسید که جمله‌ای چاپ کند مبنی بر این که شما در این فصل مشغولِ آموختنِ چه هستید. تابع را فراخوانی کنید و مطمئن شوید که پیام به درستی نمایش داده می‌شود.

۸-۲. کتاب دلخواه: تابعی به نام favorite_book() بنویسید که یک پارامتر به نام title بپذیرد. این تابع باید پیامی چاپ کند مانند One of my favorite books is Alice in Wonderland. تابع را فراخوانی کنید و مطمئن شوید که هنگام فراخوانی، عنوانِ کتابی را به عنوانِ نشان‌وند به آن می‌دهید.

1.8.3 - انتقال نشان‌وندها

فراخوانی یک تابع ممکن است به نشان‌ونن73 یاز داشته باشد زیرا تعریف تابع ممکن است شامل چند پارامتر باشد. چند راه برای انتقال نشان‌وندها به تابع‌ها دارید. می‌توانید از نشان‌وندهای مکان‌دار استفاده کنید که هنگام انتقال به تابع باید با همان ترتیبِ تعریف شدهِ پارامترها وارد شوند. در نشان‌وندهای کلیدواژه‌ای هر نشان‌وند از نام متغیّر و یک مقدار، لیست یا دیکشنری‌هایی از مقدارها تشکیل شده است. در ادامه به هر یک از این دو مورد خواهیم پرداخت.

نشان‌وندهای مکان‌دار

هنگام فراخوانی یک تابع، پایتون باید هر نشان‌وند را با پارامتر متناظرش در تعریفِ تابع، جفت کند. ساده‌ترین راه، انجامِ این کار بر اساسِ ترتیبِ تعیینِ نشان‌وندهاست. مقدارهایی که به این ترتیب جفت می‌شوند نشان‌وندهای مکان‌دار نامیده می‌شوند.

برای این که نحوه‌ی این کار را ببینید تابعی را در نظر بگیرید که اطّلاعاتی را در مورد حیوانات خانگی چاپ می‌کند. این تابع می‌گوید که هر حیوان خانگی چه نوع جانوری است و نام آن چیست:

1
2
3
4
5
6
def describe_pet(animal_type, pet_name):
    """Display information about a pet."""
    print(f"\nI have a {animal_type}.")
    print(f"My {animal_type}'s name is {pet_name.title()}.")

describe_pet('hamster', 'harry')

تعریف تابع نشان می‌دهد که به نوع و نام حیوان نیاز دارد (۱). زمانی که describe_pet() را فراخوانی می‌کنیم، باید دو مقدار برای نوع و نام حیوان را به همان ترتیب ارائه کنیم. برای نمونه در این فراخوانی تابع، نشان‌وند 'hamster' به پارامتر animal_type و نشان‌وند 'harry' به پارامتر pet_name منسوب می‌شود. (۲) در بدنه‌ی تابع این دو پارامتر برای نمایش اطّلاعاتی در مورد حیوان مورد بحث ارائه می‌شود.

خروجی، همستری با نام Harry را شرح می‌دهد:

1
2
3
I have a hamster.

My hamster's name is Harry.

فراخوانی چندباره‌ی تابع

می‌توانید یک تابع را هر چند بار که نیاز داشته باشید فراخوانی کنید. توصیف حیوان خانگی متفاوت دوم تنها نیازمند فراخوانی دوباره‌ی describe_pet() است:

1
2
3
4
5
6
7
def describe_pet(animal_type, pet_name):
    """Display information about a pet."""
    print(f"\nI have a {animal_type}.")
    print(f"My {animal_type}'s name is {pet_name.title()}.")

describe_pet('hamster', 'harry')
describe_pet('dog', 'willie')

در دومین فراخوانیِ تابع دو نشان‌وندِ 'dog' و 'willie' را به describe_pet() دادیم. مشابه نشان‌وندهایی که پیش از این به کار بردیم، پایتون 'dog' را با پارامتر animal_type و 'willie' را با پارامتر pet_name تطبیق می‌دهد.

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

1
2
3
4
5
6
7
I have a hamster.

My hamster's name is Harry.

I have a dog.

My dog's name is Willie.

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

می‌توانید هر تعداد نشان‌وند مکان‌دار که نیاز داشته باشید در یک تابع استفاده کنید. پایتون نشان‌وندهایی را که ارائه کرده‌اید یکی یکی بررسی می‌کند و هر کدام را با پارامتر متناظر آن در تعریفِ تابع تطبیق می‌دهد.

در نشان‌وندهای مکان‌دار ترتیب اهمّیّت دارد

اگر هنگام به‌کارگیری نشان‌وندهای مکان‌دار ترتیب آن‌ها را به هم بریزید ممکن است به نتایج ناخواسته‌ای برسید:

1
2
3
4
5
6
def describe_pet(animal_type, pet_name):
    """Display information about a pet."""
    print(f"\nI have a {animal_type}.")
    print(f"My {animal_type}'s name is {pet_name.title()}.")

describe_pet('harry', 'hamster')

در این فراخوانیِ تابع نام را اول و نوع حیوان را پس از آن وارد کرده‌ایم. چون نشان‌وند 'harry' در ابتدا آمده است، مقدار آن به پارامتر animal_type منسوب می‌شود. به طور مشابه، 'hamster' به pet_name منسوب می‌شود. اکنون یک هری با نام همستر داریم:

1
2
3
I have a harry.

My harry's name is Hamster.

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

نشان‌وندهای کلیدواژه‌ای

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

اکنون pets.py را با فراخوانی describe_pet() به کمک نشان‌وندهای کلیدواژه‌ای بازنویسی می‌کنیم:

1
2
3
4
5
6
def describe_pet(animal_type, pet_name):
    """Display information about a pet."""
    print(f"\nI have a {animal_type}.")
    print(f"My {animal_type}'s name is {pet_name.title()}.")

describe_pet(animal_type='hamster', pet_name='harry')

تابع describe_pet() تغییری نکرده است. امّا هنگام فراخوانی آن به طور صریح به پایتون می‌گوییم که کدام نشان‌وند باید با کدام پارامتر جفت شود. پایتون فراخوانیِ تابع را می‌خواند و می‌فهمد که نشان‌وند 'hamster' را به پارامتر animal_type و 'harry' را به pet_name نسبت دهد.

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

1
2
describe_pet(animal_type='hamster', pet_name='harry')
describe_pet(pet_name='harry', animal_type='hamster')
نکته
اگر از نشان‌وندهای کلیدواژه‌ای استفاده می‌کنید مطمئن شوید که در تعریفِ تابع نام دقیق پارامترها را وارد کنید.

مقدارهای پیش‌فرض 75

می‌توانید هنگام نوشتن یک تابع برای هر پارامتر آن یک مقدار پیش‌فرض تعیین کنید. اگر هنگام فراخوانیِ تابع برای یک نشان‌وند، پارامتری ارائه شود پایتون از مقدار آن استفاده می‌کند. اگر نه، مقدارِ پیش‌فرضِ پارامتر به کار گرفته می‌شود. بنابراین اگر برای یک پارامتر مقداری پیش‌فرض تعیین کنید، ارائه‌ی نشان‌وندِ متناظر هنگام فراخوانیِ تابع، الزامی نخواهد بود. به‌کارگیری مقدارهای پیش‌فرض فراخوانی توابع را ساده‌تر می‌کند و روش‌های معمول به‌کارگیری توابع را شفّاف‌تر می‌کند.

برای مثال اگر متوجّه شوید که بیشتر فراخوانی‌های تابع describe_pets() برای توصیف سگ‌ها به کار می‌روند، می‌توانید مقدار پیش‌فرض animal_type را 'dog' قرار دهید. حال هرکس هنگام فراخوانی describe_pet() برای یک سگ می‌تواند آن بخش از اطّلاعات ورودی را حذف کند.

1
2
3
4
5
6
def describe_pet(pet_name, animal_type='dog'):
    """Display information about a pet."""
    print(f"\nI have a {animal_type}.")
    print(f"My {animal_type}'s name is {pet_name.title()}.")

describe_pet(pet_name='willie')

تعریفِ describe_pet() را تغییر دادیم تا مقدار پیش‌فرضِ 'dog' را برای animal_type شامل شود. اکنون هرگاه تابع بدون تعیینِ animal_type فراخوانی شود، پایتون می‌داند که مقدار 'dog' را به جای این پارامتر به کار برد:

1
2
3
I have a dog.

My dog's name is Willie.

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

اکنون ساده‌ترین روش به‌کارگیری این تابع این است که هنگام فراخوانی آن تنها نام سگ را ارائه کنیم:

1
describe_pet('willie')

خروجیِ این فراخوانیِ تابع مشابه مثال قبل است. تنها نشان‌وندی که ارائه شده 'willie' است، بنابراین این مقدار با اوّلین پارامتر در تعریفِ تابع، pet_name، جفت می‌شود. از آن‌جایی‌که هیچ نشان‌وندی برای animal_type آورده نشده، پایتون از مقدارِ پیش‌فرضِ 'dog' استفاده می‌کند.

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

1
describe_pet(pet_name='harry', animal_type='hamster')

پایتون مقدارِ پیش‌فرضِ پارامتر را نادیده می‌گیرد چون این بار صریحاً نشان‌وندی برای animal_type تعیین شده است.

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

فراخوانی‌های تابع همسان

از آن‌جایی که نشان‌وندهای مکان‌دار، نشان‌وندهای کلیدواژه‌ای و مقدارهای پیش‌فرض می‌توانند همگی در کنار هم به کار روند، اغلب برای فراخوانی یک تابع چندین راه پیش رو دارید. تعریف زیر را برای describe_pet() با یک مقدارِ پیش‌فرض در نظر بگیرید:

1
def describe_pet(pet_name, animal_type='dog'):

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

تمام فراخوانی‌های زیر برای این تابع قابل استفاده‌اند:

1
2
3
4
5
6
7
8
# A dog named Willie.
describe_pet('willie')
describe_pet(pet_name='willie')

# A hamster named Harry.
describe_pet('harry', 'hamster')
describe_pet(pet_name='harry', animal_type='hamster')
describe_pet(animal_type='hamster', pet_name='harry')

تمام این فراخوانی‌های خروجی‌ای مشابه با قبل خواهند داشت.

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

پرهیز از خطاهای نشان‌وندی

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

1
2
3
4
5
6
def describe_pet(animal_type, pet_name):
    """Display information about a pet."""
    print(f"\nI have a {animal_type}.")
    print(f"My {animal_type}'s name is {pet_name.title()}.")

describe_pet()

پایتون تشخیص می‌دهد که برخی اطّلاعات در فراخوانی تابع جا افتاده است و tracebackای در این مورد به ما اطّلاعات می‌دهد:

1
2
3
4
5
Traceback (most recent call last):
  File "pets.py", line 6, in <module>
    describe_pet()
TypeError: describe_pet() missing 2 required positional arguments:
    'animal_type' and 'pet_name'

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

این موضوع که پایتون کد تابع را می‌خواند و نام نشان‌وندهای جاافتاده را به ما ارائه می‌کند، سودمند است. این می‌تواند انگیزه‌ای باشد که برای تابع‌ها و متغیّرهای خود نام‌های گویا و پرمعنی انتخاب کنید. اگر چنین کنید پیام‌های خطای پایتون می‌تواند برای شما و هر‌کس که از کد شما استفاده می‌کند، مفیدتر باشد.

اگر تعداد نشان‌وندها بیش از حد نیاز باشد، به tracebackای مشابه برمی‌خورید که به شما کمک می‌کند فراخوانی تابع را با تعریفِ آن جور کنید.

خودآزمایی

۸-۳. تیشرت: تابعی به نام make_shirt() بنویسید که اندازه تیشرت و متن پیامی را که قرار است روی آن چاپ شود دریافت کند. این تابع باید پیامی چاپ کند که شامل پیامی خلاصه در مورد اندازه و جمله‌ی چاپ‌شده روی تیشرت باشد.

تابع را یک‌بار به کمک نشان‌وندهای مکان‌دار فراخوانی کنید تا اطلاعات تیشرتی را تولید کند. بار دیگر تابع را این بار به کمک نشان‌وندهای کلیدواژه‌ای فراخوانی کنید.

۸-۴. تیشرت‌های سایز بزرگ: تابع make_shirt() را طوری تغییر دهید که سایز تیشرت‌ها به صورت پیش‌فرض بزرگ و پیام چاپ‌شده I love Python باشد. یک تیشرت بزرگ و متوسط با پیام پیش‌فرض و یک تیشرت با پیامی متفاوت (و در هر سایزی که خواستید) تولید کنید.

۸-۵. شهرها: تابعی به نام describe_city() بنویسید که نام یک شهر و کشور آن را دریافت می‌کند. این تابع باید جمله‌ای ساده چاپ کند، مانند Reykjavik is in Iceland. به پارامتر مربوط به کشور یک مقدارِ پیش‌فرض بدهید. تابع را برای سه شهر مختلف فراخوانی کنید که دستِ کم در یکی از آن‌ها از کشور پیش‌فرض استفاده نشود.

1.8.4 - مقدارهای برگشتی

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

بازگرداندن یک مقدار ساده

تابعی را می‌بینیم که نام و نام خانوادگی را می‌گیرد و نام کامل را به شکلی مرتّب برمی‌گرداند:

1
2
3
4
5
6
7
def get_formatted_name(first_name, last_name):
    """Return a full name, neatly formatted."""
    full_name = f"{first_name} {last_name}"
    return full_name.title()

musician = get_formatted_name('jimi', 'hendrix')
print(musician)

تعریف تابع در ۱ دو پارامتر شامل نام و نام خانوادگی دریافت می‌کند. تابع این دو نام را ترکیب می‌کند، بین آن‌ها یک فاصله‌ی خالی قرار می‌دهد و حاصل را به full_name نسبت می‌دهد (۲). حرف‌های اوّل مقدار full_name به حروف بزرگ تبدیل می‌شود و به خطّی که تابع را فراخوانده برگردانده می‌شود. (۳)

وقتی تابعِ تعریف‌شده مقداری را برمی‌گرداند باید متغیّری ارائه کنید که مقدار برگشتی به آن منسوب شود. در این مورد مقدار برگشتی به متغیّر musician نسبت داده می‌شود (۴). خروجی نامی مرتّب را نشان می‌دهد که از اجزای نام یک فرد تشکیل شده است:

1
Jimi Hendrix

به نظر می‌رسد که برای تولید یک نام به این شیوه کار زیادی انجام دادیم در حالی‌که می‌توانستیم به سادگی بنویسیم:

1
print("Jimi Hendrix")

امّا برنامه‌ای بزرگ را در نظر بگیرید که باید تعداد زیادی نام و نام خانوادگی را جداگانه ذخیره کند. در چنین برنامه‌ای تابع‌هایی مثل get_formatted_name() بسیار سودمند خواهد بود. نام‌ها و نام‌های خانوادگی را جداگانه ذخیره می‌کنید و سپس هرگاه نیاز داشتید آن را به صورت نام کامل نمایش می‌دهید.

اختیاری کردن یک نشان‌وند

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

برای مثال فرض کنید می‌خواهیم get_formatted_name() را طوری گسترش دهیم که نام‌های میانی را هم دربربگیرد. اولّین تلاش برای چنین کاری ممکن است اینگونه باشد:

1
2
3
4
5
6
7
def get_formatted_name(first_name, middle_name, last_name):
    """Return a full name, neatly formatted."""
    full_name = f"{first_name} {middle_name} {last_name}"
    return full_name.title()

musician = get_formatted_name('john', 'lee', 'hooker')
print(musician)

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

1
John Lee Hooker

امّا نام میانی همواره مورد نیاز نیست و این تابع جوری نوشته شده که اگر فقط نام و نام خانوادگی را به آن بدهید کار نمی‌کند. برای اختیاری کردن نام میانی می‌توانیم به نشان‌وند middle_name یک مقدار پیش‌فرض خالی بدهیم و از آن جز در مواردی که کاربر مقدار ارائه کرده، صرف نظر کنیم. برای این که کاری کنیم که get_formatted_name() بدون نام میانی کار کند، مقدار پیش‌فرض middle_name را یک رشته‌ی خالی می‌گذاریم و آن را به انتهای فهرست پارامترها منتقل می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def get_formatted_name(first_name, last_name, middle_name=''):
    """Return a full name, neatly formatted."""
    if middle_name:
        full_name = f"{first_name} {middle_name} {last_name}"
    else:
        full_name = f"{first_name} {last_name}"
    return full_name.title()

musician = get_formatted_name('jimi', 'hendrix')
print(musician)

musician = get_formatted_name('john', 'hooker', 'lee')
print(musician)

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

در بدنه‌ی تابع بررسی می‌کنیم که آیا نام میانی وارد شده یا نه. پایتون رشته‌های ناتُهی را True تلقّی می‌کند پس اگر middle_name درست (True) ارزیابی شود به این معنی است که هنگام فراخوانی تابع برای آن مقداری تعیین شده است. (۲). اگر نام میانی ارائه شده باشد، نام، نام میانی و نام خانوادگی با هم ترکیب می‌شوند تا نام کامل ساخته شود. نام کامل به حالت عنوان درآمده و به خطّ فراخوانی تابع برگردانده می‌شود؛ جایی که مقدار آن به متغیّر musician نسبت داده شده و چاپ می‌شود. اگر نام میانی وارد نشده باشد، آزمون شرطی if رد می‌شود و بخش else اجرا می‌شود (۳). نام کامل تنها با نام و نام خانوادگی ساخته می‌شود و نام شکل‌داده‌شده به خط فراخوانی برگردانده شده و چاپ می‌شود.

فراخوانی تابع با نام و نام خانوادگی سرراست است. اگر بخواهیم نام میانی را هم وارد کنیم، باید مطمئن شویم که آن را به عنوان آخرین نشان‌وند بنویسیم تا پایتون بتواند نشان‌وندهای مکان‌دار را به درستی تطبیق دهد. (۴)

نسخه‌ی تغییریافته‌ی تابع برای افرادی که تنها نام و نام خانوادگی دارند کار می‌کند، و به همان ترتیب برای افرادی که نام میانی هم دارند، عمل می‌کند:

1
2
3
Jimi Hendrix

John Lee Hooker

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

بازگرداندن یک دیکشنری

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

1
2
3
4
5
6
7
def build_person(first_name, last_name):
    """Return a dictionary of information about a person."""
    person = {'first': first_name, 'last': last_name}
    return person

musician = build_person('jimi', 'hendrix')
print(musician)

تابع build_person() نام و نام خانوادگی را می‌گیرد و این مقدارها را در ۱ در یک دیکشنری قرار می‌دهد. مقدار first_name در کلید 'first' و مقدار last_name در کلید 'last' ذخیره می‌شود. کل دیکشنری که نماینده‌ی یک فرد است در ۲ برگردانده می‌شود. مقدار برگشتی در ۳ چاپ می‌شود که نشان‌دهنده‌ی دو دانسته‌ی متنی است که این بار در یک دیکشنری ذخیره شده است:

1
{'first': 'jimi', 'last': 'hendrix'}

این تابع دو داده‌ی متنی ساده را دریافت کرده و آن در یک ساختمان داده‌ی بامعناتر قرار می‌دهد که به شما امکان می‌دهد با داده‌ها کارهایی فراتر از چاپ کردن بکنید. رشته‌های 'jimi' و 'hendrix' اکنون به عنوان نام و نام خانوادگی برچسب خورده‌اند. می‌توانید این تابع را به سادگی گسترش دهید تا مقدارهای اختیاری مختلفی بگیرد؛ همچون نام میانی، سن، شغل و یا هر اطّلاعاتی دیگری که بخواهید در مورد یک فرد ذخیره کنید. برای مثال تغییر زیر موجب می‌شود که بتوانید سن فرد را هم ذخیره کنید:

1
2
3
4
5
6
7
8
9
def build_person(first_name, last_name, age=None):
    """Return a dictionary of information about a person."""
    person = {'first': first_name, 'last': last_name}
    if age:
        person['age'] = age
    return person

musician = build_person('jimi', 'hendrix', age=27)
print(musician)

پارامتر اختیاری سن را به تعریفِ تابع افزوده‌ایم و77 به آن مقدار ویژه‌ی None را نسبت داده‌ایم. این مقدار زمانی استفاده می‌شود که متغیّر هیچ مقدار خاصّی ندارد. به None می‌توانید به عنوان یک نگهدارنده یا جای‌بان نگاه کنید. در آزمون‌های شرطی None نادرست (False) ارزیابی می‌شود. اگر هنگام فراخوانیِ تابع سن وارد شود، مقدار آن در دیکشنری ذخیره می‌گردد. این تابع همواره نام فرد را ذخیره می‌کند امّا می‌توان آن را جوری تغییر داد که هر اطّلاعات دیگری را در مورد یک فرد در بر بگیرد.

به‌کارگیری تابع با یک حلقه‌ی while

می‌توانید از تابع‌ها در تمام ساختارهای پایتون که تاکنون آموخته‌اید استفاده کنید. برای مثال بیایید از تابع get_formatted_name() به همراه یک حلقه‌ی while استفاده کنیم تا به کاربران کمی رسمی‌تر سلام کنیم. این اوّلین تلاش برای سلام کردن به افراد با نام و نام خانوادگی آن‌هاست:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def get_formatted_name(first_name, last_name):
    """Return a full name, neatly formatted."""
    full_name = f"{first_name} {last_name}"
    return full_name.title()

# This is an infinite loop!
while True:
    print("\nPlease tell me your name:")
    f_name = input("First name: ")
    l_name = input("Last name: ")
    formatted_name = get_formatted_name(f_name, l_name)
    print(f"\nHello, {formatted_name}!")

در این مثال از نسخه‌ی ساده‌تر get_formatted_name() استفاده کرده‌ایم که نام‌های میانی را شامل نمی‌شود. حلقه‌ی while از کاربران نام و نام خانوادگی آن‌ها را می‌پرسد. گرفتن نام و نام خانوادگی جداگانه انجام می‌پذیرد. (۱)

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def get_formatted_name(first_name, last_name):
    """Return a full name, neatly formatted."""
    full_name = f"{first_name} {last_name}"
    return full_name.title()

while True:
    print("\nPlease tell me your name:")
    print("(enter 'q' at any time to quit)")
    f_name = input("First name: ")
    if f_name == 'q':
        break
    l_name = input("Last name: ")
    if l_name == 'q':
        break
    formatted_name = get_formatted_name(f_name, l_name)
    print(f"\nHello, {formatted_name}!")

پیامی اضافه کرده‌ایم که به کاربر اطّلاع دهد چگونه می‌تواند خارج شود و سپس اگر کاربر مقدار خروج را در هرکدام از اعلان‌ها وارد کرد، حلقه را می‌شکنیم. اکنون برنامه به سلام کردن به افراد ادامه می‌دهد تا جایی که کسی 'q' را به جای هر کدام از نام‌ها وارد کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Please tell me your name:
(enter 'q' at any time to quit)
First name: **eric**
Last name: **matthes**

Hello, Eric Matthes!

Please tell me your name:
(enter 'q' at any time to quit)
First name: **q**

خودآزمایی

۸-۶. نام شهرها: تابعی به نام city_country() بنویسید که نام و کشور یک شهر را بگیرد. تابع باید رشته‌ای به شکل زیر برگرداند:

1
"Santiago, Chile"

تابع خود را با دستِ کم سه جفت شهر-کشور فراخوانی کنید و مقدارهای خروجی را چاپ کنید.

۸-۷. آلبوم: تابعی به نام make_album() بنویسید که یک دیکشنری برای توصیف یک آلبوم موسیقی می‌سازد. این تابع باید نام هنرمند و عنوان آلبوم را بگیرد و یک دیکشنری شامل این اطّلاعات را برگرداند. از تابع برای ساختن سه دیکشنری که هر کدام بیانگر یک آلبوم موسیقی است، استفاده کنید. مقدار برگشتی هر کدام را چاپ کنید تا نشان دهید دیکشنری‌ها اطّلاعات آلبوم را به درستی ذخیره می‌کنند.

از None برای اضافه کردن یک پارامتر اختیاری به make_album() استفاده کنید تا امکان ذخیره کردن تعداد آهنگ‌های یک آلبوم را هم فراهم آورد. اگر در خطّ فراخوانی مقداری هم برای تعداد آهنگ‌ها وارد شود، آن مقدار را به دیکشنری آلبوم اضافه کنید. حداقل یک فراخوانیِ تابع اضافه کنید که شامل تعداد آهنگ‌های یک آلبوم باشد.

۸-۸. آلبوم‌های کاربران: با برنامه‌ی تمرین ۸-۷ شروع کنید. یک حلقه‌ی while بنویسید که به کاربران امکان دهد نام و نام هنرمند یک آلبوم را وارد کنند. به محض دریافت اطّلاعات make_album() را با اطّلاعات کاربران فراخوانی کنید و دیکشنری‌ای را که ساخته شده نمایش دهید. مطمئن شوید که یک مقدار خروج در حلقه‌ی while تعیین کرده‌اید.

1.8.5 - انتقال لیست

انتقال لیست‌ها به یک تابع غالباً کاری سودمند است، چه لیستی از نام‌ها باشد، چه اعداد یا چیزهای پیچیده‌تر مثل دیکشنری‌ها. زمانی که لیستی را به تابعی می‌دهید، تابع به محتویّات آن دسترسی مستقیم دارد. اکنون از تابع‌ها برای کار کردن کارآمدتر با لیست‌ها استفاده می‌کنیم.

فرض کنید که لیستی از کاربران داریم و می‌خواهیم به هر کدامشان سلام کنیم. مثال زیر لیستی از نام‌ها را به تابعی به نام greet_users() می‌فرستد که به هر کدام از افراد داخل لیست به طور جداگانه سلام می‌کند.

1
2
3
4
5
6
7
8
def greet_users(names):
    """Print a simple greeting to each user in the list."""
    for name in names:
        msg = f"Hello, {name.title()}!"
        print(msg)

usernames = ['hannah', 'ty', 'margot']
greet_users(usernames)

تابع greet_users() را طوری تعریف کرده‌ایم که لیستی از نام‌ها را بپذیرد و آن را به پارامتر names نسبت دهد. تابع داخل لیست دریافتی حلقه می‌زند و به هر کدام از کاربرها سلام می‌کند. در ۱ لیستی از کاربران تعریف کرده‌ایم و آن را (usernames) هنگام فراخوانی به تابع greet_users() داده‌ایم:

1
2
3
4
5
Hello, Hannah!

Hello, Ty!

Hello, Margot!

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

تغییر دادن یک لیست در داخل تابع

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Start with some designs that need to be printed.
unprinted_designs = ['phone case', 'robot pendant', 'dodecahedron']
completed_models = []

# Simulate printing each design, until none are left.
# Move each design to completed_models after printing.
while unprinted_designs:
    current_design = unprinted_designs.pop()
    print(f"Printing model: {current_design}")
    completed_models.append(current_design)

# Display all completed models.
print("\nThe following models have been printed:")
for completed_model in completed_models:
    print(completed_model)

این برنامه با دو لیست شروع می‌شود: یکی لیستِ طرح‌هایی که باید چاپ شوند و دیگری لیستی خالی که طرح‌ها پس از چاپ شدن به آن منتقل می‌شوند. تا زمانی که طرحی در unprinted_designs باقی مانده باشد، حلقه‌ی while با خارج کردن طرح از انتهای لیست، ذخیره کردن آن در current_design و چاپ کردن پیامی مبنی بر در حالِ چاپ بودنِ طرح فعلی، عملیّات چاپ را شبیه‌سازی می‌کند. حلقه پس از آن طرح را به لیستِ مدل‌های کامل‌شده اضافه می‌کند. زمانی که حلقه به پایان رسید، فهرستی از طرح‌هایی که چاپ شده‌اند، نمایش داده می‌شود:

1
2
3
4
5
6
7
8
Printing model: dodecahedron
Printing model: robot pendant
Printing model: phone case

The following models have been printed:
dodecahedron
robot pendant
phone case

می‌توانیم این کد را با نوشتن دو تابع که هر یک کار خاصّی انجام می‌دهند، بازآرایی کنیم. بیشترِ کد تغییر نخواهد کرد؛ تنها آن را با دقّت بیشتری ساختاردهی می‌کنیم. تابع اوّل چاپ کردن طرح‌ها را مدیریّت می‌کند و دومی کارهای چاپی را که انجام شده خلاصه می‌کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def print_models(unprinted_designs, completed_models):
    """Simulate printing each design, until none are left.
    Move each design to completed_models after printing."""
    while unprinted_designs:
        current_design = unprinted_designs.pop()
        print(f"Printing model: {current_design}")
        completed_models.append(current_design)


def show_completed_models(completed_models):
    """Show all the models that were printed."""
    print("\nThe following models have been printed:")
    for completed_model in completed_models:
        print(completed_model)


unprinted_designs = ['phone case', 'robot pendant', 'dodecahedron']
completed_models = []

print_models(unprinted_designs, completed_models)
show_completed_models(completed_models)

در ۱ تابع print_models() را با دو پارامتر تعریف می‌کنیم: لیستی از طرح‌هایی که باید چاپ شوند و لیستی از مدل‌های تکمیل‌شده. تابع با گرفتنِ این دو لیست، فرآیند چاپ را با خالی کردنِ لیست طرح‌های چاپ‌نشده و پر کردنِ لیستِ مدل‌های تکمیل‌شده شبیه‌سازی می‌کند. در ۲ تابع show_completed_models() را با یک پارامتر تعریف می‌کنیم: لیستِ مدل‌های تکمیل‌شده. تابع show_completed_models() با داشتنِ این لیست، نام هر کدام از مدل‌های چاپ‌شده را نمایش می‌دهد.

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

1
2
3
4
5
unprinted_designs = ['phone case', 'robot pendant', 'dodecahedron']
completed_models = []

print_models(unprinted_designs, completed_models)
show_completed_models(completed_models)

لیستی از طرح‌های چاپ‌نشده و یک لیست خالی که مدل‌های تکمیل‌شده را نگه می‌دارد تعریف می‌کنیم. پس از آن، چون قبلاً دو تابع خود را تعریف کرده‌ایم، تنها آن‌ها را با پارامترهای درست فراخوانی کنیم. تابع print_models() را صدا کرده و دو لیست مورد نیاز را به آن می‌دهیم؛ چنان‌که انتظار داشتیم این تابع چاپ کردن طرح‌ها را شبیه‌سازی می‌کند. سپس show_completed_models() را فراخوانی کرده و لیست مدل‌های تکمیل‌شده را به آن می‌دهیم تا بتواند چاپ شدنِ آن‌ها را گزارش کند. این نام‌های تابع گویا به دیگران این امکان را می‌دهد که کد را حتّی بدون توضیحات، خوانده و درک کنند.

نگه‌داری و توسعه‌ی این برنامه از نسخه‌ی بدونِ تابعِ آن ساده‌تر است. اگر بخواهیم بعداً طرح‌های دیگری را چاپ کنیم به سادگی تابع print_models() را دوباره فرامی‌خوانیم. اگر متوجّه شویم که کد مربوط به چاپ کردن باید تغییر کند، کد را یک بار تغییر می‌دهیم و هر جا که تابع استفاده شده باشد، تمام تغییراتِ اعمالی اعمال می‌شوند. این روش از تغییر دادن کد در جای جایِ برنامه کارآمدتر است.

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

منعِ یک تابع از تغییر دادن یک لیست

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

می‌توانید کپی‌ای از لیست را به این صورت به تابع ارسال کنید:

1
function_name(list_name[:])

نماد برش [:] یک کپی از لیست را به تابع می‌فرستد. اگر قصد نداشتیم که لیست طرح‌های چاپ‌نشده را در printing_models.py دست بزنیم، باید print_models() را مانند زیر فراخوانی می‌کردیم:

1
print_models(unprinted_designs[:], completed_models)

تابع print_models() می‌تواند به کار خود برسد زیرا همچنان نام تمام طرح‌های چاپ‌نشده را دریافت می‌کند. امّا این بار این از کپیِ لیست اصلی طرح‌های چاپ‌نشده استفاده می‌کند و نه لیستِ unprinted_designs واقعی. لیست completed_models مثل قبل با نام مدل‌های چاپ‌شده پر می‌شود امّا لیستِ اصلیِ طرح‌هایِ چاپ‌نشده در این تابع تغییری نمی‌کند.

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

خودآزمایی

۸-۹. پیامک‌ها: لیستی شامل تعدادی پیامک بسازید. لیست را به تابع show_message() بفرستید تا هر پیامک را چاپ کند.

۸-۱۰. ارسال پیامک‌ها: از یک کپی از برنامه‌ی تمرین ۸-۹ شروع کنید. تابعی به نام send_messages() بنویسید که هر پیامک را چاپ کرده و آن را پس از چاپ به لیستی جدید به نام sent_messages منتقل می‌کند. پس از فراخوانی تابع هر دو لیست را چاپ کنید تا مطمئن شوید که پیامک‌ها به درستی منتقل شده‌اند.

۸-۱۱. پیامک‌های بایگانی‌شده: کار خود را از تمرین ۸-۱۰ شروع کنید. تابع send_messages() را با یک کپی از لیست پیامک‌ها فراخوانی کنید. بعد از صدا کردنِ تابع هر دو لیست را چاپ کنید و نشان دهید که لیست اصلی پیامک‌های خود را حفظ کرده است.

1.8.6 - انتقال تعداد دلخواهی نشان‌وند

گاهی از تعداد نشان‌وندهایی که یک تابع نیاز دارد، از پیش اطّلاع ندارید. خوشبختانه پایتون این امکان را می‌دهد که تابع تعداد دلخواهی نشان‌وند را هنگام فراخوانی دریافت کند.

برای مثال تابعی را در نظر بگیرید که پیتزا درست می‌کند. این تابع به تعداد مواد روی پیتزا نیاز دارد ولی شما از پیش نمی‌دانید که فرد چه تعدادی از این مواد را درخواست می‌کند. تابع مثال زیر یک پارامتر دارد، *toppings، امّا این پارامترها تمام نشان‌وندهایی را که در خط فراخوانی ارائه می‌شود، جمع‌آوری می‌کند:

1
2
3
4
5
6
def make_pizza(*toppings):
    """Print the list of toppings that have been requested."""
    print(toppings)

make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')

علامت ستاره در نام پارامتر *toppings به پایتون می‌گوید که یک چندتایی به نام toppings درست کن و تمام مقدارهایی را که دریافت می‌شود درونِ آن قرار بده. خروجیِ تولیدشده با فراخوانی print() در بدنه‌ی تابع نشان می‌دهد که پایتون می‌تواند فراخوانیِ تابع را با یک یا سه مقدار مدیریّت کند و با فراخوانی‌های مختلف یک جور برخورد می‌کند. توجّه کنید که پایتون نشان‌وندها را در یک چندتایی قرار می‌دهد، حتّی اگر تابع تنها یک مقدار ورودی داشته باشد:

1
2
('pepperoni',)
('mushrooms', 'green peppers', 'extra cheese')

اکنون می‌توانیم print() را با یک حلقه جایگزین کنیم که در مواد روی پیتزا می‌چرخد و پیتزایی را که سفارش داده شده توصیف می‌کند:

1
2
3
4
5
6
7
8
def make_pizza(*toppings):
    """Summarize the pizza we are about to make."""
    print("\nMaking a pizza with the following toppings:")
    for topping in toppings:
        print(f"- {topping}")

make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')

تابع چه یک مقدارِ ورودی بگیرد چه سه مقدار، پاسخِ متناسبی می‌دهد:

1
2
3
4
5
6
7
Making a pizza with the following toppings:
- pepperoni

Making a pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese

این شکلِ دستوری صرف نظر از تعداد نشان‌وندهایی که تابع دریافت کرده، کار می‌کند.

ترکیب کردن نشان‌وندهای مکان‌دار و اختیاری

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

برای مثال اگر تابع به پارامتری برای اندازه‌ی پیتزا هم نیاز داشته باشد، باید آن را پیش از پارامتر *toppings بیاوریم:

1
2
3
4
5
6
7
8
def make_pizza(size, *toppings):
    """Summarize the pizza we are about to make."""
    print(f"\nMaking a {size}-inch pizza with the following toppings:")
    for topping in toppings:
        print(f"- {topping}")

make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

پایتون در تعریفِ تابع، اوّلین مقداری را که دریافت می‌کند به پارامتر size نسبت می‌دهد. تمام مقدارهای دیگر که پس از آن می‌آیند در چندتایی toppings ذخیره می‌شوند. فراخوانی‌های این تابع ابتدا شامل نشان‌وندی برای اندازه‌ی پیتزا هستند و پس از هر تعداد مواد روی پیتزا که مورد نیاز باشد، به تابع ارسال می‌شود.

اکنون هر پیتزا یک اندازه و تعدادی مواد دلخواه دارد و هر بخش اطّلاعات در جای مناسب خود چاپ می‌شود که نمایانگر اندازه در ابتدا و سپس مواد روی پیتزاست:

1
2
3
4
5
6
7
Making a 16-inch pizza with the following toppings:
- pepperoni

Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese
نکته
شما اغلب پارامتری عمومی به نام *args می‌بینید که تمام نشان‌وندهای مکان‌دار مانند این را جمع‌آوری می‌کند.

به‌کارگیری نشان‌وندهای کلیدواژه‌ای اختیاری

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def build_profile(first, last, **user_info):
    """Build a dictionary containing everything we know about a user."""
    user_info['first_name'] = first
    user_info['last_name'] = last
    return user_info

user_profile = build_profile('albert', 'einstein',
    location='princeton',
    field='physics')
print(user_profile)

تعریف build_profile() انتظار دارد که نام و نام خانوادگی وارد شود و سپس اجازه می‌دهد که کاربر هر تعداد جفتِ نام-مقدار که بخواهید وارد کند. دو علامت ستاره پشت **user_info به پایتون می‌گوید که یک دیکشنری خالی به نام user_info بسازد و تمام جفت نام-مقدارهایی که دریافت می‌کند در آن قرار دهد. داخل تابع می‌توانید همانند هر دیکشنری دیگری، به جفت‌های کلید-مقدار user_info دسترسی داشته باشید.

در بدنه‌ی build_profile() همواره نام و نام خانوادگی را به دیکشنری user_info اضافه می‌کنیم زیرا دو دانسته را از کاربر دریافت می‌کنیم (۱) امّا باید آن‌ها را در دیکشنری قرار دهیم. سپس دیکشنری user_info را به خط فراخوانی تابع برمی‌گردانیم.

تابع build_profile() را فراخوانی می‌کنیم، نام 'albert'، نام خانوادگی 'einstein' و دو جفت کلید-مقدارِ location='princeton' و field='physics' را به آن انتقال می‌دهیم. profile برگردانده شده را به user_profile نسبت می‌دهیم و آن را چاپ می‌کنیم:

1
2
{'location': 'princeton', 'field': 'physics',
 'first_name': 'albert', 'last_name': 'einstein'}

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

نکته
اغلب به پارامتری به نام **kwargs برمی‌خورید که برای جمع‌آوری نشان‌وندهای کلیدواژه‌ای نامشخّص استفاده می‌شود.

خودآزمایی

۸-۱۲. ساندویچ‌ها: تابعی بنویسید که لیستی از مواد دلخواه داخل ساندویچ را از فرد بگیرد. تابع باید یک پارامتر داشته باشد که تمام مواردی را که در فراخوانی تابع ارائه می‌شود جمع‌آوری کند و خلاصه‌ای از ساندویچی که سفارش داده می‌شود، چاپ کند. تابع را سه بار فرابخوانید و هر بار تعداد متفاوتی نشان‌وند استفاده کنید.

۸-۱۳. نمایه‌ی کاربری: از نسخه‌ای از user_profile.py در صفحه‌ی *** شروع کنید. با فراخوانی تابع build_profile() برای خود نمایه‌ای بسازید که شامل نام و نام خانوادگی و سه جفت کلید-مقدار باشد که شما را توصیف می‌کند.

۸-۱۴. خودروها: تابعی بنویسید که اطّلاعات پیرامون یک خودرو را در یک دیکشنری ذخیره می‌کند. تابع باید همیشه نام سازنده و مدل خودرو را دریافت کند. پس از آن باید تعداد دلخواهی نشان‌وند کلیدواژه‌ای دریافت کند. تابع را با اطّلاعات الزامی و دو جفت نام-مقدار دیگر فرابخوانید، مانند یک رنگ یا ویژگی سفارشی. تابع شما باید با فراخوانی زیر کار کند:

1
car = make_car('subaru', 'outback', color='blue', tow_package=True)

دیکشنری برگشتی از تابع را چاپ کنید تا مطمئن شوید که اطّلاعات به درستی ذخیره شده‌اند.

1.8.7 - ذخیره کردن تابع‌ها در ماژول‌ها

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

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

چندین راه برای وارد کردن یک ماژول وجود دارد که هر کدام از آن‌ها به طور مختصر معرّفی می‌شود.

وارد کردن یک ماژول کامل

برای آغاز وارد کردن تابع‌ها، نیاز دارید ابتدا یک ماژول بسازید. یک ماژول فایلی با پسوند .py است و کدی را شامل می‌شود که می‌خواهید به برنامه‌ی خود وارد کنید. اکنون ماژولی می‌سازیم که حاوی تابع make_pizza() است. برای ساختن این ماژول همه‌چیز را به جز این تابع، از فایل pizza.py حذف می‌کنیم:

1
2
3
4
5
def make_pizza(size, *toppings):
    """Summarize the pizza we are about to make."""
    print(f"\nMaking a {size}-inch pizza with the following toppings:")
    for topping in toppings:
        print(f"- {topping}")

حال فایلی جداگانه به نام making_pizza.py در همان جایی که pizza.py را ذخیره کرده‌ایم می‌سازیم. این فایل ماژولی را که اکنون ساخته‌ایم وارد کرده و تابع make_pizza() را دوبار فراخوانی می‌کند.

1
2
3
4
import pizza

pizza.make_pizza(16, 'pepperoni')
pizza.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

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

برای فراخوانی تابعی از یک ماژولِ واردشده، نام ماژول را، در اینجا pizza، و پس از آن نام تابع را، در اینجا make_pizza() بنویسید و بین این دو یک نقطه قرار دهید (۱). خروجی این کد و برنامه‌ی اصلی که ماژولی را وارد نمی‌کرد، یکسان است:

1
2
3
4
5
6
7
Making a 16-inch pizza with the following toppings:
- pepperoni

Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese

اوّلین رویکرد برای وارد کردن، که در آن به سادگی دستور import و نام ماژول را پس از آن می‌نویسید، تمام تابع‌های موجود در ماژول در برنامه‌ی شما در اختیار قرار می‌دهد. اگر این نوع import برای وارد کردن کامل یک ماژول به نام module_name.py استفاده کنید، هر تابع این ماژول از طریق دستور زیر در دسترس است:

1
module_name.function_name()

وارد کردن تابع‌های مشخّص

در عین حال می‌توانید تنها تابع خاصّی را از یک ماژول وارد کنید. این دستورِ کلی برای چنین رویکردی است:

1
from module_name import function_name

می‌توانید هر تعداد تابع را که خواستید از یک ماژول وارد کنید. برای این کار نام هر تابع را با یک کاما جدا کنید:

1
from module_name import function_0, function_1, function_2

اگر بخواهید تنها تابعی را که مورد نیاز است وارد کنیم، مثال making_pizzas.py به شکل زیر درمی‌آید:

1
2
3
4
from pizza import make_pizza

make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

با این دستور دیگر نیازی به استفاده از نقطه برای فراخوانی تابع ندارید. می‌توانیم هنگام استفاده از تابع آن را با نام فراخوانی کنیم، زیرا تابع make_pizza() را به طور صریح در عبارت import وارد کرده‌ایم.

استفاده از as برای تعیین نام مستعار 78 برای تابع

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

در اینجا با وارد کردنِ ماژول به صورت make_pizza as mp، به تابع make_pizza() نام مستعار mp() می‌دهیم. کلیدواژه‌ی as نام تابع را به نام مستعاری که تعیین کرده‌اید تغییر می‌دهد:

1
2
3
4
from pizza import make_pizza as mp

mp(16, 'pepperoni')
mp(12, 'mushrooms', 'green peppers', 'extra cheese')

عبارت import که اینجا نمایش داده شده نام تابع make_pizza() را در این برنامه به mp() تغییر می‌دهد. هر زمان که بخواهید می‌توانید برای فراخوانی به جای make_pizza() از mp() استفاده کنید و پایتون کد make_pizza() را اجرا خواهد کرد بدون این که با تابع make_pizza() دیگری که احتمالاً در برنامه‌ی خود تعریف کرده‌اید، هیچ گونه تداخلی به وجود بیاید.

دستور عمومی برای ارائه کردن نام مستعار به این صورت است:

1
from module_name import function_name as fn

استفاده از as برای تعیین نام مستعار برای یک ماژول

شما همچنین می‌توانید برای یک ماژول نام مستعار بگذارید. تعیین یک نام مستعار کوتاه، مانند p به جای pizza، باعث می‌شود که فراخوانی تابع‌های آن ماژول سریع‌تر انجام گیرد. فراخوانی p.make_pizza() بسیار خلاصه‌تر از pizza.make_pizza() است:

1
2
3
4
import pizza as p

p.make_pizza(16, 'pepperoni')
p.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

در عبارت import به ماژول pizza نام مستعار p داده‌ایم، امّا تابع‌های ماژول نام‌های اصلی خود را حفظ می‌کنند. فراخواندن تابع‌ها با نوشتن p.make_pizza() به جای pizza.make_pizza() نه تنها مختصرتر است بلکه توجّه شما از نام ماژول برمی‌گرداند و امکان می‌دهد که روی نام‌های گویای تابع‌های آن تمرکز کنید. نام این تابع‌ها به وضوح به شما می‌گویند که این تابع چه می‌کند و به‌کارگیری آن‌ها نسبت به به‌کارگیری نام کامل ماژول، برای خوانایی برنامه‌ی شما اهمّیّت بیشتری دارند.

دستور کلّی برای این رویکرد این است:

1
import module_name as mn

وارد کردن تمام تابع‌های یک ماژول

با استفاده از عملگر ستاره (*) می‌توانید از پایتون بخواهید که تمام تابع‌های یک ماژول را وارد کند:

1
2
3
4
from pizza import *

make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

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

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

1
from module_name import *

1.8.8 - شکل‌دهی به تابع‌ها

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

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

اگر برای پارامتری مقدار پیش‌فرض تعیین می‌کنید، هیچ فاصله‌ای نباید در اطراف علامت تساوی قرار بگیرد:

1
def function_name(parameter_0, parameter_1='default value'):

چنین قراردادی باید هنگام استفاده از نشان‌وندهای کلیدواژه‌ای در فراخوانی تابع هم مورد استفاده قرار گیرد:

1
function_name(value_0, parameter_1='value')

PEP 8 توصیه می‌کند که تعداد کاراکترهای هر خطّ کد را به ۷۹ عدد محدود کنید تا هرکدام از خط‌ها در پنجره‌ای با اندازه‌ی معقول قابل نمایش باشد. اگر مجموعه‌ای از پارامترها باعث می‌شوند که تعریفِ تابع بیش از ۷۹ کاراکتر شود، در تعریفِ تابع و پس از پرانتز باز یک ENTER بزنید. در خطّ بعدی دوبار TAB بزنید تا فهرست نشان‌وندها را از بدنه‌ی تابع، که یک مرحله تورفتگی دارد، جدا کنید.

اکثر ویرایشگرها خط‌های اضافیِ پارامترها را جوری تنظیم می‌کنند که با تورفتگی خطّ اول تناسب داشته باشد:

1
2
3
4
def function_name(
        parameter_0, parameter_1, parameter_2,
        parameter_3, parameter_4, parameter_5):
    function body...

اگر برنامه یا ماژول شما بیش از یک تابع دارد، می‌توانید آن‌ها را با دو خطّ خالی از یکدیگر جدا کنید تا پایان یک تابع و آغاز تابع بعدی آسان‌تر به چشم بیاید.

تمام عبارت‌های import باید در ابتدای یک فایل نوشته شوند. تنها استثنا این است که شما توضیحاتی در اوّل فایل برنامه بیاورید که کلیّت آن را شرح دهد.

خودآزمایی

۸-۱۵. چاپ کردن مدل‌ها: تابع‌های مثال printing_models.py را در یک فایل جداگانه به نام printing_functions.py قرار دهید. یک عبارت import بالای printing_models.py بنویسید و فایل را طوری تغییر دهید که از تابع‌های واردشده استفاده کند.

۸-۱۶. راه‌های مختلف وارد کردن: از یکی از برنامه‌های خود که یک تابع دارد استفاده و آن تابع را در یک فایل مجزّا ذخیره کنید. تابع را در برنامه‌ی اصلی خود وارد کنید و آن را با هر یک از رویکردهای زیر فراخوانی کنید:

1
2
3
4
5
6
7
8
9
import module_name

from module_name import function_name

from module_name import function_name as fn

import module_name as mn

from module_name import *

۸-۱۷. شکل‌دهی به تابع‌ها: سه برنامه از برنامه‌هایی که در این فصل نوشته‌اید انتخاب کنید و مطمئن شوید که در نوشتن آن‌ها شیوه‌نامه‌ی شکل‌دهی این بخش رعایت شده باشد.

خلاصه

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

یکی از هدف‌های شما به عنوان برنامه‌نویس نوشتنِ کد ساده برای انجام کارِ مورد نظرتان است و تابع‌ها به شما برای رسیدن به چنین هدفی کمک می‌کنند. آن‌ها این امکان را فراهم می‌آورند که قطعه‌کدهایی بنویسید و پس از این که فهمیدید کار می‌کنند آن‌ها را کناری بگذارید. زمانی که بفهمید تابعی وظیفه‌ی خود را درست انجام می‌دهد می‌توانید اعتماد کنید که همیشه درست کار خواهد کرد و به وظیفه‌ی برنامه‌نویسی بعدی خود بپردازید.

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

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

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

در فصل ۹ نوشتنِ کلاس‌ها را می‌آموزید. کلاس‌ها تابع‌ها و داده‌ها را در یک بسته‌بندی منظّم قرار می‌دهند که می‌توانید از آن‌ها به روش‌های منعطف و کارآمدی استفاده کنید.


1.9 - کلاس‌ها

1.9.1 - ساختن و به‌کارگیری یک کلاس

برنامه ‌نویسی شیء‌گرا یکی از موثّ 79 رترین رویکردها برای نوشتن نرم‌افزار است. در برنامه‌نویسی شیءگرا شما کلاس‌هایی می‌سازید که بیانگر مسائل دنیای واقعی هستند و سپس شیءها را بر اساس آن‌ها ایجاد می‌کنید. با ساختنِ یک کلاس رفتاری عمومی را برای دسته‌ای از اشیاء تعیین می‌کنید.

زمانی که از یک کلاس، چند شیءِ جداگانه می‌سازیم، هر شیء به طور خودکار به این رفتار عمومی مجهّز می‌شود؛ پس از آن می‌توانید به هر کدام از اشیاء هر ویژگی یا خصلتی که خواستید اضافه کنید. این که وضعیت‌های دنیای واقعی با برنامه‌نویسی شیءگرا چگونه به خوبی مدل می‌شوند، موجب شگفتی شما خواهد شد.

ساختن شیء از روی یک کلاس را نمونه‌سازی گویند و شما با نمونه‌های یک کلاس کار خواهید کرد. در این فصل کلاس‌ها را ایجاد کرده و نمونه‌هایی از روی آن‌ها می‌سازید. نوع اطّلاعاتی را که ممکن است در نمونه‌ها ذخیره شود، تعیین و همچنین کنش‌هایی را که می‌توان با این نمونه‌ها انجام داد، تعریف می‌کنید. کلاس‌هایی را هم برای گسترشِ کارآمدیِ کلاس‌های موجود ایجاد خواهید کرد تا کلاس‌های مشابه بتوانند به طور موثّری کد را بین خود به اشتراک بگذارند. کلاس‌های خود را در ماژول‌ها ذخیره و کلاس‌هایی را که دیگر برنامه‌نویسان نوشته‌اند در فایل‌های برنامه‌ی خود وارد می‌کنید.

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

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

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

ساختن کلاس Dog

هر نمونه‌ای که از کلاس Dog ساخته شود یک نام (name) و یک سن (age) ذخیره می‌کند و به هر سگ دو قابلیّت نشستن (sit()) و غلت زدن (roll_over()) خواهد داد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Dog:
    """A simple attempt to model a dog."""

    def __init__(self, name, age):
        """Initialize name and age attributes."""
        self.name = name
        self.age = age

    def sit(self):
        """Simulate a dog sitting in response to a command."""
        print(f"{self.name} is now sitting.")

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

متد __init__()

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

تابع __init__() را طوری تعریف می‌کنیم که سه پارامتر داشته باشد: self، name و age. پارامتر self در تعریفِ متد الزامی است و باید پیش از دیگر پارامترها ذکر شود. این پارامتر باید در تعریفِ متد آورده شود زیرا زمانی‌که پایتون بعداً (هنگامِ ساختن یک نمونه از Dog) این متد را فراخوانی می‌کند، به طور خودکار نشان‌وند self را به آن انتقال می‌دهد. فراخوانی هر کدام از متدهای مربوط به یک نمونه به طور خودکار نشان‌وند self را انتقال می‌دهد که در واقع ارجاعی به خودِ نمونه است و به یک نمونه‌ی مجزّا امکان می‌دهد به مشخّصه‌ها و متدهای یک کلاس دسترسی داشته باشد. در زمانِ ساختنِ نمونه‌ای از Dog، پایتون متد __init__() را از کلاس Dog فراخوانی می‌کند. یک نام و یک سن را به عنوان نشان‌وند به Dog() می‌دهیم؛ self به طور خودکار منتقل می‌شود و نیازی به ذکر آن نیست. هر بار خواستیم نمونه‌ای از کلاس Dog بسازیم، تنها مقدارهای دو پارامتر آخر، name و age را ارائه می‌کنیم.

دو متغیّرِ تعریف‌شده در __init__() هر دو پیش‌وند self دارند. هر متغیّری که پیش‌وندِ self داشته باشد در دسترسِ تمام متدهای کلاس خواهد بود و ما نیز از82 طریقِ نمونه‌هایی که از کلاس ساخته می‌شود به این متغیّرها دسترسی داریم. خط self.name = name مقداری را که به پارامتر name داده شده به متغیّر name نسبت می‌دهد و سپس این متغیّر، به نمونه‌ای که ساخته می‌شود الصاق می‌شود. چنین فرآیندی برای self.age = age هم طی می‌شود. به متغیّری که به این شکل و از طریق نمونه‌ها در دسترس است مشخّصه می‌گویند.

در کلاس Dog دو متد دیگر نیز تعریف شده است: sit() و roll_over(). چون این متدها برای اجرا شدن به اطّلاعات اضافی نیاز ندارند آن را تنها با یک پارامتر، self، تعریف می‌کنیم. نمونه‌هایی که بعداً می‌سازیم به این متدها دسترسی خواهند داشت. به بیانِ دیگر آن‌ها می‌توانند بنشینند یا غلت بزنند. فعلاً sit() و roll_over() کار زیادی نمی‌کنند. آن‌ها تنها پیامی چاپ می‌کنند که سگ در حال نشستن یا غلت زدن است. امّا این مفهوم را می‌توان به وضعیّت‌های واقعی بسط داد: اگر این کلاس بخشی از یک بازی رایانه‌ای واقعی بود، این متدها کدی را شامل می‌شدند که باعث می‌شد سگی انیمیشنی بنشیند یا غلت بزند. اگر این کلاس برای کنترل کردن رباتی نوشته بود، این متدها حرکت‌هایی را طوری هدایت می‌کردند که به نشستن یا غلت زدن سگِ رباتی منجر می‌شوند.

ساختن یک نمونه از یک کلاس

به کلاس به عنوان مجموعه‌ای دستورالعمل‌ها برای چگونگی ساختنِ یک نمونه نگاه کنید. کلاس Dog مجموعه‌ای از دستورالعمل‌هاست که به پایتون می‌گوید چگونه نمونه‌های جداگانه‌ای بسازد که هر کدام بیانگر سگِ خاصّی هستند.

اکنون نمونه‌ای که نماینده‌ی سگ خاصّی باشد می‌سازیم:

1
2
3
4
5
6
7
8
class Dog:
    --snip--

my_dog = Dog('Willie', 6)

print(f"My dog's name is {my_dog.name}.")

print(f"My dog is {my_dog.age} years old.")

کلاس Dog در اینجا همانی است که در مثال قبلی تعریف کردیم. از پایتون می‌خواهیم که سگی به نام 'Willie' با ۶ سال سن بسازد. هنگامی که پایتون این خط را می‌خواند، متد __init__() را در Dog با نشان‌وندهای 'Willie' و 6 فراخوانی می‌کند. متد __init__() نمونه‌ای بیانگر این سگِ خاص می‌سازد و مشخّصه‌های name و age را با توجه به مقادیرِ ارائه شده، تعیین می‌کند. سپس پایتون نمونه‌ای را که بیانگر این سگ است برمی‌گرداند. این نمونه را به متغیّر my_dog نسبت می‌دهیم. قرارداد نام‌گذاری اینجا سودمند است: معمولاً می‌توانیم فرض کنیم که نام‌هایی مانند Dog که حرف اوّل آن‌ها بزرگ است به یک کلاس و آن‌هایی که با حروف کوچک نوشته می‌شوند، مانند my_dog، به یک نمونه از آن کلاس اشاره دارند.

دسترسی به مشخّصه‌ها

برای دسترسی به مشخّصه‌های یک نمونه، از نمادگذاریِ نقطه استفاده می‌کنید. به این ترتیب به مقدار مشخّصه‌ی name از نمونه‌ی my_dog دسترسی پیدا می‌کنیم:

1
my_dog.name

در پایتون نماد نقطه بسیار به کار می‌رود. این دستور نشان می‌دهد که چگونه پایتون مقدار یک مشخّصه را پیدا می‌کند. اینجا پایتون به نمونه‌ی my_dog نگاه می‌کند و سپس مشخّصه‌ی name را که به آن وابسته است پیدا می‌کند. این همان مشخّصه‌ای است که در کلاس Dog به عنوان self.name مورد ارجاع قرار گرفت. از همین رویکرد برای کار کردن با مشخّصه‌ی age استفاده کرده‌ایم.

خروجی این کد خلاصه‌ای است که درباره‌ی my_dog می‌دانیم:

1
2
3
My dog's name is Willie.

My dog is 6 years old.

فراخوانی متدها

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

1
2
3
4
5
6
7
8
class Dog:
    --snip--

my_dog = Dog('Willie', 6)

my_dog.sit()

my_dog.roll_over()

برای فراخوانیِ یک متد، نامِ نمونه (در این مورد my_dog) و نام متدی را که می‌خواهید فراخوانی کنید بنویسید و بین آن‌ها یک نقطه بگذارید. زمانی که پایتون my_dog.sit() را می‌خواند به دنبال متد sit() در کلاس Dog می‌گردد و کد آن را اجرا می‌کند. پایتون خط my_dog.roll_over() را به همین ترتیب تفسیر می‌کند.

اکنون ویلی آن‌چه را به او فرمان داده‌ایم اجرا می‌کند:

1
2
3
Willie is now sitting.

Willie rolled over!

این نوع نوشتن بسیار سودمند است. زمانی که مشخّصه‌ها و متدها نام‌های گویایی داشته باشند، مانند name، age، sit() و roll_over()، می‌توانیم به سادگی به آن‌چه کد قرار است انجام دهد پی ببریم هر چند قبلاً هرگز آن را ندیده باشیم.

ساختن چند نمونه

می‌توانید هر تعداد نمونه که بخواهید از یک کلاس بسازید. بیاید سگ دومی به نام your_dog بسازیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Dog:
    --snip--

my_dog = Dog('Willie', 6)

your_dog = Dog('Lucy', 3)

print(f"My dog's name is {my_dog.name}.")

print(f"My dog is {my_dog.age} years old.")

my_dog.sit()

print(f"\nYour dog's name is {your_dog.name}.")

print(f"Your dog is {your_dog.age} years old.")

your_dog.sit()

در این مثال سگی به نام ویلی و سگی دیگر به نام لوسی ساخته‌ایم. هر سگ نمونه‌ی جداگانه‌ی خود را با مجموعه‌ی مشخّصه‌های مخصوص به خود دارد و قادر به انجام کارهای مشترکی است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
My dog's name is Willie.

My dog is 6 years old.

Willie is now sitting.

Your dog's name is Lucy.

Your dog is 3 years old.

Lucy is now sitting.

حتّی اگر از یک نام و سن برای سگ دوم استفاده می‌کردیم، باز هم پایتون یک نمونه‌ی جداگانه از کلاس Dog می‌ساخت. شما می‌توانید هر تعداد نمونه که خواستید از یک کلاس بسازید، تا زمانی که به هر نمونه یک نام متغیّر یکتا بدهید یا نمونه‌ جای یکتایی را در یک لیست یا دیکشنری اشغال کند.

خودآزمایی

۹-۱. رستوران: کلاسی به نام Restaurant بسازید. متد __init__() در Restaurant باید دو مشخّصه را ذخیره کند: restaurant_name و cuisine_type. متدی به نام describe_restaurant() بسازید که این دو دسته اطّلاعات را چاپ کند، متدی هم به نام open_restaurant() بسازید که پیامی برای باز کردنِ رستوران چاپ کند.

نمونه‌ای از کلاس خود به نام restaurant بسازید. دو مشخّصه را جداگانه چاپ کنید و سپس دو متد آن را فرابخوانید.

۹-۲. سه رستوران: از کلاسی که در تمرین ۹-۱ تعریف کردید شروع کنید. سه نمونه‌ی مختلف از این کلاس بسازید و describe_restaurant() را برای هر نمونه فراخوانی کنید.

۹-۳. کاربران: کلاسی به نام User بسازید. دو مشخّصه به نام‌های first_name و last_name بسازید و سپس چند مشخّصه‌ی دیگر که معمولاً در نمایه‌ی یک کاربر ذخیره می‌شود به آن اضافه کنید. متدی به نام describe_user() بسازید که خلاصه‌ای از اطّلاعات یک کاربر را چاپ می‌کند. متد دیگری به نام greet_user() بسازید که یک پیام احوال‌پرسی شخصی برای کاربر چاپ می‌کند.

چندین نمونه که هر کدام بیانگر کاربر متفاوتی است بسازید و برای هر کدام هر دو متد را فراخوانی کنید.

1.9.2 - کار کردن با کلاس‌ها و نمونه‌ها

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

کلاس Car

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Car:
    """A simple attempt to represent a car."""

    def __init__(self, make, model, year):
        """Initialize attributes to describe a car."""
        self.make = make
        self.model = model
        self.year = year

    def get_descriptive_name(self):
        """Return a neatly formatted descriptive name."""
        long_name = f"{self.year} {self.manufacturer} {self.model}"
        return long_name.title()

my_new_car = Car('audi', 'a4', 2019)

print(my_new_car.get_descriptive_name())

متد __init__() را با پارامتر ابتدایی self تعریف می‌کنیم. همچنین سه پارامتر دیگر به آن می‌دهیم: make، model و year. متد __init__() این پارامترها را می‌گیرد و آن‌ها را به مشخّصه‌هایی که با نمونه‌های ساخته‌شده از این کلاس در ارتباطند، نسبت می‌دهد. زمانی که نمونه‌ی جدیدی از کلاس Car می‌سازیم باید نام شرکت سازنده، مدل و سال ساخت آن را برای نمونه‌ی خود تعیین کنیم.

متدی به نام get_descriptive_name() تعریف می‌کنیم که year، make و model را در یک رشته قرار می‌دهد که خودرو را با سادگی و آراستگی توصیف می‌کند. این متد ما را از چاپ کردن جداگانه‌ی مقدار هر مشخّصه خلاص می‌کند. برای کار کردن با مقادیر مشخّصه‌های در این متد از self.make، self.model و self.year استفاده می‌کنیم. نمونه‌ای از کلاس Car می‌سازیم و آن را به متغیّر my_new_car نسبت می‌دهیم. سپس get_descriptive_name() را فراخوانی می‌کنید تا نوع خودروی ما را مشخّص کند:

1
2019 Audi A4

برای جالب‌تر کردن کلاس، مشخّصه‌ای به آن اضافه می‌کنیم که با زمان تغییر کند. این مشخّصه مسافت پیموده شده‌ی کلّی خودرو را ذخیره می‌کند.

تعیین کردن مقداری پیش‌فرض برای یک مشخّصه

می‌توان هنگامِ ساختنِ یک نمونه، مشخّصه‌ای را بدون انتقال آن به صورت پارامتر تعریف کرد. این مشخّصه‌ها می‌توانند در متد __init__() تعریف شوند و در آن‌جا به آن‌ها مقداری پیش‌فرض داده شود.

مشخّصه‌ای به نام odometer_reading اضافه می‌کنیم که همیشه در ابتدا مقدار ۰ دارد. همچنین متدی به نام read_odometer() اضافه می‌کنیم که ما را در خواندن مقدار کیلومترشمار هر خودرو کمک می‌کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Car:

    def __init__(self, make, model, year):
        """Initialize attributes to describe a car."""
        self.make = make
        self.model = model
        self.year = year
        self.odometer_reading = 0

    def get_descriptive_name(self):
        --snip

    def read_odometer(self):
        """Print a statement showing the car's mileage."""
        print(f"This car has {self.odometer_reading} miles on it.")

my_new_car = Car('audi', 'a4', 2019)

print(my_new_car.get_descriptive_name())

my_new_car.read_odometer()

این بار پایتون برای ساختن نمونه‌ی جدید متد __init__() را فرامی‌خواند که مقدار سازنده، مدل و سال را مانند مثال قبلی به عنوان مشخّصه ذخیره می‌کند. سپس پایتون مشخّصه‌ای جدید به نام odometer_reading می‌سازد و به آن مقدارِ اوّلیّه‌ی ۰ می‌دهد. همچنین متد جدیدی به نام read_odometer() دارید که خواندن کارکرد خودرو را ساده می‌کند.

کارکرد خودروی ما در ابتدا ۰ است:

1
2
3
2019 Audi A4

This car has 0 miles on it.

تمام خودروها با کارکرد اوّلیّه‌ی دقیقاً ۰ فروخته نمی‌شوند پس نیاز به روشی داریم تا مقدار این مشخّصه را تغییر دهیم.

تغییردادن مقدار مشخّصه‌ها

مقدار یک مشخّصه را می‌توانید به سه روش عوض کنید: تغییر دادن مستقیم از طریق یک نمونه، تعیین کردن مقدار از طریق یک متد یا افزایش دادن مقدار (افزودن مقداری به آن) به کمک یک متد. به هر یک از این رویکردها نگاهی می‌اندازیم.

تغییر دادن مستقیمِ مقدار یک مشخّصه

ساده‌ترین راه برای تغییرِ مقدارِ یک مشخّصه این است که از طریق یک نمونه به طور مستقیم به آن دسترسی پیدا کنیم. اینجا مقدار کیلومترشمار را به طور مستقیم ۲۳ قرار می‌دهیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Car:
    --snip--

my_new_car = Car('audi', 'a4', 2019)

print(my_new_car.get_descriptive_name())

my_new_car.odometer_reading = 23

my_new_car.read_odometer()

از نماد نقطه برای دسترسی به مشخّصه‌ی odometer_reading خودرو استفاده کرده و مقدار آن را به طور مستقیم تعیین می‌کنیم. این خط به پایتون می‌گوید که نمونه‌ی my_new_car را بگیر، مشخّصه‌ی odometer_reading مربوط به آن را پیدا کن و مقدار آن را برابر با ۲۳ بگذار:

1
2
3
2019 Audi A4

This car has 23 miles on it.

گاهی می‌خواهید مانندِ اینجا به مشخّصه‌ها به طور مستقیم دسترسی پیدا کنیم امّا برخی اوقات متدی می‌نویسید که مقدار را برای شما به‌روز کند.

تغییر دادن مقدار یک مشخّصه از طریق یک متد

داشتنِ متدهایی برای به‌روزرسانی مشخّصه‌های خاصّی می‌تواند سودمند باشد. می‌توانید به جای دسترسی مستقیم به مشخّصه، مقدار جدید را به متدی بدهیم که به صورت داخلی این به‌روزرسانی را انجام می‌دهد.

مثالِ اینجا متدی به نام update_odometer() را نشان می‌دهد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Car:
    --snip--

    def update_odometer(self, mileage):
        """Set the odometer reading to the given value."""
        self.odometer_reading = mileage

my_new_car = Car('audi', 'a4', 2019)

print(my_new_car.get_descriptive_name())

my_new_car.update_odometer(23)

my_new_car.read_odometer()

تنها تغییرِ ایجادشده در Car افزوده شدنِ update_odometer() است. این متد مقدار کارکرد را گرفته و به مشخّصه‌ی self.odometer_reading نسبت می‌دهد. update_odometer() را فراخوانی می‌کنیم و مقدار ۲۳ را به عنوان نشان‌وند به آن می‌دهیم (که به پارامتر mileage در تعریفِ متد مربوط است.) این متد مقدار کیلومترشمار را ۲۳ می‌کند و read_odometer() این مقدار را چاپ می‌کند:

1
2
3
2019 Audi A4

This car has 23 miles on it.

می‌توانیم متد update_odometer() را طوری گسترش دهیم که در هر بار تغییرِ مقدارِ کیلومترشمار، کاری انجام دهد. اجازه دهید منطقی کوچکی اضافه کنیم تا مطمئن شویم هیچ‌کس مقدار کیلومترشمار را به عقب برنمی‌گرداند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Car:
    --snip--

    def update_odometer(self, mileage):
        """
        Set the odometer reading to the given value.
        Reject the change if it attempts to roll the odometer back.
        """
        if mileage >= self.odometer_reading:
            self.odometer_reading = mileage
        else:
            print("You can't roll back an odometer!")

اکنون update_odometer() پیش از تغییر دادنِ مشخّصه، مقدار جدید را بررسی می‌کند. اگر کارکرد جدید، mileage، بزرگتر یا مساوی کارکرد فعلی (self.odometer_reading) باشد، می‌توانید مقدار فعلی کیلومترشمار را به مقدار جدید به‌روزرسانی کنید. اگر مقدار جدید کمتر از مقدار فعلی باشد، اخطاری دریافت می‌کنید مبنی بر این که نمی‌توانید مقدار کیلومترشمار را به عقب برگردانید.

افزایش مقدار مشخّصه از طریق یک متد

گاهی می‌خواهید به جای تعیین یک مقدار کاملاً جدید برای یک مشخّصه، مقدار آن را به میزان مشخّصی افزایش دهید. فرض کنید خودرویی کارکرده خریده‌ایم و در فاصله‌ی بین خریدن و سند زدن، ۱۰۰ مایل کار کرده است. متدی که اینجاست مقدار افزایشی را دریافت کرده و آن را به مقدار فعلی کیلومترشمار اضافه می‌کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Car:
    --snip--

    def update_odometer(self, mileage):
        --snip--

    def increment_odometer(self, miles):
        """Add the given amount to the odometer reading."""
        self.odometer_reading += miles

my_used_car = Car('subaru', 'outback', 2015)

print(my_used_car.get_descriptive_name())

my_used_car.update_odometer(23_500)

my_used_car.read_odometer()

my_used_car.increment_odometer(100)

my_used_car.read_odometer()

متد جدید increment_odometer() تعداد مایل پیموده‌شده را گرفته و آن را به self.odometer_reading اضافه می‌کند. یک خودروی کارکرده، my_used_car، ایجاد می‌کنیم. مقدار کیلومترشمار آن را با فراخوانی update_odometer() و انتقال مقدار 23_500 به آن، ۲۳،۵۰۰ می‌گذاریم. سپس increment_odometer() را صدا کرده و مقدار 100 را به آن می‌دهیم تا ۱۰۰ مایل پیموده‌شده بین خریدن و ثبت خودرو را اضافه کند:

1
2
3
4
5
2015 Subaru Outback

This car has 23500 miles on it.

This car has 23600 miles on it.

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

نکته
این که کاربرانِ برنامه‌ی شما چگونه مقدارهایی نظیر مقدار کیلومترشمار را به‌روزرسانی می‌کنند، به کمک چنین متدهایی قابل کنترل است امّا هرکس که به برنامه دسترسی داشته باشد می‌تواند این مقدار را با دسترسی مستقیم به مشخّصه‌ها مستقیماً دستکاری کند. امنیّت موثّر علاوه بر بررسی‌های ابتدایی مانند بالا، توجّه بسیار بیشتری به جزئیات می‌طلبد.

خودآزمایی

۹-۴. تعداد مشتری‌ها: از برنامه‌ی خود در تمرین ۹-۱ شروع کنید. مشخّصه‌ای به نام number_served با مقدار پیش‌فرض ۰ اضافه کنید. نمونه‌ای به نام restaurant از این کلاس بسازید. تعداد مشتری‌هایی را که رستوران پذیرای آن‌ها بوده چاپ کنید و سپس با تغییر این مقدار آن را دوباره چاپ کنید.

متدی به نام set_number_served() اضافه کنید که امکان تعیین تعداد مشتری‌ها را فراهم می‌آورد. این متد را با عدد جدیدی فراخوانی کنید و مقدار را دوباره چاپ کنید.

متدی به نام increment_number_served() اضافه کنید که به شما امکان افزایش تعداد مشتری‌ها را می‌دهد. این متد را با هر عددی که دوست دارید فرابخوانید که بیانگر تعداد مهمان‌های پذیرفته‌شده در یک روز کاری باشد.

۹-۵. تعداد تلاش‌ها برای ورود: مشخّصه‌ای به نام login_attempts به کلاس User در تمرین ۹-۳ اضافه کنید. متدی به نام increment_login_attempts() بنویسید که مقدار login_attempts را یکی افزایش می‌دهد. متد دیگری به نام reset_login_attempts() بنویسید که مقدار login_attempts را صفر می‌کند.

نمونه‌ای از کلاس User بسازید و increment_login_attempts() را چندین بار صدا کنید. مقدار login_attempts را چاپ کنید تا مطمئن شوید مقدار آن افزایش یافته است و سپس reset_login_attempts() را فراخوانی کنید. مقدار login_attempts را بار دیگر چاپ کنید تا مطمئن شوید مقدار آن صفر شده است.

1.9.3 - ارث‌بری

همیشه نیاز نیست نوشتنِ یک کلاس 85 84 را ازد85 . اگر کلاسی که در حال نوشتن آن هستید، خود نسخه‌ای اختصاصی‌شده از کلاسی دیگر باشد که قبلاً نوشته‌اید، می‌توانید از ارث‌بری استفاده کنید. هنگامی که کلاسی از کلاس دیگری ارث می‌برد، مشخّصه‌ها و متدهای کلاس اوّل را دریافت می‌کند. به کلاس اوّلّیه و اصلی کلاس والد و به کلاس جدید کلاس فرزند گویند. کلاس فرزند می‌تواند یکی یا تمام مشخّصه‌ها و متدهای کلاس والد را به ارث ببرد امّا در عین حال آزاد است که مشخّصه‌ها و متدهای خود را نیز تعریف کند.

متد __init__() برای کلاس فرزند

زمانی که کلاس جدیدی را بر پایه‌ی یک کلاس موجود می‌نویسید، اغلب می‌خواهید متد __init__() کلاس والد را فراخوانی کنید. این کار موجب مقداردهی اوّلیّه‌ی تمام مشخّصه‌های تعریف‌شده در متد __init__() والد خواهد شد و آن‌ها را در کلاس فرزند در دسترس قرار می‌دهد.

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

از ساختن نسخه‌ی ساده‌ای از کلاس ElectricCar شروع می‌کنیم که رفتار آن مشابه کلاس Car است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Car:
    """A simple attempt to represent a car."""

    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
        self.odometer_reading = 0

    def get_descriptive_name(self):
        long_name = f"{self.year} {self.manufacturer} {self.model}"
        return long_name.title()

    def read_odometer(self):
        print(f"This car has {self.odometer_reading} miles on it.")

    def update_odometer(self, mileage):
        if mileage >= self.odometer_reading:
            self.odometer_reading = mileage
        else:
            print("You can't roll back an odometer!")

    def increment_odometer(self, miles):
        self.odometer_reading += miles

class ElectricCar(Car):
    """Represent aspects of a car, specific to electric vehicles."""

    def __init__(self, make, model, year):
        """Initialize attributes of the parent class."""
        super().__init__(make, model, year)

my_tesla = ElectricCar('tesla', 'model s', 2019)

print(my_tesla.get_descriptive_name())

کلاس والد باید در فایل فعلی و پیش از تعریف کلاس فرزند در همان فایل ظاهر شود. کلاس فرزند، ElectricCar را تعریف می‌کنیم. نام کلاس والد باید هنگام تعریف کلاسِ فرزند، داخلِ پرانتز آورده شود. متد __init__() اطّلاعات لازم را برای ساختن یک نمونه از Car می‌گیرد.

تابع super() تابعی ویژه است که امکان فراخوانی متدی را از کلاس 87 86 ی87 ‌آورد. این خط به پایتون می‌گوید که متد __init__() را از Car فراخوانی کند، تا به یک نمونه‌ی ElectricCar تمام مشخّصه‌های تعریف‌شده در آن متد داده شود. نام super از قراردادی می‌آید که در آن به کلاس والد ابرکلاس و به کلاس فرزند زیرکلاس گویند.

اکنون پس از ساختنِ یک خودروی الکتریکی بر اساس اطّلاعاتِ خودروی معمولی، بررسی می‌کنیم که آیا ارث‌بری به خوبی کار می‌کند یا نه. نمونه‌ای از کلاس ElectricCar می‌سازیم و آن را به my_tesla نسبت می‌دهیم. این خط متد __init__() تعریف‌شده در ElectricCar را فراخوانی می‌کند که خود به پایتون می‌گوید که متد __init__() تعریف‌شده در کلاس والد Car را صدا کند. نشان‌وندهای 'tesla'، 'model s' و 2019 را ارائه می‌کنیم.

به جز __init__() هیچ مشخّصه یا متدی وجود ندارد که مخصوصِ یک خودروی الکتریکی باشد. تا اینجا فقط مطمئن می‌شویم که خودروی الکتریکی رفتارهای متناسب Car را دارد یا نه:

1
2019 Tesla Model S

نمونه‌ی ElectricCar درست مانند یک نمونه‌ی Car کار می‌کند، بنابراین اکنون تعریف کردنِ متدها و مشخّصه‌های مختصّ خودروهای الکتریکی را آغاز می‌کنیم.

تعریف کردن مشخّصه‌ها و متدهای کلاس فرزند

به محضِ ساختن یک کلاس فرزند که از کلاس والدی ارث می‌برد، می‌توانید هر مشخّصه یا متد جدیدی را که برای تمایز ایجاد کردن بین کلاس فرزند و والد نیاز است، اضافه کنید.

بیاید مشخّصه‌ای را که مختصّ خودروهای الکتریکی است (مثلاً باتری) و متدی که این مقدار این مشخّصه را گزارش می‌کند، اضافه کنیم. ظرفیّت باتری و متدی را برای بیان وضعیّت باتری، ذخیره می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Car:
    --snip--

class ElectricCar(Car):
    """Represent aspects of a car, specific to electric vehicles."""

    def __init__(self, make, model, year):
        """
        Initialize attributes of the parent class.
        Then initialize attributes specific to an electric car.
        """
        super().__init__(make, model, year)
        self.battery_size = 75

    def describe_battery(self):
        """Print a statement describing the battery size."""
        print(f"This car has a {self.battery_size}-kWh battery.")

my_tesla = ElectricCar('tesla', 'model s', 2019)

print(my_tesla.get_descriptive_name())

my_tesla.describe_battery()

مشخّصه‌ی جدید self.battery_size را افزوده و مقدار اوّلیّه‌ی آن را ۷۵ گذاشته‌ایم. این مشخّصه به تمام نمونه‌هایی که از کلاس ElectricCar ساخته می‌شوند مربوط خواهد بود امّا در هیچ نمونه‌ای از Car وجود نخواهد داشت. همچنین متدی به نام describe_battery() اضافه می‌کنیم که اطّلاعات مربوط به باتری را چاپ می‌کند. زمانی که این متد را فراخوانی می‌کنیم توضیحی دریافت می‌کنیم که مختصّ یک خودروی الکتریکی است:

1
2
3
2019 Tesla Model S

This car has a 75-kWh battery.

هیچ محدودیّتی برای میزانِ اختصاصی کردن کلاس ElectricCar وجود ندارد. می‌توانید هر تعداد مشخّصه یا متد که نیاز دارید اضافه کنید تا یک خودروی الکتریکی را با هر درجه‌ای از دقت که می‌خواهید مدل کنید. هر مشخّصه یا متدی که می‌تواند به هر خودرویی تعلّق داشته باشد و مخصوص خودروی الکتریکی نیست، باید به جای کلاس ElectricCar به کلاس Car اضافه شود، تا این که هر کس از کلاس Car استفاده می‌کند به این قابلیّت هم دسترسی داشته باشد و کلاس ElectricCar تنها شامل کدی باشد که بیانگر اطّلاعات و رفتارهای مختصّ خودروهای الکتریکی باشد.

بازتعریف 88 ِ متدهای کلاس والد

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

فرض کنید کلاس Car متدی به نام fill_gas_tank() دارد. این متد برای خودروهای تماماً الکتریکی بی‌معناست بنابراین ممکن است بخواهید آن را بازتعریف کنید. یک راه برای این کار چنین است:

1
2
3
4
5
6
class ElectricCar(Car):
    *--snip--*

    def fill_gas_tank(self):
        """Electric cars don't have gas tanks."""
        print("This car doesn't need a gas tank!")

اکنون اگر کسی بخواهد متد fill_gas_tank() را برای یک خودروی الکتریکی فراخوانی کند، پایتون از متد fill_gas_tank() در کلاس Car صرف نظر کرده و به جای آن این کد را اجرا می‌کند. هنگامِ استفاده از ارث‌بری می‌توانید کاری کنید که کلاس فرزند چیزهایی را که از کلاس والد نیاز دارید حفظ کند و هر آن‌چه را نمی‌خواهید بازتعریف کنید.

نمونه‌ها در قالب مشخّصه‌ها

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

برای مثال زمانی که به افزودن جزئیات به کلاس ElectricCar ادامه می‌دهید، ممکن است متوجّه شوید که مشخّصه‌ها و متدهای زیادی را مختصّ باتری اضافه کرده‌اید. زمانی که متوجه چنین وضعیتی می‌شویم، می‌توانیم کار را متوقّف کنیم و آن مشخّصه‌ها و متدها را به کلاسی جداگانه به نام Battery منتقل کنیم. سپس از نمونه‌ی Battery به عنوان مشخّصه‌ای در کلاس ElectricCar استفاده می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Car:
    *--snip--*

class Battery:
    """A simple attempt to model a battery for an electric car."""

    def __init__(self, battery_size=75):
        """Initialize the battery's attributes."""
        self.battery_size = battery_size

    def describe_battery(self):
        """Print a statement describing the battery size."""
        print(f"This car has a {self.battery_size}-kWh battery.")

class ElectricCar(Car):
    """Represent aspects of a car, specific to electric vehicles."""

    def __init__(self, make, model, year):
        """
        Initialize attributes of the parent class.
        Then initialize attributes specific to an electric car.
        """
        super().__init__(make, model, year)
        self.battery = Battery()

my_tesla = ElectricCar('tesla', 'model s', 2019)

print(my_tesla.get_descriptive_name())

my_tesla.battery.describe_battery()

کلاس جدیدی به نام Battery تعریف می‌کنیم که از هیچ کلاسی چیزی به ارث نمی‌برد. متد __init__() علاوه بر self یک پارامتر دارد، battery_size. این پارامتر اختیاری است و اگر مقداری برای آن ارائه نشود ظرفیّت باتری را برابر ۷۵ می‌گذارد. متد describe_battery() هم به این کلاس منتقل شده است.

اکنون به کلاس ElectricCar مشخّصه‌ای به نام self.battery اضافه می‌کنیم. این خط به پایتون می‌گوید که نمونه‌ای جدید از Battery بساز (که چون مقداری برای آن تعیین نکرده‌ایم ظرفیّت پیش‌فرض آن ۷۵ است) و این نمونه‌ را به مشخّصه‌ی self.battery نسبت بده. این اتّفاق هر بار که متد __init__() فراخوانده می‌شود، رخ می‌دهد؛ اکنون هر نمونه‌ی ElectricCar یک نمونه‌ی Battery هم دارد که به طور خودکار ساخته می‌شود.

یک خودروی الکتریکی ساخته و آن را به متغیّر my_tesla نسبت می‌دهیم. زمانی که می‌خواهیم درباره‌ی باتری توضیح دهیم، باید از طریق مشخّصه‌ی battery خودرو عمل کنیم:

1
my_tesla.battery.describe_battery()

با این خط پایتون می‌فهمد که باید به نمونه‌ی my_tesla نگاه کند، مشخّصه‌ی battery آن را بیاید و متد describe_battery() را اجرا کند که به نمونه‌ی Battery ذخیره‌شده در این مشخّصه مربوط است.

خروجی عیناً مشابه آن چیزی است که قبلاً دیده‌ایم:

1
2
3
2019 Tesla Model S

This car has a 75-kWh battery.

این به نظر زیاده‌کاری می‌آید امّا اکنون می‌توانید هر چه‌قدر جزئیات که بخواهیم به باتری اضافه کنیم بدون این که کلاس ElectricCar را شلوغ و درهم‌ریخته کنیم. حالا متد دیگری به Battery اضافه می‌کنیم که مسافت قابل طی کردن با این ظرفیّت باتری را گزارش می‌کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Car:
    --snip--

class Battery:
    --snip--

    def get_range(self):
        """Print a statement about the range this battery provides."""
        if self.battery_size == 75:
            range = 260
        elif self.battery_size == 100:
            range = 315

        print(f"This car can go about {range} miles on a full charge.")

class ElectricCar(Car):
    --snip--

my_tesla = ElectricCar('tesla', 'model s', 2019)

print(my_tesla.get_descriptive_name())

my_tesla.battery.describe_battery()

my_tesla.battery.get_range()

متد جدید get_range() تحلیل ساده‌ای انجام می‌دهد. اگر ظرفیّت باتری ۷۵ کیلووات‌ساعت باشد، فاصله‌ی قابل پیمایش را ۲۶۰ مایل و اگر ۱۰۰ کیلووات‌ساعت باشد برابر با ۳۱۵ مایل می‌گذارد. سپس مقدار را گزارش می‌کند. هرگاه بخواهیم این متد را فراخوانی کنیم، باید دوباره آن را از طریق مشخّصه‌ی battery خودرو صدا کنیم.

خروجی بسته به ظرفیّت باتری، مسافتی را که خودرو می‌تواند طی کند گزارش می‌کند:

1
2
3
4
5
2019 Tesla Model S

This car has a 75-kWh battery.

This car can go about 260 miles on a full charge.

مدل کردن اشیای دنیای واقعی

همزمان که مدل کردن چیزهای پیچیده‌تر مانند خودروهای الکتریکی را شروع می‌کنید، با پرسش‌های جالب‌تری سر و کار خواهید داشت. آیا مسافت قابل پیمایش ویژگیِ خودروی الکتریکی است، یا باتری یا خودرو؟ اگر تنها یک خودرو را توصیف کنیم احتمالاً برقرار کردن ارتباط بین متد get_range() و کلاس Battery خوب است. امّا اگر بخواهیم کل خطّ تولید یک کارخانه را توصیف کنیم، ممکن است get_range() را به کلاس ElectricCar منتقل کنیم. متد get_range() پیش از محاسبه‌ی مسافت همچنان ظرفیّت باتری را در نظر می‌گیرد امّا مسافت قابل پیمایش یک نوع خاصّ خودرو را که با آن در ارتباط است گزارش می‌دهد. به جای این کار، از سوی دیگر، می‌توانیم ارتباط را بین متد get_range() و باتری برقرار کنیم امّا پارامتری مانند car_model را به آن بدهیم. در نتیجه متد get_range() مسافت را بر اساس ظرفیّت باتری و مدل خودرو گزارش خواهد کرد.

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

خودآزمایی

۹-۶. دکه‌ی بستنی‌فروشی: دکه‌ی بستنی‌فروشی را نوعی رستوران در نظر بگیرید. کلاسی به نام IceCreamStand تعریف کنید که از کلاس Restaurant که در تمرین ۹-۱ یا تمرین ۹-۴ ارث می‌برد. هر دو کلاس کار می‌کنند؛ موردی را انتخاب کنید که بیشتر دوست دارید. مشخّصه‌ای به نام flavors اضافه کنید که لیستی از طعم‌های بستنی را ذخیره کند. متدی بنویسید که این طعم‌ها را نمایش دهد. نمونه‌ای از IceCreamStand بسازید و این متد را فراخوانی کنید.

۹-۷. مدیر: مدیر سیستم نوع خاصّی کاربر است. کلاسی به نام Admin بنویسید که از کلاس User (تمرین ۹-۳ یا تمرین ۹-۵) ارث ببرد. مشخّصه‌ای به نام privileges (امتیازات) اضافه کنید که لیستی از رشته‌ها مانند "can add post"، "can delete post"، "can ban user" و … باشد. متدی به نام show_privileges() بنویسید که مجموعه‌ی امتیازات مدیر را فهرست کند. نمونه‌ای از Admin بسازید و متد خود را فرابخوانید.

۹-۸. امتیازات: کلاس جداگانه‌ای به نام Privileges بسازید. این کلاس باید یک مشخّصه داشته باشد، privileges، که لیستی از رشته‌های شرح داده‌شده در تمرین ۹-۷ در آن ذخیره شود. متد show_privileges() را به این کلاس منتقل کنید. نمونه‌ای از Privileges را به عنوان یک مشخّصه در کلاس Admin تعریف کنید. نمونه‌ای جدید از Admin بسازید و از متد خود برای نمایش امتیازات مدیر بهره بگیرید.

۹-۹. ارتقای باتری: از نسخه‌ی نهایی electric_car.py در این بخش استفاده کنید. متدی به نام upgrade_battery() به کلاس Battery اضافه کنید. این متد باید ظرفّیت باتری را بررسی کند و آن را برابر با ۱۰۰ بگذارد (البته اگر قبلاً این مقدار را نداشته است.) خودرویی الکتریکی با ظرفیّت باتری پیش‌فرض بسازید، یک بار get_range() را فراخوانی کنید. پس از ارتقای باتری به کمک متد خود، بار دیگر get_range() را صدا کنید. باید افزایشی در مسافت قابل پیمایش خودرو مشاهده کنید.

1.9.4 - وارد کردن کلاس‌ها

بیایید ماژولی بسازیم که تنها شامل کلاس Car باشد. اینجا به یک مشکل ظریف در نام‌گذاری برمی‌خوریم: ما قبلاً در این فصل فایلی به نام car.py داشته‌ایم امّا این ماژول هم باید car.py نام‌گذاری شود زیرا دربردارنده‌ی کدی بیانگر خودروست. این مشکل نامگذاری را این‌گونه حل می‌کنیم: کلاس Car را در ماژولی به نام car.py ذخیره می‌کنیم، سپس آن را به جای فایل car.py قبلی می‌گذاریم. از این به بعد هر برنامه‌ای که از این ماژول استفاده می‌کند به نام فایل ویژه‌تری نیاز دارد، مثلاً my_car.py. در اینجا، در car.py تنها باید کد کلاس Car را ببینیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
"""A class that can be used to represent a car."""

class Car:
    """A simple attempt to represent a car."""

    def __init__(self, make, model, year):
        """Initialize attributes to describe a car."""
        self.make = make
        self.model = model
        self.year = year
        self.odometer_reading = 0

    def get_descriptive_name(self):
        """Return a neatly formatted descriptive name."""
        long_name = f"{self.year} {self.manufacturer} {self.model}"
        return long_name.title()

    def read_odometer(self):
        """Print a statement showing the car's mileage."""
        print(f"This car has {self.odometer_reading} miles on it.")

    def update_odometer(self, mileage):
        """
        Set the odometer reading to the given value.
        Reject the change if it attempts to roll the odometer back.
        """
        if mileage >= self.odometer_reading:
            self.odometer_reading = mileage
        else:
            print("You can't roll back an odometer!")

    def increment_odometer(self, miles):
        """Add the given amount to the odometer reading."""
        self.odometer_reading += miles

docstringای در سطح ماژول تعریف کرده‌ایم که به طور خلاصه محتویّات ماژول را شرح می‌دهد. شما باید برای هر ماژولی که می‌سازید یک docstring بنویسید.

اکنون فایلی جداگانه به نام my_car.py می‌سازیم. این فایل کلاس Car را وارد کرده و نمونه‌ای از آن را می‌سازد:

1
2
3
4
5
6
7
8
9
from car import Car

my_new_car = Car('audi', 'a4', 2019)

print(my_new_car.get_descriptive_name())

my_new_car.odometer_reading = 23

my_new_car.read_odometer()

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

1
2
3
2019 Audi A4

This car has 23 miles on it.

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

ذخیره کردن چند کلاس در یک ماژول

شما می‌توانید هر تعداد کلاس که نیاز دارید در یک ماژول ذخیره کنید، با این حال کلاس‌های هر ماژول باید به نحوی به یکدیگر مربوط باشند. کلاس‌های Battery و ElectricCar هر دو نمایانگر خودروها هستند بنابراین آن‌ها را به ماژول car.py اضافه می‌کنیم.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
"""A set of classes used to represent gas and electric cars."""

class Car:
    --snip--

class Battery:
    """A simple attempt to model a battery for an electric car."""

    def __init__(self, battery_size=70):
        """Initialize the battery's attributes."""
        self.battery_size = battery_size

    def describe_battery(self):
        """Print a statement describing the battery size."""
        print(f"This car has a {self.battery_size}-kWh battery.")

    def get_range(self):
        """Print a statement about the range this battery provides."""
        if self.battery_size == 75:
            range = 260
        elif self.battery_size == 100:
            range = 315

        print(f"This car can go about {range} miles on a full charge.")

class ElectricCar(Car):
    """Models aspects of a car, specific to electric vehicles."""

    def __init__(self, make, model, year):
        """
        Initialize attributes of the parent class.
        Then initialize attributes specific to an electric car.
        """
        super().__init__(make, model, year)
        self.battery = Battery()

اکنون می‌توانیم فایل جدیدی به نام my_electric_car.py بسازیم، کلاس ElectricCar را وارد کنیم و یک خودروی الکتریکی بسازیم:

1
2
3
4
5
6
7
8
9
from car import ElectricCar

my_tesla = ElectricCar('tesla', 'model s', 2019)

print(my_tesla.get_descriptive_name())

my_tesla.battery.describe_battery()

my_tesla.battery.get_range()

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

1
2
3
4
5
2019 Tesla Model S

This car has a 75-kWh battery.

This car can go about 260 miles on a full charge.

وارد کردن چند کلاس از یک ماژول

شما می‌توانید هر تعداد کلاسی که نیاز داشتید در یک فایل برنامه وارد کنید. اگر بخواهیم در یک فایل یک خودروی معمولی و یک خودروی الکتریکی بسازیم، باید هر دو کلاس Car و ElectricCar را وارد کنیم:

1
2
3
4
5
6
7
8
9
from car import Car, ElectricCar

my_beetle = Car('volkswagen', 'beetle', 2019)

print(my_beetle.get_descriptive_name())

my_tesla = ElectricCar('tesla', 'roadster', 2019)

print(my_tesla.get_descriptive_name())

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

در این مثال یک خودروی معمولی Volkswagen Beetle و یک خودروی الکتریکی Tesla Roadster می‌سازیم:

1
2
3
2019 Volkswagen Beetle

2019 Tesla Roadster

وارد کردن کلّ یک ماژول

علاوه بر این روش‌ها می‌توانید کلّ یک ماژول را وارد کنید و به کلاس‌های مورد نیاز به کمک علامت نقطه دسترسی پیدا کنید. این رویکرد ساده است و به کدی منجر می‌شود که خواندن آن آسان است. از آن‌جایی‌که هر فراخوانی برای ساختن نمونه‌ی یک کلاس باید شامل نام ماژول باشد، در این روش به تداخل نام با نام‌های به کار رفته در فایل فعلی برنمی‌خورید.

وارد کلّ ماژول car و ساختن یک خودروی معمولی و الکتریکی به این شکل است:

1
2
3
4
5
6
7
8
9
import car

my_beetle = car.Car('volkswagen', 'beetle', 2019)

print(my_beetle.get_descriptive_name())

my_tesla = car.ElectricCar('tesla', 'roadster', 2019)

print(my_tesla.get_descriptive_name())

کلّ ماژول car را وارد می‌کنیم. پس از آن از طریق دستور module_name.ClassName به کلاس‌های آن دسترسی پیدا می‌کنیم. دوباره یک Volkswagen Beetle و یک Tesla Roadster می‌سازیم.

وارد کردن تمام کلاس‌های یک ماژول

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

1
from module_name import *

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

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

وارد کردن یک ماژول در ماژول دیگر

گاهی ممکن است بخواهید که کلاس‌های خود را بین چند ماژول پخش کنید تا از طولانی شدن بیش از اندازه‌ی یک فایل و ذخیره شدن چند کلاس نامربوط در یک ماژول جلوگیری کنید. هنگام ذخیره کردن کلاس‌ها در چند ماژول ممکن است دریابید که کلاسی در یک ماژول به کلاسی در ماژول دیگری وابسته است. زمانی که چنین چیزی رخ دهد می‌توانید کلاس مورد نیاز را از ماژول اوّل وارد کنید.

برای مثال کلاس Car را در یک ماژول و کلاس‌های ElectricCar و Battery را در ماژولی جدا ذخیره می‌کنیم. ماژول جدیدی به نام electric_car.py می‌سازیم و آن را جایگزینِ فایل electric_car.py که پیش از این ساخته بودیم می‌کنیم و تنها دو کلاس Battery و ElectricCar را درون آن کپی می‌کنیم:

1
2
3
4
5
6
7
8
9
"""A set of classes that can be used to represent electric cars."""

from car import Car

class Battery:
    --snip--

class ElectricCar(Car):
    --snip--

کلاس ElectricCar نیاز به دسترسی به کلاس والد خود Car دارد، بنابراین Car را مستقیماً در ماژول وارد می‌کنیم. اگر این خط را فراموش کنیم، هنگام وارد کردن ماژول electric_car اعلام خطا می‌کند. همچنین باید ماژول Car را به‌روزرسانی کنیم تا تنها شامل کلاس Car باشد:

1
2
3
4
"""A class that can be used to represent a car."""

class Car:
    --snip--

اکنون می‌توانیم از هر ماژول جداگانه کلاس‌هایی را وارد کنیم و هر نوع خودرو که خواستیم بسازیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from car import Car

from electric_car import ElectricCar

my_beetle = Car('volkswagen', 'beetle', 2019)

print(my_beetle.get_descriptive_name())

my_tesla = ElectricCar('tesla', 'roadster', 2019)

print(my_tesla.get_descriptive_name())

Car را از ماژول خود و ElectricCar را از ماژول مربوطه‌اش وارد می‌کنیم. سپس یک خودروی معمولی و یک خودروی الکتریکی می‌سازیم. هر دو نوع خودرو به درستی ساخته شده‌اند:

1
2
3
2019 Volkswagen Beetle

2019 Tesla Roadster

استفاده از نام‌های مستعار

چنان‌که در فصل ۸ دیدید هنگام استفاده از ماژول‌ها نام‌های مستعار می‌توانند به مرتب‌سازی کد پروژه‌های شما کمک کنند. می‌توانید از نام‌های مستعار هنگام وارد کردن کلاس‌ها هم استفاده کنید.

به عنوان یک مثال برنامه‌ای را در نظر بگیرید که می‌خواهید در آن تعدادی خودروی الکتریکی بسازید. نوشتن و خواندن چندباره‌ی ElectricCar ممکن است خسته‌کننده باشد. شما می‌توانید در عبارت وارد کردن نام مستعاری به ElectricCar بدهید:

1
from electric_car import ElectricCar as EC

اکنون هر جا خواستید که یک خودروی الکتریکی بسازید، می‌توانید از این نام مستعار بهره بگیرید:

1
my_tesla = EC('tesla', 'roadster', 2019)

یافتن یک جریانِ کاری برای خود

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

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

خودآزمایی

۹-۱۰. رستوران‌های واردشده: از آخرین کلاس Restaurant خود استفاده کنید و آن را در یک ماژول ذخیره کنید. فایلی جدا بسازید که Restaurant را وارد می‌کند. نمونه‌ای از این کلاس بسازید و از متدهای آن استفاده کنید تا نشان دهید که عبارت import به درستی کار می‌کند.

۹-۱۱. کلاس مدیر واردشده: از کار خود در تمرین ۹-۸ شروع کنید. کلاس‌های User، Privileges و Admin را در یک ماژول ذخیره کنید. فایلی جدا بسازید، نمونه‌ای از Admin ایجاد کنید و show_privileges() را فراخوانی کنید تا معلوم شود همه چیز درست کار می‌کند.

۹-۱۲. چند ماژول: کلاس User را در یک ماژول و دو کلاس Privileges و Admin را در ماژولی دیگر ذخیره کنید. در فایلی جدا نمونه‌ای از Admin بسازید و show_privileges() را صدا کنید تا نشان دهید که همه چیز همچنان درست کار می‌کند.

1.9.5 - کتابخانه‌ی استاندارد پایتون و شکل‌دهی به کلاس‌ها

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

یکی از تابع‌های جالب در ماژول اعداد تصادفی randint() است. این تابع دو عدد صحیح را به عنوان نشان‌وند می‌گیرد و یک عدد صحیح تصادفی بین این دو عدد (و خود آن‌ها) برمی‌گرداند.

اینجا نحوه‌ی تولید عددی تصادفی بین ۱ و ۶ را می‌بینیم:

1
2
3
from random import randint

randint(1, 6)
1
3

تابع سودمند دیگر choice() است. این تابع یک لیست یا چندتایی را گرفته و یکی از اعضای آن را به صورت تصادفی برمی‌گرداند:

1
2
3
4
5
6
7
from random import choice

players = ['charles', 'martina', 'michael', 'florence', 'eli']

first_up = choice(players)

first_up
1
'florence'

ماژول random نباید برای ساختن برنامه‌های کاربردی با محوریّت امنیّت استفاده شود امّا برای پروژه‌های سرگرمی و جذّاب به اندازه‌ی کافی خوب است.

نکته
می‌توانید ماژول‌ها را از منابع خارجی هم بگیرید. چند نمونه از آن‌ها را در بخش دوم کتاب خواهید دید جایی که برای تکمیل هر پروژه به ماژول‌های خارجی نیاز دارید.

خودآزمایی

۹-۱۳. تاس: کلاسی به نام Die بسازید که مشخّصه‌ای به نام sides (وجه‌ها) دارد و مقدار اوّلیّه‌ی آن ۶ است. متدی به نام roll_die() بنویسید که عددی تصادفی بین ۱ و تعداد وجه‌های تاس چاپ می‌کند. تاسی شش‌وجهی بسازید و آن را ۱۰ بار بیندازید.

یک تاس ده‌وجهی و بیست‌وجهی بسازید و هر کدام را ۱۰ بار بیندازید.

۹-۱۴. بخت‌آزمایی: لیست یا چندتایی‌ای از ۱۰ عدد و ۵ حرف بسازید. چهار عدد یا حرف را به طور تصادفی از لیست انتخاب کنید و پیامی چاپ کنید که شماره‌ی بلیت هرکس به این چهار حرف یا عدد بخورد برنده‌ی جایزه‌ای شده است.

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

۹-۱۶. ماژول هفته‌ی پایتون: یکی از منابع عالی برای کاویدن کتابخانه‌ی استاندارد پایتون سایتی به نام Python Module of the Week است. به نشانی https://pymotw.com/ بروید و به فهرست آن نگاهی بیندازید. ماژولی را پیدا کنید که به نظرتان جالب می‌آید و درباره‌ی آن بخوانید، شاید بهتر باشد از ماژول random شروع کنید.

شکل‌دادن به کلاس‌ها

تعداد کمی از نکات مربوط به شکل‌دهی کلاس‌ها ارزش شفاف‌سازی دارند به ویژه زمانی که برنامه‌های شما به تدریج پیچیده‌تر می‌شوند.

نام کلاس‌ها باید به صورت CamelCase نوشته شوند. برای این کار اوّلین حرف هر کلمه را بزرگ کنید و از زیرخط‌ها استفاده نکنید. نام ماژول‌ها و نمونه‌ها باید با حروف کوچک و زیرخط بین واژگان نوشته شوند.

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

همچنین می‌توانید از خط‌های خالی برای مرتّب کردن کدهای خود استفاده کنید امّا از آن‌ها بیش از حد استفاده نکنید. در هر کلاس از یک خط خالی بین متدها و در هر ماژول از دو خط خالی برای جدا کردن کلاس‌ها استفاده کنید.

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

خلاصه

در این فصل آموختید که چگونه کلاس‌های خود را بنویسید. یاد گرفتید که چگونه به کمک مشخّصه‌ها اطّلاعاتی را در کلاس‌ها ذخیره کنید و چگونه با نوشتن متدها رفتاری را که می‌خواهید به کلاس‌های خود بدهید. چگونه نوشتن متدهای __init__() را آموختید که دقیقاً با مشخّصه‌هایی که می‌خواهید، نمونه‌هایی از کلاس‌های شما می‌سازند. چگونگی تغییر دادن مشخّصه‌ها را به طور مستقیم و از طریق متدها دیدید. آموختید که ارث‌بری می‌تواند ساختن کلاس‌هایی را که به هم مربوط‌اند ساده کند و یاد گرفتید که چگونه از نمونه‌های یک کلاس به عنوان مشخّصه‌هایی در کلاس دیگر استفاده کنید تا هر کلاس را ساده نگه دارید.

دیدید که چگونه ذخیره کردن کلاس‌ها در ماژول‌ها و وارد کردن آن‌ها در فایل‌ها (هر جا که مورد نیاز بودند) پروژه‌های شما را مرتّب نگه می‌دارد. آموختن درباره‌ی کتابخانه‌ی استاندارد پایتون را آغاز کردید و مثالی بر پایه‌ی ماژول random مشاهده کردید. در نهایت شکل‌دهی به کلاس‌های خود را بر اساس قراردادهای پایتون آموختید.

در فصل ۱۰ کار کردن با فایل‌ها را خواهید آموخت تا بتوانید کارهایی را که در یک برنامه کرده‌اید یا به کاربران اجازه‌ی انجام داده‌اید، ذخیره کنید و هم‌چنین در مورد استثناها خواهید آموخت که کلاسی خاص در پایتون است که به شما در واکنش دادن به خطاهایی که رخ می‌دهند کمک می‌کند.


1.10 - فایل‌ها و استثناها

1.10.1 - خواندن یک فایل

حال که بر مهارت‌های مقدّماتیِ مورد نیاز برای نوشتنِ برنامه‌های سازمان‌یافته و با کاربریِ آسان تسلّط یافته‌اید، زمانِ فکر کردن به این موضوع رسیده است که چگونه برنامه‌هایتان را از این هم مناسب‌تر و قابل استفاده‌تر کنید. در این فصل کار کردن با فایل‌ها را خواهید آموخت تا برنامه‌هایتان بتوانند حجم زیادی از داده‌ها را به سرعت تحلیل کنند. مدیریّتِ خطاها را نیز می‌آموزید تا برنامه‌ها هنگام برخورد با شرایط پیش‌بینی‌نشده از کار نیفتند. درباره‌ی استثناها خواهید آموخت: شیءهای ویژه‌ای که پایتون هنگام اجرای برنامه می‌سازد تا خطاهای رخ‌داده را مدیریّت کند. همچنین درباره‌ی ماژول json خواهید آموخت که به کمک آن می‌توانید اطّلاعات کاربر را ذخیره کنید تا با توقّف برنامه از بین نروند.

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

حجم بسیار زیادی داده در فایل‌های متنی موجود است. فایل‌های متنی ممکن است شامل داده‌های هواشناسی، ترافیکی، اقتصادی-اجتماعی، ادبیّات و … باشند. خواندن داده‌ها از یک فایل به ویژه در کاربردهای مربوط به تحلیلِ داده سودمند است، امّا در بسیاری از وضعیّت‌ها که در آن‌ها به تحلیل یا تغییرِ اطّلاعاتِ ذخیره‌شده در یک فایل نیاز داشته باشید هم کاربردی است. برای مثال می‌توانید برنامه‌ای بنویسید که محتویّات یک فایل متنی را می‌خواند و آن را به شکلی بازنویسی می‌کند که در یک مرورگر قابل نمایش باشد.

اوّلین گام برای کار کردن با اطّلاعاتِ داخلِ یک فایل خواندنِ آن و انتقال آن به حافظه است. می‌توانید کلّ یک فایل را یک‌جا بخوانید یا این که هر بار با یک خطّ آن کار کنید.

خواندن کامل یک فایل

برای آغاز کردن به فایلی نیاز داریم که چند خطّ متن داخلِ آن باشد. بیایید از فایلی شروع کنیم که در خود عدد پی را تا ۳۰ رقم اعشار دارد و هر ده رقم اعشار در یک خط قرار گرفته است:

1
2
3
3.1415926535
  8979323846
  2643383279

برای اجرا کردن مثال‌های پیشِ رو می‌توانید این خط‌ها را در یک ویرایشگر وارد کنید و آن را با نام pi_digits.txt ذخیره کنید یا این فایل را از منابع این کتاب به آدرس https://nostarch.com/pythoncrashcourse2e دریافت کنید. فایل را در همان پوشه‌ای ذخیره کنید که برنامه‌های این فصل را نگه می‌دارید.

این برنامه‌ای است که این فایل را باز می‌کند، آن را می‌خواند و محتویّات آن را روی صفحه چاپ می‌کند:

1
2
3
with open('pi_digits.txt') as file_object:
    contents = file_object.read()
    print(contents)

خطّ اوّل این برنامه حرف بسیاری برای گفتن دارد. با بررسی تابع open() شروع کنیم. برای کار کردن با یک فایل، حتّی برای چاپ کردن محتویّات آن، باید ابتدا آن را باز کنید تا به آن دسترسی داشته باشید. تابع open() تنها یک نشان‌وند می‌پذیرد: نام فایلی که می‌خواهید باز کنید. پایتون در پوشه‌ای به دنبال این فایل می‌گردد که برنامه‌ی در حالِ اجرای فعلی در آن ذخیره شده است. در این مثال file_reader.py در حال اجراست پس پایتون در پوشه‌ای که این فایل ذخیره شده به دنبال فایل pi_digits.txt می‌گردد. تابع open() شیءای را برمی‌گرداند که نماینده‌ی این فایل است. در اینجا open('pi_digits.txt') شیءای را برمی‌گرداند که نماینده‌ی pi_digits.txt باشد. پایتون این شیء را به file_object نسبت می‌دهد که جلوتر در این برنامه با آن کار خواهیم کرد.

کلیدواژه‌ی with بلافاصله بعد از این که دیگر نیازی به دسترسی به فایل نباشد، آن را می‌بندد. به این نکته دقّت کنید که در این برنامه open() را صدا کرده‌ایم امّا close() را نه. می‌توانستید فایل را با فراخوانی open() و close() باز و بسته کنید، امّا اگر اشکالی در برنامه‌ی شما مانع اجرا شدنِ متد close() شود، ممکن است فایل هیچ‌گاه بسته نشود. ممکن است بدیهی به نظر بیاید ولی بستنِ نامناسبِ فایل‌ها می‌تواند موجبِ از بین رفتن یا آسیب دیدن اطّلاعات شود. و اگر close() را زودتر از موعد فرابخوانید، در جایی از برنامه خود را در حال کار کردن با یک فایل بسته خواهید یافت که خود موجب خطاهای بیشتر خواهد شد. فهمیدنِ زمانِ دقیقِ بستنِ یک فایل ممکن است همیشه ساده نباشد امّا با ساختاری که اینجا نشان داده شده پایتون خود این کار را برای شما خواهد کرد. تمامِ کاری که باید بکنید این است که فایل را باز کرده و کاری که می‌خواهید با آن انجام دهید و مطمئن باشید که پایتون هر گاه که کار اجرای قطعه‌ی with به پایان رسید فایل را به طور خودکار خواهد بست.

پس از آن‌که شیء فایلیِ نماینده‌ی pi_digits.txt در اختیار بود، برنامه با استفاده از متد read() در خطّ دوم، تمام محتویّات آن را خوانده و در یک رشته‌ی بلند به نام contents ذخیره می‌کند. زمانی که مقدار contents را چاپ می‌کنیم و تمام فایل متنی را دریافت می‌کنیم:

1
2
3
3.1415926535
  8979323846
  2643383279

تنها تفاوت بین خروجی و فایلِ اصلی خطّ خالی اضافه‌ای در آخر خروجی است. این خطّ خالی به این دلیل پدیدار شده که read() هنگام رسیدن به پایان فایل یک رشته‌ی خالی برمی‌گرداند؛ این رشته‌ی خالی به صورت یک خطّ خالی نمایش داده می‌شود. اگر بخواهید این خطّ خالی را حذف کنید می‌توانید هنگام فراخوانی print()، rstrip() را به کار بگیرید:

1
2
3
with open('pi_digits.txt') as file_object:
    contents = file_object.read()
    print(contents.rstrip())

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

1
2
3
3.1415926535
  8979323846
  2643383279

1.10.2 - مسیر فایل‌ها

زمانی که نام فایل ساده‌ای مانند pi_digits.txt را به تابع open() می‌دهید پایتون در پوشه‌ای به دنبال آن می‌گردد که فایلِ در حالِ اجرای فعلی در آن قرار دارد. (که همان فایلِ .py برنامه‌ی شماست.)

بسته به نحوه‌ی سازماندهی کار شما، گاهی فایلی که به90 دنبال باز کردنِ آن هستید در پوشه‌ی فایل برنامه‌ی شما وجود ندارد. برای مثال ممکن است شما فایل برنامه‌ی خود را در پوشه‌ای به نام python_work ذخیره کنید و در این پوشه، پوشه‌ی دیگری به نام text_files بسازید تا بین فایل‌های برنامه و فایل‌های متنی تمایز قایل شوید. اگرچه text_files داخل python_work قرار دارد امّا اگر تنها نام فایلی را که در text_files قرار دارد، در open() وارد کنیم، کارگر نخواهد بود چون پایتون تنها در python_work جست‌و‌جو می‌کند و در همان‌جا متوقّف می‌شود؛ و داخلِ text_files را نمی‌گردد. برای این که پایتون در پوشه‌ای به جز پوشه‌ی محلّ ذخیره‌سازی فایل برنامه به دنبال فایلی بگردد باید یک مسیر 91 فایل ارائه کنید که به پایتون بگوید یک محلّ خاص را در سیستم شما نگاه کند.

چون text_files داخل python_work قرار دارد می‌توانید برای باز کردن فایلی در داخل آن، از یک مسیر فایل نسبی استفاده کنید. مسیر فایل نسبی به پایتون می‌گوید که محلّ واردشده را نسبت به پوشه‌ای در نظر بگیر که محلِّ ذخیره‌سازی فایلِ برنامه‌ی در حالِ اجرای فعلی است و داخل آن را ببیند. برای مثال می‌توانید به این صورت بنویسید:

1
with open('text_files/filename.txt') as file_object:

این خط از پایتون می‌خواهد که در پوشه‌ی text_files به دنبال فایل .txt مطلوب بگردد و فرض کند که text_files داخل python_work قرار دارد.

نکته
سیستم‌های ویندوزی برای نمایش مسیر 92 فایل‌ها به جای اسلش (/) از بک‌اسلش (\) استفاده می‌کند، با این حال می‌توانید در کد خود همچنان از اسلش استفاده کنید.

علاوه بر این، می‌توانید صرف نظر از جایی که برنامه‌ی فعلی ذخیره شده، به پایتون محلّ دقیق فایل را بدهید. به این مسیر فایل مطلق می‌گویند. زمانی که مسیر نسبی به کار نیامد می‌توانید از مسیر مطلق استفاده کنید. برای مثال اگر text_files را در پوشه‌ای به جز python_work قرار دهید (مثلاً پوشه‌ای به نام other_files)، دادنِ مسیری مانند 'text_files/filename.txt' به open()، کار نخواهد کرد زیرا پایتون تنها داخل python_work دنبال آن خواهد گشت. اینجا نیاز دارید که مسیر کامل را بنویسید تا برای پایتون شفّاف باشد که کجا را بگردد.

مسیرهای مطلق معمولاً از مسیرهای نسبی طولانی‌ترند پس خوب است که آن‌ها را به متغیّری نسبت دهیم و سپس آن متغیّر را به open() بدهیم:

1
2
3
file_path = '/home/ehmatthes/other_files/text_files/filename.txt'

with open(file_path) as file_object:

با به‌کارگیری مسیرهای مطلق می‌توانید فایل‌ها را از هر جایی در سیستم خود بخوانید. فعلاً ساده‌ترین کار ذخیره کردنِ فایل‌ها در همان پوشه‌ی فایل‌های برنامه است؛ یا این که آن‌ها را در پوشه‌ای مانند text_files در همان پوشه‌ی فایل‌های برنامه ذخیره کنید.

نکته
اگر در یک مسیر فایل از بک‌اسلش استفاده کنید به خطا می‌خو93 رید چون بک‌اسلش در رشته‌ها در کاراکترهای گریز استفاده می‌شود. برای مثال در مسیرِ "C:\path\to\file.txt" دنباله‌ی \t به عنوان تب تفسیر می‌شود. اگر به به‌کارگیری بک‌اسلش‌ها نیاز داشتید باید خود آن‌ها را با کاراکتر گریز به کار ببرید: "C:\\path\\to\\file.txt"

1.10.3 - خواندن خط به خط

اغلب هنگامِ خواندنِ یک فایل نیاز دارید هر خطّ آن را بررسی کنید. شاید به دنبال اطّلاعات خاصّی در فایل باشید یا بخواهید متن داخل آن را به شکلی تغییر دهید. برای مثال ممکن است بخواهید فایلی از داده‌های هواشناسی را بخوانید و با خط‌هایی که در توضیح آب و هوای آن روز عبارت sunny (آفتابی) داشته باشد کاری انجام دهید. در گزارش اخبار ممکن است به دنبال خط‌هایی با برچسب <headline> باشید و به آن خط شکل خاصّی بدهید.

برای بررسی خط به خط یک فایل می‌توانید با حلقه‌ی for در شیء فایل حلقه بزنید:

1
2
3
4
5
filename = 'pi_digits.txt'

with open(filename) as file_object:
    for line in file_object:
        print(line)

در ۱ نام فایلی را که در حال خواندن آن هستیم به متغیّر filename نسبت می‌دهیم. این رویّه‌ای مرسوم برای کار کردن با فایل‌هاست. چون متغیّر filename بیانگر یک فایل واقعی نیست—فقط رشته‌ای است که به پایتون جای فایل را نشان می‌دهد—می‌توانید 'pi_digits.txt' را با نام فایل دیگری جابه‌جا کنید که می‌خواهید با آن کار کنید. پس از فراخوانی open() شیءای که بیانگر فایل است و محتویّات آن را به متغیّر file_object نسبت می‌دهیم (۲). دوباره از دستور with استفاده می‌کنیم تا به پایتون اجازه دهیم فایل را به طور مناسب باز و بسته کند. برای بررسی محتویّات فایل، در شیء فایل حلقه می‌زنیم و با هر خطّ فایل کار می‌کنیم (۳).

وقتی هر خط را چاپ می‌کنیم، به فاصله‌های خالی بیشتری بین خط‌ها برمی‌خوریم:

1
2
3
4
5
3.1415926535

  8979323846

  2643383279

این خط‌های خالی به این دلیل پدیدار می‌شوند که در فایل متنی در پایان هر خط یک کاراکتر خطّ جدید پنهان وجود دارد. تابع print هم که پس از هر بار فراخوانی یک خطّ جدید اضافه می‌کند، در نتیجه در پایان هر خط در مجموع به دو خطّ خالی برمی‌خوریم: یکی از فایل و یکی هم به خاطر print(). هنگام فراخوانی print() با به‌کارگیری rstrip() برای هر خط این خط‌های خالی اضافی حذف می‌شوند:

1
2
3
4
5
filename = 'pi_digits.txt'

with open(filename) as file_object:
    for line in file_object:
        print(line.rstrip())

اکنون بار دیگر خروجی مشابه محتویّات فایل می‌شود.

ساختن لیستی از خط‌های یک فایل

زمانی که از with استفاده می‌کنید شیء فایلی که open() برمی‌گرداند تنها داخل همان بخش with() در دسترس است که آن را در برگرفته. اگر بخواهید دسترسی به محتویّات فایل را پس از قسمت with هم حفظ کنید، می‌توانید در داخل بخش with خط‌های فایل را در لیستی ذخیره کنید و بعداً با آن لیست کار کنید. می‌توانید بخش‌هایی از فایل را بلافاصله پس از خواندن پردازش کنید و بخشی دیگر از پردازش را به زمان دیگری موکول کنید.

مثال زیر خط‌های pi_digits.txt را در داخل بخش with در یک لیست ذخیره می‌کند و سپس خارج از آن، خط‌ها را چاپ می‌کند:

1
2
3
4
5
6
7
filename = 'pi_digits.txt'

with open(filename) as file_object:
    lines = file_object.readlines()

for line in lines:
    print(line.rstrip())

در ۱ متد readlines() هر خطّ فایل را در یک لیست ذخیره می‌کند. سپس این لیست به lines نسبت داده می‌شود که می‌توانیم پس از پایان بخش with هم با آن کار کنیم. در ۲ از یک حلقه‌ی for ساده برای چاپ کردن هر خط در lines استفاده می‌کنیم. از آن‌جایی‌که هر عضو lines متناظر با یک خطّ فایل است، خروجی عیناً مشابه محتویّات فایل خواهد بود.

1.10.4 - کار کردن با محتویّات یک فایل

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
filename = 'pi_digits.txt'

with open(filename) as file_object:
    lines = file_object.readlines()

pi_string = ''
for line in lines:
    pi_string += line.rstrip()

print(pi_string)
print(len(pi_string))

مشابه کاری که در مثال قبل کردیم، فایل را باز می‌کنیم و هر خط از رقم‌ها را در یک لیست ذخیره می‌کنیم. در ۱ متغیّر pi_string را می‌سازیم تا رقم‌های pi را دربربگیرد. سپس حلقه‌ای ایجاد می‌کنیم که هر خطّ از رقم‌ها را به pi_string افزوده و کاراکتر خطّ جدید را از هر خط حذف می‌کند (۲). در ۳ رشته را چاپ می‌کنیم و طول آن را نمایش می‌دهیم:

1
2
3
3.1415926535    8979323846    2643383279

36

متغیّر pi_string شامل فاصله‌هایی است که در سمت چپ رقم‌های هر خط وجود دارد، امّا می‌توانیم با به‌کارگیری strip() به جای rstrip() از شرّ آن‌ها خلاص شویم:

1
2
3
4
5
6
7
--snip--

for line in lines:
    pi_string += line.strip()

print(pi_string)
print(len(pi_string))

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

1
2
3
3.141592653589793238462643383279

32
نکته
زمانی که پایتون فایلی متنی را می‌خواند تمام متن آن را به عنوان رشته در نظر می‌گیرد. اگر مقداری عددی را خوانده‌اید و می‌خواهید با آن محاسبات عددی انجام دهید، باید آن را به کمک تابع int() به عدد صحیح یا به کمک تابع float() به عدد اعشاری تبدیل کنید.

فایل‌های بزرگ: یک میلیون رقم اعشار

تا اینجا بر روی تحلیل فایلی متنی تمرکز کرده‌ایم که تنها سه خط را شامل می‌شد امّا کد این مثال‌ها می‌توانند با فایل‌های بسیار بزرگ‌تر هم به خوبی کار کنند. اگر با فایلی متنی شروع کنیم که به جای ۳۰ رقم، عدد پی را تا یک میلیون رقم اعشار داشته باشد، می‌توانیم رشته‌ای بسازیم که تمام این رقم‌ها را دربربگیرد. نیاز به ایجاد هیچ تغییری در برنامه نیست به جز این که نام فایل ورودی را عوض کنیم. ما همچنین فقط ۵۰ رقم اوّل را چاپ می‌کنیم تا نیاز نباشد یک میلیون رقم را در ترمینال ببینیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
filename = 'pi_million_digits.txt'

with open(filename) as file_object:
    lines = file_object.readlines()

pi_string = ''
for line in lines:
    pi_string += line.strip()

print(f"{pi_string[:52]}...")
print(len(pi_string))

خروجی نشان می‌دهد که رشته‌ای از عدد پی تا یک میلیون رقم اعشار داریم:

1
2
3
3.14159265358979323846264338327950288419716939937510...

1000002

پایتون هیچ نوع محدودیّت ذاتی برای حجم داده‌ای که می‌خواهید با آن کار کنید، ندارد؛ می‌توانید با هر چه‌قدر داده که حافظه‌ی سیستم شما می‌تواند در خود جا دهد کار کنید.

نکته
برای اجرا کردن این برنامه (و بسیاری از مثال‌هایی که از این پس می‌آیند) باید منابع موجود در https://nostarch.com/pythoncrashcourse2e را دریافت کنید.

آیا تاریخ تولّد شما در عدد پی وجود دارد؟

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--snip--

for line in lines:
    pi_string += line.strip()

birthday = input("Enter your birthday, in the form mmddyy: ")

if birthday in pi_string:
    print("Your birthday appears in the first million digits of pi!")
else:
    print("Your birthday does not appear in the first million digits of pi.")

در ۱ از کاربر می‌خواهیم که تاریخ تولّد خود را وارد کند و در ۲ بررسی می‌کنیم که آیا آن رشته در pi_string وجود دارد یا نه. آن را امتحان می‌کنیم:

1
2
3
Enter your birthdate, in the form mmddyy: 120372

Your birthday appears in the first million digits of pi!

روز تولّد من در رقم‌های اعشار پی پیدا شد! پس از این که فایلی را خواندید می‌توانید محتویّات آن را به هر شکلی که تصوّر می‌کنیم تحلیل و بررسی کنید.

1.10.5 - خودآزمایی

۱۰-۱- یادگیریِ پایتون: فایلی خالی در ویرایشگر متنی خود باز کنید و در چند خط آن‌چه تا اینجا در مورد پایتون آموخته‌اید خلاصه کنید. هر خط را به این شکل آغاز کنید: In Python you can… فایل را با نام learning_python.txt در همان پوشه‌ای ذخیره کنید که تمرین‌های این فصل را در آن قرار می‌دهید. برنامه‌ای بنویسید که فایل را بخواند و آن‌چه را نوشته‌اید سه بار چاپ کند. محتوّیات را یک بار با خواندن کامل فایل، یک بار با حلقه زدن بر شیء فایل و یک بار با ذخیره کردن خط‌های آن در یک لیست و سپس کار کردن با آن بیرون از بخش with، چاپ کنید.

۱۰-۲- یادگیری C: می‌توانید به کمک متد replace() هر کلمه‌ای را در یک رشته با کلمه‌ای دیگر جایگزین کنید. اینجا مثالی سریع درباره‌ی جایگزینی 'dog' با 'cat' در یک جمله می‌بینید:

1
2
3
>>> message = "I really like dogs."
>>> message.replace('dog', 'cat')
'I really like cats.'

فایلی که هم‌اکنون ساخته‌اید، learning_python.txt، خط به خط بخوانید و کلمه‌ی Python را با نام زبان دیگری، مثلاً C، جایگزین کنید. هر خطِ تغییریافته را روی صفحه چاپ کنید.

1.10.6 - نوشتن در یک فایل

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

نوشتن در یک فایل خالی

برای نوشتن در یک فایل باید open() را با نشان‌وند دومی فراخوانی کنید که به پایتون می‌گوید می‌خواهید در آن فایل چیزی بنویسید. برای این که ببینیم این کار چگونه انجام می‌شود، پیام ساده‌ای می‌نویسیم و به جای نمایش آن بر روی صفحه، آن را در یک فایل ذخیره می‌کنیم:

1
2
3
4
filename = 'programming.txt'

with open(filename, 'w') as file_object:
    file_object.write("I love programming.")

در این مثال فراخوانی open() در ۱ با دو نشان‌وند انجام می‌شود. اوّلین نشان‌وند همچنان نام فایلی95 94 است که می‌خواهیم باز کنیم. دومین نشان‌وند 'w'، به پایتون می‌گوید که می‌خواهیم فایل را در حالت نوشتن باز کنید. شما می‌توانید فایلی را در حالت خواندن ('r')، حالت نوشتن ('w')، حالت الحاق ('a') و یا حالتی که به شما اجازه‌ی خواندن و نوشتن فایل می‌دهد ('r+') باز کنید. اگر نشان‌وند مربوط به حالت خواندن را حذف کنید، پایتون به طور پیش‌فرض فایل را در حالت فقط-خواندنی باز می‌کند.

اگر فایلی که می‌خواهید در آن چیزی بنویسید وجود نداشته باشد، تابع open() آن را به طور خودکار برای شما می‌سازد. با این حال هنگام باز کردن فایل در حالت ('w') دقّت کنید؛ چون اگر قبلاً فایلی به این نام وجود داشته باشد، پایتون پیش از بازگرداندن شیء فایل، تمام محتویّات آن را پاک می‌کند.

در ۲ از متد write() بر روی شیء فایل استفاده می‌کنیم تا رشته‌ای در آن بنویسیم. این برنامه هیچ خروجی‌ای ندارد امّا اگر فایل programming.txt را باز کنید در آن یک خط می‌بینید:

1
I love programming.

این فایل مانند هر فایل دیگری در رایانه‌ی شما رفتار می‌کند. می‌توانید آن را باز کنید، متن جدیدی در آن بنویسید، از آن چیزی کپی کنید، به آن چیزی اضافه کنید و … .

نکته
پایتون تنها می‌تواند در یک فایل متنی رشته‌ها را بنویسد. اگر می‌خواهید داده‌های عددی را در یک فایل متنی ذخیره کنید باید ابتدا آن را به کمک تابع str() به شکل متنی دربیاورید.

نوشتن چند خط

تابع write() هیچ خطّ جدیدی به متن شما اضافه نمی‌کند. بنابراین اگر بدون افزودن کاراکتر خطّ اضافه بیشتر از یک خط بنویسید، ممکن است فایل شما آن‌جور که انتظار دارید به نظر نیاید:

1
2
3
4
5
filename = 'programming.txt'

with open(filename, 'w') as file_object:
    file_object.write("I love programming.")
    file_object.write("I love creating new games.")

افزودن خط‌های جدید هنگام فراخوانی write() هر رشته را در خط خود جای می‌دهد:

1
2
3
4
5
filename = 'programming.txt'

with open(filename, 'w') as file_object:
    file_object.write("I love programming.\n")
    file_object.write("I love creating new games.\n")

حال در خروجی خط‌های جداگانه می‌بینیم:

1
2
I love programming.
I love creating new games.

شما می‌توانید از فاصله، کاراکترِ تب یا خط‌های خالی برای شکل دادن به خروجی خود استفاده کنید همانگونه که در خروجی‌های خود در ترمینال انجام می‌دادید.

اضافه کردن به یک فایل

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

حال write_message.py را با اضافه کردن چند دلیل برای دوست‌داشتن برنامه‌نویسی به فایلِ موجودِ programming.txt تغییر می‌دهیم:

1
2
3
4
5
filename = 'programming.txt'

with open(filename, 'a') as file_object:
    file_object.write("I also love finding meaning in large datasets.\n")
    file_object.write("I love creating apps that can run in a browser.\n")

در ۱ از نشان‌وند 'a' برای باز کردن فایل استفاده می‌کنیم تا به جای این که محتویّات آن را رونویسی کنیم، به آن چیزی اضافه کنیم. در ۲ دو خطّ جدید می‌نویسیم که به programming.txt اضافه می‌شوند:

1
2
3
4
I love programming.
I love creating new games.
I also love finding meaning in large datasets.
I love creating apps that can run in a browser.

در نهایت به همان محتویّات اصلی فایل می‌رسیم که دو خطّ جدید به آن اضافه شده است.

1.10.7 - خودآزمایی

۱۰-۳- مهمان‌ها: برنامه‌ای بنویسید که از کاربران نام آن‌ها را بپرسد. زمانی که پاسخ دادند، نام آن‌ها در فایلی به نام guest.txt بنویسید.

۱۰-۴- کتابچه‌ی مهمان‌ها: یک حلقه‌ی while بنویسید که از کاربران نامشان را بپرسد. زمانی که نام خود را وارد کردند، پیام سلامی خطاب به آن‌ها روی صفحه چاپ کنید و در فایلی به نام guest_book.txt خطی برای ثبت کردنِ بازدیدِ آن‌ها اضافه کنید. مطمئن شوید که در فایل هر عنوان ورودی در یک خط جدید ذخیره می‌شود.

۱۰-۵- نظرسنجی برنامه‌نویسی: یک حلقه‌ی while بنویسید که از افراد بپرسد چرا برنامه‌نویسی را دوست دارند. هر بار که کسی دلیل وارد کرد، آن را به فایلی که تمام پاسخ‌ها را نگه می‌دارد، اضافه کنید.

1.10.8 - استثناها

پایتون از شیءی خاص به نام *استثنا * برای مدیریّت خطاهایی که حین اجرای برنامه رخ می‌دهند استفاده می‌کند. هرگاه پس از رخ دادن خطایی، پایتون مطمئن نباشد که چه باید بکند، یک شیء استثنا می‌سازد. اگر کدی برای رسیدگی کردن به استثنا بنویسید برنامه به کار خود ادامه خواهد داد. اگر استثنا را مدیریّت نکنید برنامه متوقّف شده و یک traceback نمایش داده می‌شود که حاوی گزارشی در مورد استثنایی است که رخ داده است.

رسیدگی به استثناها با بلوک‌های try-except انجام می‌پذیرد. بلوک try-except از پایتون می‌خواهد که کاری انجام دهد و امّا در عین حال مشخّص می‌کند که در صورت رخ دادن استثنا حین انجام آن کار چه کاری بکند. زمانی که از بلوک‌های try-except استفاده می‌کنید برنامه‌های شما حتی در صورت به اشتباه افتادن به کار خود ادامه می‌دهند. کاربران به جای traceback که ممکن است خواندن آن برای آن‌ها گیج‌کننده باشد، پیام‌هایی مناسب می‌بینند که شما خود نوشته‌اید.

رسیدگی به استثنای ZeroDivisionError

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

1
print(5/0)

البته که پایتون نمی‌تواند چنین کاری کند بنابراین به یک traceback برمی‌خوریم:

1
2
3
4
Traceback (most recent call last):
  File "division_calculator.py", line 1, in <module>
    print(5/0)
ZeroDivisionError: division by zero

خطایی که در traceback نشان داده شده، ZeroDivisionError، یک شیء استثنا است. پایتون چنین شیء‌ای را در واکنش به وضعیّتی می‌سازد که در آن نمی‌تواند کاری را که از آن خواسته‌ایم انجام دهد. زمانی که چنین چیزی رخ می‌دهد، پایتون برنامه را متوقّف کرده و به ما می‌گوید چه نوع استثنایی رخ داده است. ما می‌توانیم از این اطّلاعات برای تغییر دادن و اصلاح برنامه‌ی خود استفاده کنیم. به پایتون می‌گوییم زمانی که چنین استثنایی رخ داد چه کاری انجام بده؛ با این کار اگر چنین موقعیّتی دوباره پیش بیاید ما آمادگی داریم.

استفاده از بلوک‌های try-except

زمانی که فکر می‌کنید ممکن است خطایی رخ دهد، می‌توانید یک بلوک try-except بنویسید تا به استثنایی که احتمالاً رخ می‌دهد رسیدگی کنید. از پایتون می‌خواهید که کدی را اجرا کند و به آن می‌گویید در صورتی که اجرای کد به وقوع نوع خاصّی از استثنا انجامید، چه کاری انجام دهد.

اینجا نشان می‌دهیم که بلوک try-except برای رسیدگی به استثنای ZeroDivisionError چه شکلی است:

1
2
3
4
try:
    print(5/0)
except ZeroDivisionError:
    print("You can't divide by zero!")

خطّی را که موجب بروز خطا می‌شود، print(5/0)، در بلوک try قرار می‌دهیم. اگر کد داخل try کار کند، پایتون از روی بلوک except می‌پرد. اگر کد داخل بلوک try منجر به خطا شود، پایتون به دنبال بلوک except‌ای می‌گردد که استثنای آن با استثنای رخ‌داده منطبق باشد و کد داخل آن را اجرا می‌کند.

در این مثال کد داخل try خطای ZeroDivisionError را تولید می‌کند بنابراین پایتون به دنبال بلوک except‌ای می‌گردد که به او بگوید چه باید بکند. پایتون سپس کد داخل آن بلوک را اجرا می‌کند و کاربر به جای traceback یک پیام خطای دوستانه می‌بیند:

1
You can't divide by zero!

اگر پس از بلوک try-except کد دیگری نوشته شده باشد، اجرای برنامه ادامه می‌یابد زیرا به پایتون گفته‌ایم که چگونه خطا را مدیریّت کند.

1.10.9 - به‌کارگیری استثناها برای پیش‌گیری از از کار افتادن برنامه

مدیریّت خطاها به ویژه زمانی مهم است که برنامه پس از رخ دادن خطا کارِ زیادی برای انجام دادن داشته باشد. این وضع اغلب در برنامه‌هایی رخ می‌دهد که از کاربر ورودی می‌گیرند. اگر برنامه به ورودی‌های نامناسب به درستی پاسخ دهد، به جای از کار افتادن می‌تواند به ورودی‌های درستِ بیشتری رسیدگی کند.

بیایید ماشین حساب ساده‌ای بسازیم که تنها عمل تقسیم را انجام می‌دهد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
print("Give me two numbers, and I'll divide them.")
print("Enter 'q' to quit.")

while True:
    first_number = input("\nFirst number: ")
    if first_number == 'q':
        break
    second_number = input("Second number: ")
    if second_number == 'q':
        break
    answer = int(first_number) / int(second_number)
    print(answer)

این برنامه از کاربر می‌خواهد تا first_number را وارد کند (۱) و اگر برای خروج q را فشار نداد، second_number را هم وارد کند (۲). سپس این دو عدد را بر هم تقسیم می‌کنیم تا answer به دست بیاید (۳). این برنامه هیچ کاری برای مدیریّت خطا انجام نمی‌دهد بنابراین درخواست تقسیم بر صفر موجب از کار افتادن برنامه می‌شود:

1
2
3
4
5
6
7
8
9
Give me two numbers, and I'll divide them.
Enter 'q' to quit.

First number: 5
Second number: 0
Traceback (most recent call last):
  File "division_calculator.py", line 9, in <module>
    answer = int(first_number) / int(second_number)
ZeroDivisionError: division by zero

این که برنامه از کار افتاده بد است؛ و علاوه بر آن نشان دادنِ traceback به کاربر هم فکر خوبی نیست. زیرا کاربران غیر فنّی را گیج می‌کند و در صورتی که کسی به دنبال کارهای مخرّب باشد، به کمکِ آن ممکن است بیش از آن چیزی که شما بخواهید در مورد برنامه‌یتان اطّلاعات کسب کند. مثلاً نام برنامه‌ی شما را می‌فهمد یا می‌بیند که کجای برنامه‌ی شما به درستی کار نمی‌کند. یک مهاجم کارآزموده گاهی می‌تواند از این دانسته‌ها استفاده کند تا نوعِ حمله علیهِ کد شما را تعیین کند.

بلوک else

می‌توانیم با قرار دادن خطّی که ممکن است ایجاد خطا کند داخل یک بلوک try این برنامه را در برابر خطا مقاوم‌تر کنیم. خطا در خطّی رخ می‌دهد که تقسیم در آن انجام می‌گیرد بنابراین آن‌جا جایی است که در بلوک try-except قرار می‌دهیم. این مثال همچنین شامل یک بلوک else است. هر کدی که اجرای آن به اجرای موفّق بلوک try وابسته است در بلوک else قرار می‌گیرد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
--snip--

while True:
    --snip--

    if second_number == 'q':
        break

    try:
        answer = int(first_number) / int(second_number)
    except ZeroDivisionError:
        print("You can't divide by 0!")
    else:
        print(answer)

از پایتون می‌خواهیم که تلاش خود را برای به انجام رساندن عملیّات تقسیم در یک بلوک try بکند (۱). این بلوک تنها شامل کدی است که ممکن است موجب بروز خطا شود. هر کدی که به اجرای موفقیّت‌آمیز بلوک try وابسته باشد به بلوک else افزوده می‌شود. در این مورد اگر عملیّات تقسیم موفّق باشد از بلوک else برای چاپ کردن نتیجه در ۳ استفاده می‌کنیم.

بلوک except به پایتون می‌گوید که هنگام رخ دادن ZeroDivisionError چه پاسخی بدهد (۲). اگر بلوک try به دلیل خطای تقسیم بر صفر با موفقیّت انجام نشد، پیامی دوستانه چاپ می‌کنیم که به کاربر بگوید چگونه از این خطا پرهیز کند. اجرای برنامه ادامه می‌یابد و کاربر هرگز traceback‌ای نمی‌بیند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Give me two numbers, and I'll divide them.
Enter 'q' to quit.

First number: 5
Second number: 0
You can't divide by 0!

First number: 5
Second number: 2
2.5

First number: q

بلوک try-except-else این گونه کار می‌کند: پایتون تلاش می‌کند که کد داخل بلوک try را اجرا کند. تنها کدی باید در این بلوک قرار بگیرد که ممکن است موجب بروز استثنا شود. گاهی شما کد دیگری دارید که باید تنها در صورت موفقیّت‌آمیز بودن اجرای بلوک try اجرا شود؛ این کد در بلوک else قرار می‌گیرد. بلوک except به پایتون می‌گوید که در صورت وقوع استثنای خاصّی در اجرای کد بلوک try، چه کاری انجام دهد.

با پیش‌بینی منابع احتمالی خطا می‌توانید برنامه‌های مقاومی بنویسید که حتّی در صورت برخوردن به داده‌های نامعتبر یا پیدا نشدنِ منابع، به کار خود ادامه دهند. کد شما در برابر خطاهای غیرعمدی کاربران یا حمله‌های مخرّب مقاومت خواهد کرد.

1.10.10 - رسیدگی به استثنای FileNotFoundError

یکی از مشکلات شایع هنگام کار کردن با فایل‌ها مدیریّت فایل‌های ناموجود است. فایلی که به دنبال آن هستید ممکن است در جای متفاوتی باشد، نام آن ممکن است اشتباه نوشته شده باشد یا چه بسا اصلاً وجود نداشته باشد. شما می‌توانید به کمک یک بلوک try-except و با روشی سرراست به تمام این وضعیّت‌ها رسیدگی کنید.

حال بیایید تلاش کنیم فایلی را که وجود ندارد بخوانیم. برنامه‌ی زیر می‌خواهد محتویّات آلیس در سرزمین عجایب را بخواند امّا من فایل alice.txt را در پوشه‌ی alice.py ذخیره نکرده‌ام:

1
2
3
4
filename = 'alice.txt'

with open(filename, encoding='utf-8') as f:
    contents = f.read()

اینجا دو تغییر وجود دارد. یکی به‌کارگیری متغیّر f برای نمایش شیء فایل است که قراردادی مرسوم است. دیگری استفاده از نشان‌وند encoding است. این نشان‌وند هنگامی مورد نیاز است که کدگذاری پیش‌فرض سیست 97 م شما با کدگذاری فایلی که می‌خواهید بخوانید هم‌خوانی ندارد.

پایتون نمی‌تواند فایلی را که وجود ندارد بخواند پس استثنایی ایجاد می‌کند:

1
2
3
4
Traceback (most recent call last):
  File "alice.py", line 3, in <module>
    with open(filename, encoding='utf-8') as f:
FileNotFoundError: [Errno 2] No such file or directory: 'alice.txt'

آخرین خط traceback خطای FileNotFoundError را گزارش می‌کند: زمانی که پایتون فایلی را که تلاش می‌کند بخواند، پیدا نمی‌کند این استثنا را تولید می‌کند.

در این مثال تابع open() ایجاد خطا می‌کند بنابراین برای رسیدگی به آن، بلوک try از خطّی شروع می‌شود که شامل open() است:

1
2
3
4
5
6
7
filename = 'alice.txt'

try:
    with open(filename, encoding='utf-8') as f:
        contents = f.read()
except FileNotFoundError:
    print(f"Sorry, the file {filename} does not exist.")

در این مثال کد داخل بلوک try ایجاد FileNotFoundError می‌کند پس پایتون به دنبال بلوک except‌ای می‌گردد که به این خطا بخورد. سپس کد داخل آن بلوک را اجرا می‌کند که به جای یک traceback به صدور پیام دوستانه‌ای منجر می‌شود:

1
Sorry, the file alice.txt does not exist.

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

تحلیل متن

شما می‌توانید فایل‌های متنی شامل کتاب‌های کامل را98 تحلیل کنید. بخش زیادی از ادبیّات کلاسیک در قالب فایل‌های متنی ساده در دسترس هستند چون در قلمرو عمومی هستند. متن‌های به کار رفته در این بخش از پروژه‌ی گوتنبرگ گرفته شده‌اند (http://gutenberg.org) پروژه‌ی گوتنبرگ مجموعه‌ای از کارهای ادبی را نگه‌داری می‌کند که در قلمرو عمومی در دسترس‌اند و اگر در پروژه‌های برنامه‌نویسی خود به کار با متن‌های ادبی علاقه‌مندید، منبع بسیار خوبی خواهد بود.

اجازه دهید متن آلیس در سرزمین عجایب را بیرون بکشیم تا تعداد کلمات متن آن را بشمریم. از متد رشته‌ای split() استفاده می‌کنیم که می‌تواند رشته را به لیستی از کلمات تبدیل کند. اینجا می‌بینیم که split() با رشته‌ای که شامل عنوان متن یعنی "Alice in Wonderland" است چه می‌کند:

1
2
3
>>> title = "Alice in Wonderland"
>>> title.split()
['Alice', 'in', 'Wonderland']

متد split() هر جا در رشته به فاصله‌ی خالی می‌رسد، آن بخش را جدا کرده و تمام تکّه‌ها را در لیستی ذخیره می‌کند. نتیجه لیستی از کلماتِ رشته است، هرچند ممکن است در برخی از کلمات علائم سجاوندی هم پیدا شود. برای شمارش کلمات آلیس در سرزمین عجایب از split() بر روی کل متن استفاده می‌کنیم. سپس تنها اعضای لیست را می‌شماریم تا به عددی سرانگشتی از تعداد کلمات موجود در متن برسیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
filename = 'alice.txt'

try:
    with open(filename, encoding='utf-8') as f:
        contents = f.read()
except FileNotFoundError:
    print(f"Sorry, the file {filename} does not exist.")
else:
    # Count the approximate number of words in the file.
    words = contents.split()
    num_words = len(words)
    print(f"The file {filename} has about {num_words} words.")

فایل alice.txt را به پوشه‌ی درست منتقل کردم بنابراین بلوک try این بار درست کار خواهد کرد. در ۱ رشته‌ی contents را که اکنون کل متن آلیس در سرزمین عجایب را به صورت یک رشته‌ی بلند در بر دارد، می‌گیریم و به کمک متد split() لیستی از تمام کلمات داخل کتاب درست می‌کنیم. زمانی که از len() برای بررسی طول آن استفاده می‌کنیم تقریب خوبی از تعداد کلمات رشته‌ی اصلی به دست می‌آوریم (۲). در ۳ عبارتی چاپ می‌کنیم که تعداد کلمات یافت‌شده در فایل را گزارش می‌کند. این کد داخل بلوک else قرار گرفته زیرا تنها در صورتی کار خواهند کرد که بلوک try با موفقیّت اجرا شود. خروجی به ما می‌گوید که تعداد کلمات موجود در alice.txt چندتاست:

1
The file alice.txt has about 29465 words.

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

1.10.11 - کار کردن با چند فایل

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def count_words(filename):
    """Count the approximate number of words in a file."""
    try:
        with open(filename, encoding='utf-8') as f:
            contents = f.read()
    except FileNotFoundError:
        print(f"Sorry, the file {filename} does not exist.")
    else:
        words = contents.split()
        num_words = len(words)
        print(f"The file {filename} has about {num_words} words.")

filename = 'alice.txt'
count_words(filename)

بیشتر کد دست‌نخورده است. صرفاً آن را با تورفتگی داخل بدنه‌ی count_words() قرار داده‌ایم. به‌روز نگه‌داشتنِ توضیحات هنگام تغییر دادن برنامه عادت خوبی است بنابراین توضیح را به یک docstring تبدیل کرده و کمی آن را تغییر داده‌ایم (۱).

حال حلقه‌ی ساده‌ای می‌نویسیم که تعداد کلمات هر متن99 ی را محاسبه کند. این کار را با ذخیره کردنِ نام فایل‌ها در یک لیست و سپس فراخوانی count_words() برای هر فایل در لیست انجام می‌دهیم. سعی می‌کنیم تعداد کلمات آلیس در سرزمین عجایب، سیذارتا، mobی دیک و زنان کوچک را که همگی در قلمرو عمومی هستند، تحلیل کنیم. من به عمد فایل siddhartha.txt را از پوشه‌ی برنامه‌ی word_count.py بیرون گذاشته‌ام تا ببینم برنامه چگونه فقدان فایل را مدیریّت می‌کند:

1
2
3
4
5
6
7
def count_words(filename):
    --snip--

filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt', 'little_women.txt']

for filename in filenames:
    count_words(filename)

فقدانِ فایل siddhartha.txt تاثیری بر اجرای باقی برنامه ندارد:

1
2
3
4
The file alice.txt has about 29465 words.
Sorry, the file siddhartha.txt does not exist.
The file moby_dick.txt has about 215830 words.
The file little_women.txt has about 189079 words.

به‌کارگیریِ بلوک try-except در این مثال دو مزیّت مهم دارد. مانع این می‌شویم که کاربر traceback‌ای ببیند و باعث می‌شویم برنامه به تحلیل متن‌هایی که می‌تواند پیدا کند ادامه دهد. اگر مانع خطای FileNotFoundError را که Siddhartha.txt موجب آن شده بود، نمی‌شدیم کاربر یک traceback کامل را مشاهده می‌کرد و برنامه پس از تلاش برای تحلیل سیذارتا متوقّف می‌شد. در نتیجه هیچ‌گاه mobی‌دیک و زنان کوچک را بررسی نمی‌کرد.

رد کردنِ بی‌سروصدای یک خطا

در مثال پیشین به کاربران خود اطّلاع دادیم که یکی از فایل‌ها موجود نیست. امّا نیازی نیست هر استثنایی را که رخ می‌دهد گزارش کنید. گاهی شما می‌خواهید که مشکلات برنامه بدون هیچ سروصدایی رد شوند درست مثل این که هیچ اتّفاقی نیفتاده است. برای رسیدن به این مقصود یک بلوک try مانند همیشه می‌نویسید امّا به پایتون به طور صریح می‌گویید که در بلوک except هیچ کاری نکند. دستوری به نام pass به پایتون می‌گوید که هیچ کاری انجام ندهد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def count_words(filename):
    """Count the approximate number of words in a file."""
    try:
        --snip--
    except FileNotFoundError:
        pass
    else:
        --snip--

filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt', 'little_women.txt']

for filename in filenames:
    count_words(filename)

تنها تفاوت این برنامه با قبلی عبارت pass در ۱ است. حال وقتی FileNotFoundError رخ می‌دهد کد داخل بلوک except اجرا می‌شود امّا هیچ اتّفاقی نمی‌افتد. هیچ traceback‌ای و هیچ خروجی‌ای در پاسخ به خطای رخ‌داده تولید نمی‌شود. کاربران تعداد کلمات هر فایل موجود را می‌بینند امّا در مورد وجود نداشتنِ یک فایل هیچ علامتی نمی‌بینند:

1
2
3
The file alice.txt has about 29465 words.
The file moby_dick.txt has about 215830 words.
The file little_women.txt has about 189079 words.

دستور pass همچنین به عنوان یک جای‌بان هم عمل می‌کند. به شما یادآوری می‌کند که شما تصمیم گرفته‌اید در جایی از برنامه هیچ کاری نکنید و این که ممکن است بعداً بخواهید در آن نقطه کاری انجام دهید. برای مثال در این برنامه ممکن است تصمیم بگیرید که نام فایل‌های یافت‌نشده را در فایلی به نام missing_files.txt بنویسید. کاربران ما این فایل را نمی‌بینند ولی ما قادر هستیم که آن را بخوانیم و در مورد متن‌های پیدانشده کاری بکنیم.

کدام خطا را گزارش کنیم؟

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

برنامه‌ای که به خوبی نوشته و به درستی آزموده شده خیلی در معرضِ خطاهای داخلی مانند خطاهای نحوی یا منطقی نیست. امّا هر بار برنامه‌ی شما به چیزی خارج از خود وابسته باشد، مانند ورودیِ کاربر، وجودِ یک فایل یا ارتباط شبکه، امکان رخ دادنِ یک استثنا وجود دارد. کمی تجربه به شما کمک می‌کند که بدانید کی از بلوک‌های مدیریّت استثنا استفاده کنید و به کاربر در مورد خطاهایی که رخ می‌دهد چه‌قدر اطّلاعات بدهید.

1.10.12 - خودآزمایی

۱۰-۶- جمع کردن: یکی از مشکلات رایج هنگام دریافت مقادیر عددی زمانی رخ می‌دهد که افراد به جای عدد متن وارد می‌کنند. وقتی تلاش می‌کنیم که این ورودی را با int به عدد تبدیل کنیم خطای ValueError دریافت می‌کنید. برنامه‌ای بنویسید که از کاربر دو عدد بگیرد. آن‌ها را جمع و نتیجه را چاپ کند. اگر هر کدام از ورودی‌ها عدد نبودند خطای ValueError را بگیرید و پیامی دوستانه چاپ کنید. برنامه‌ی خود را با وارد کردن دو عدد و سپس متنی به جای یکی از اعداد بیازمایید.

۱۰-۷- ماشین جمع کردن: کد تمرین ۱۰-۶ را در یک حلقه‌ی while قرار دهید تا حتّی اگر کاربر یک بار به اشتباه به جای عدد متن وارد کرد، همچنان بتواند عددهای بعدی را وارد کند.

۱۰-۸- سگ‌ها و گربه‌ها: دو فایل به نام cats.txt و dogs.txt بسازید. در اوّلی دستِ کم نام سه گربه و در دیگری نام سه سگ را ذخیره کنید. برنامه‌ای بنویسید که سعی کند این فایل‌ها را بخواند و محتویّات آن‌ها را روی صفحه چاپ کند. کد خود را در یک بلوک try-except قرار دهید تا خطای FileNotFoundError را بگیرد و در صورت فقدان فایلی پیامی چاپ کند. یکی از فایل‌ها را به مکان دیگری در سیستم خود منتقل کنید و مطمئن شوید که بلوک except به درستی اجرا می‌شود.

۱۰-۹- سگ‌ها و گربه‌های بی‌صدا: بلوک except را در تمرین ۱۰-۸ جوری تغییر دهید که در صورت یافت نشدنِ بدون سر و صدا آن را رد کند.

۱۰-۱۰- کلمات مشترک: پروژه‌ی گوتنبرگ (https://gutenberg.org) را ببینید و چند متن پیدا کنید که دوست دارید آن‌ها را تحلیل کنید. فایل‌های متنی آن‌ها را دانلود کنید یا متن خام را از مرورگر خود کپی و در یک فایل متنی در رایانه‌ی خود ذخیره کنید.

می‌توانید از متد count() استفاده کنید تا بفهمید یک کلمه یا عبارت چند بار در یک رشته تکرار شده است. برای مثال کد زیر تعداد دفعات پدیدار شدن 'row' را در یک رشته می‌شمارد:

1
2
3
4
5
>>> line = "Row, row, row your boat"
>>> line.count('row')
2
>>> line.lower().count('row')
3

توجّه کنید که تبدیل تمام رشته به حروف کوچک به کمک lower() تمام موارد وقوع یک کلمه را صرف نظر از شکل و بزرگ و کوچکی حروف آن پیدا می‌کند.

برنامه‌ای بنویسید که فایل‌هایی را که در پروژه‌ی گوتنبرگ پیدا کرده‌اید بخواند و تعیین کند که کلمه‌ی 'the' چند بار در هر کدام تکرار شده است. این عدد تقریبی خواهند بود زیرا کلمه‌هایی مانند 'then' و 'there' هم شمرده خواهند شد. حال شمردن 'the ' را امتحان کنید، با افزودنِ یک فاصله در انتهای رشته، و ببینید که تعداد نسبت به قبل چه‌قدر کمتر می‌شود.

1.10.13 - ذخیره‌سازی داده‌ها

بسیاری از برنامه‌های شما از کاربران می‌خواهند که انواع خاصّی اطّلاعات را وارد کنند. ممکن است به کاربران امکان دهید که تنظیمات پیش‌فرض خود را در یک بازی ذخیره یا داده‌هایی را برای بصری‌سازی ارائه کنند. تمرکزِ برنامه‌ی شما بر هر چیزی باشد، شما اطّلاعاتی را که کاربر ارائه می‌کند در ساختمان‌های داده‌ای مانند لیست‌ها و دیکشنری‌ها ذخیره می‌کنید. زمانی که کاربران برنامه‌ای را می‌بندند تقریباً همیشه می‌خواهید که اطّلاعات وارد شده را ذخیره کنید. یک راه ساده برای این کار ذخیره کردن داده به کمک ماژول json است.

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

نکته
JSON در ابتدا برای جاوا‌اسکریپت توسعه داده شد. با این حال مدت‌هاست که به عنوان شکل فایلی مشترک بین زبان‌های بسیاری از جمله پایتون تبدیل شده است.

استفاده از json.dump() و json.load()

بیایید برنامه‌ای برای ذخیره کردن مجموعه‌ای عدد و برنامه‌ای دیگر برای خواندن دوباره‌ی آن عدد بنویسیم. برنامه‌ی اوّل از json.dump() برای ذخیره کردن مجموعه‌ی اعداد و برنامه‌ی دوم از json.load() استفاده می‌کند.

تابع json.dump() دو نشان‌وند می‌گیرد: قطعه‌ای داده و شیء فایلی که برای ذخیره کردن آن استفاده می‌شود. اینجا چگونگی استفاده از json.dump() را برای ذخیره کردن یک لیست اعداد می‌بینید:

1
2
3
4
5
6
7
8
import json

numbers = [2, 3, 5, 7, 11, 13]

filename = 'numbers.json'

with open(filename, 'w') as f:
    json.dump(numbers, f)

ابتدا ماژول json را وارد می‌کنیم و لیستی از اعداد می‌سازیم. در ۱ نام فایلی را که در آن لیست اعداد ذخیره می‌شود انتخاب می‌کنیم. برای نشان دادن این که داده‌ها در فایل به شکل JSON ذخیره می‌شوند، مرسوم است که از پسوند .json برای آن استفاده کنیم. سپس در ۲ فایل را در حالت نوشتن باز می‌کنیم تا به json اجازه بدهیم داده‌ها را در فایل بنویسد. در ۳ از تابع json.dump() برای ذخیره کردن لیست numbers در فایل numbers.json استفاده می‌کنیم.

این برنامه هیچ خروجی‌ای ندارد امّا اجازه دهید فایل numbers.json را باز کنیم و نگاهی به آن بیندازیم. داده به همان شکلی ذخیره شده که در پایتون دیده می‌شود:

1
[2, 3, 5, 7, 11, 13]

حال برنامه‌ای می‌نویسیم که از json.load() استفاده کرده، لیست را دوباره خوانده و به حافظه منتقل می‌کند:

1
2
3
4
5
6
7
8
import json

filename = 'numbers.json'

with open(filename) as f:
    numbers = json.load(f)

print(numbers)

در ۱ مطمئن می‌شویم که اطّلاعات را از همان فایلی می‌خوانیم که در آن ذخیره کرده‌ایم. این بار فایل را در حالت خواندن باز می‌کنیم چون پایتون تنها نیاز دارد که فایل را بخواند (۲). در ۳ از تابع json.load() برای بارگذاری اطّلاعات ذخیره‌شده در numbers.json استفاده می‌کنیم و آن را به متغیّر numbers نسبت می‌دهیم. در نهایت لیست اعداد بازیابی‌شده را چاپ می‌کنیم و می‌بینیم که همان لیست ساخته‌شده در number_writer.py است:

1
[2, 3, 5, 7, 11, 13]

این راهی ساده برای به اشتراک گذاشتن داده‌ها بین دو برنامه است.

1.10.14 - ذخیره کردن و خواندن داده‌هایی که کاربر تولید کرده

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

با ذخیره کردن نام کاربر شروع می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import json

username = input("What is your name? ")

filename = 'username.json'

with open(filename, 'w') as f:
    json.dump(username, f)

print(f"We'll remember you when you come back, {username}!")

در ۱ یک نام کاربری را برای ذخیره کردن می‌پرسیم. سپس از json.dump() استفاده می‌کنیم و نام کاربری و یک شیء فایل را به آن می‌دهیم تا نام کاربری را در یک فایل ذخیره کند (۲). سپس پیامی چاپ می‌کنیم که ذخیره شدنِ اطّلاعات را به کاربران اطّلاع می‌دهد (۳):

1
2
3
What is your name? Eric

We'll remember you when you come back, Eric!

اکنون برنامه‌ای جدید می‌نویسیم که به کاربری که نام او قبلاً ذخیره شده سلام می‌کند:

1
2
3
4
5
6
7
8
import json

filename = 'username.json'

with open(filename) as f:
    username = json.load(f)

print(f"Welcome back, {username}!")

در ۱ از json.load() برای خواندنِ اطّلاعات ذخیره شده در username.json استفاده و آن را به متغیّر username نسبت می‌دهیم. اکنون نام‌های کاربری را بازیابی کرده‌ایم و می‌توانیم به آن‌ها خوش‌آمد بگوییم (۲):

1
Welcome back, Eric!

ما به ترکیب این دو برنامه در یک فایل نیاز داریم. زمانی که کسی remember_me.py را اجرا می‌کند، می‌خواهیم در صورتِ امکان، نامِ کاربریِ او را از حافظه بازیابی کنیم؛ بنابراین با یک بلوک try شروع می‌کنیم که سعی می‌کند نام کاربری را بازیابی کند. اگر فایل username.json وجود نداشته باشد، در بلوک except نام کاربری را می‌پرسیم و برای دفعات بعد در username.json ذخیره می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import json

# Load the username, if it has been stored previously.
#   Otherwise, prompt for the username and store it.

filename = 'username.json'

try:
    with open(filename) as f:
        username = json.load(f)
except FileNotFoundError:
    username = input("What is your name? ")
    with open(filename, 'w') as f:
        json.dump(username, f)
    print(f"We'll remember you when you come back, {username}!")
else:
    print(f"Welcome back, {username}!")

هیچ کد جدیدی اینجا وجود ندارد؛ فقط بلوک‌های کد دو مثال قبلی را در یک فایل ترکیب کرده‌ایم. در ۱ تلاش می‌کنیم که فایل username.json را باز کنیم. اگر این فایل وجود داشته باشد، نام کاربری را از آن می‌خوانیم (۲) و پیام خوش‌آمدی خطاب به کاربر در بلوک else چاپ می‌کنیم. اگر این اوّلین باری باشد که کاربر برنامه را اجرا می‌کند، پس username.json وجود ندارد و خطای FileNotFoundError رخ خواهد داد (۳). پایتون به بلوک except می‌رود و در آن‌جا از کاربر نام کاربری‌اش را می‌پرسد (۴). سپس از json.dump() برای ذخیره کردن نام کاربری استفاده می‌کنیم و پیامی چاپ می‌کنیم (۵).

هر کدام از بلوک‌ها که اجرا شوند، نتیجه یک نام کاربری و یک پیام مناسب خواهد بود. اگر اوّلین اجرای برنامه باشد، خروجی این‌چنین خواهد بود:

1
2
3
What is your name? Eric

We'll remember you when you come back, Eric!

در غیر این صورت:

1
Welcome back, Eric!

این خروجی‌ای است که اگر برنامه قبلاً یک بار اجرا شده باشد خواهید دید.

1.10.15 - بازآرایی

اغلب شما به جایی می‌رسید که کدتان کار می‌کند امّا101 تشخیص می‌دهید که با شکستن کد به مجموعه‌ای از تابع‌ها با وظیفه‌ی ویژه، می‌توانید آن را بهبود دهید. این فرآیند را بازآرایی می‌نامند. بازآرایی کد شما را تمیز‌تر و خواندن و توسعه‌ی آن را ساده‌تر می‌کند.

ما می‌توانیم remember_me.py را با انتقال منطق آن به یک یا چند تابع بازآرایی کنیم. تمرکز remember_me.py بر تبریک گفتن به کاربر است بنابراین تمام کد موجود را به تابعی به نام greet_user() منتقل می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import json

def greet_user():
    """Greet the user by name."""
    filename = 'username.json'

    try:
        with open(filename) as f:
            username = json.load(f)
    except FileNotFoundError:
        username = input("What is your name? ")
        with open(filename, 'w') as f:
            json.dump(username, f)
        print(f"We'll remember you when you come back, {username}!")
    else:
        print(f"Welcome back, {username}!")

greet_user()

چون الان از یک تابع استفاده می‌کنیم، توضیحات را با یک docstring جایگزین می‌کنیم که توضیح می‌دهد برنامه در حال حاضر چگونه کار می‌کند (۱). این فایل کمی واضح‌تر است امّا تابع greet_user() کارهای دیگری جز سلام کردن به کاربر هم انجام می‌دهد—اگر نام کاربری قبلاً وجود داشته باشد آن را بازیابی می‌کند و اگر نه، برای وارد کردن یک نام کاربری جدید اقدام می‌کند.

بیایید greet_user() را بازآرایی کنیم تا این همه کار مختلف را انجام ندهد. کار را با انتقال کدی که نام کاربری ذخیره‌شده را بازیابی می‌کند شروع می‌کنیم و آن را به یک تابع جدا منتقل می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import json

def get_stored_username():
    """Get stored username if available."""
    filename = 'username.json'

    try:
        with open(filename) as f:
            username = json.load(f)
    except FileNotFoundError:
        return None
    else:
        return username

def greet_user():
    """Greet the user by name."""
    username = get_stored_username()

    if username:
        print(f"Welcome back, {username}!")
    else:
        username = input("What is your name? ")
        filename = 'username.json'
        with open(filename, 'w') as f:
            json.dump(username, f)
        print(f"We'll remember you when you come back, {username}!")

greet_user()

تابعِ جدیدِ get_stored_username() هدف واضحی دارد چنان‌چه در docstring در ۱ هم ذکر شده است. این تابع نام کاربری ذخیره‌شده را بازیابی می‌کند و در صورت پیدا شدن آن را برمی‌گرداند. اگر فایل username.json وجود نداشته باشد این تابع None برمی‌گرداند (۲). این تمرین خوبی است: تابع یا باید مقداری را که شما انتظار دارید یا در غیرِ این صورت None برگرداند. این به ما امکان می‌دهد که خروجی تابع را به سادگی بررسی کنیم. در ۳ اگر بازیابی نام کاربری موفّقیت‌آمیز بود پیامی به کاربر نمایش می‌دهیم و اگر نه، نام کاربری جدید را درخواست می‌کنیم.

باید یک بلوک دیگر کد را از greet_user() بیرون بکشیم. اگر نام کاربری وجود نداشته باشد، باید کدی که تقاضای نام کاربری جدید می‌کند به تابعی مخصوصِ این کار انتقال یابد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import json

def get_stored_username():
    """Get stored username if available."""
    --snip--

def get_new_username():
    """Prompt for a new username."""
    username = input("What is your name? ")
    filename = 'username.json'
    with open(filename, 'w') as f:
        json.dump(username, f)
    return username

def greet_user():
    """Greet the user by name."""
    username = get_stored_username()
    if username:
        print(f"Welcome back, {username}!")
    else:
        username = get_new_username()
        print(f"We'll remember you when you come back, {username}!")

greet_user()

در این نسخه‌ی نهایی remember_me.py هر تابع یک هدف مشخّص دارد. ما greet_user() را فراخوانی می‌کنیم و آن تابع پیامی مناسب چاپ می‌کند: یا به کاربری را که قبلاً وجود داشته خوش‌آمد می‌گوید یا به کاربر جدید سلام می‌کند. این تابع این کار را با فراخواندن get_stored_username() انجام می‌دهد که تنها مسئول بازیابی نام کاربری (به شرطِ وجود) است. سرانجام greet_user() تابع get_new_username() را در صورت لزوم صدا می‌کند که این تابع آخر فقط مسئول گرفتن نام کاربری جدید و ذخیره کردنِ آن است. بخش بخش کردنِ کار، قسمتِ مهمّی از نوشتن کدی شفّاف است که بتوان آن را به سادگی نگهداری کرد و توسعه داد.

1.10.16 - خودآزمایی

۱۰-۱۱- عدد دلخواه: برنامه‌ای بنویسید که عدد دلخواهِ کاربر را بپرسد. از json.dump() برای ذخیره کردن این عدد در یک فایل استفاده کنید. برنامه‌ای جدا بنویسید که این مقدار را بخواند و پیامی مانند این چاپ کند: "I know your favorite number! It's ____"

۱۰-۱۲- عددِ دلخواهِ به‌خاطرسپرده‌شده: دو برنامه‌ی تمرین ۱۰-۱۱ را در یک فایل ترکیب کنید. اگر عدد قبلاً وجود داشت، آن را به کاربر گزارش کنید. اگر نه، از کاربر عدد دلخواهش را بپرسید و در یک فایل ذخیره کنید. برنامه را دوبار اجرا کنید تا مطمئن شوید کار می‌کند.

۱۰-۱۳- تاییدِ کاربر: آخرین نسخه‌ی remember_me.py فرض می‌کند که یا کاربر قبلاً نام کاربری خود را وارد کرده یا اوّلین بار است که برنامه اجرا می‌شود. در صورتی‌که کاربر فعلی آخرین کسی نباشد که از برنامه استفاده کرده، باید برنامه را تغییر دهیم.

پیش از چاپ کردن پیام خوش‌آمدگویی در greet_user()، از کاربر بپرسید که آیا این نام کاربری درست است یا نه. اگر نبود get_new_username() را دوباره فراخوانی کنید تا نام کاربری تصحیح شود.

1.10.17 - خلاصه

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

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


1.11 - آزمودن کد

1.11.1 - آزمودن یک تابع

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

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

برای آموختنِ چگونگیِ آزمودنِ کد، به کدی برای آزمایش نیاز داریم. اینجا تابعی ساده وجود دارد که نام و نام خانوادگی را می‌گیرد و نام کامل را به شکلی منظّم برمی‌گرداند:

1
2
3
4
def get_formatted_name(first, last):
    """Generate a neatly formatted full name."""
    full_name = f"{first} {last}"
    return full_name.title()

تابع get_formatted_name() بین نام و نام خانوادگی یک فاصله قرار می‌دهد، آن‌ها را در قالبِ یک نام کامل ترکیب می‌کند و پس از بزرگ کردن حرف‌های اوّل هر کدام، نتیجه را برمی‌گرداند. برای بررسی این که get_formatted_name() کار می‌کند یا نه برنامه‌ای می‌نویسیم که از این تابع استفاده کند. برنامه‌ی names.py به کاربران امکان ورود نام و نام خانوادگی می‌دهد و سپس نام کامل را به شکلی مرتّب به آن‌ها نشان می‌دهد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from name_function import get_formatted_name

print("Enter 'q' at any time to quit.")

while True:
    first = input("\nPlease give me a first name: ")
    if first == 'q':
        break
    last = input("Please give me a last name: ")
    if last == 'q':
        break
    formatted_name = get_formatted_name(first, last)
    print(f"\tNeatly formatted name: {formatted_name}.")

این برنامه تابع get_formatted_name() را از name_function.py وارد می‌کند. کاربران می‌توانند دنباله‌ای از نام‌ها و نام‌های خانوادگی را وارد کنند و نام کامل تولیدشده را در خروجی ببینند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Enter 'q' at any time to quit.

Please give me a first name: janis
Please give me a last name: joplin
	Neatly formatted name: Janis Joplin.

Please give me a first name: bob
Please give me a last name: dylan
	Neatly formatted name: Bob Dylan.

Please give me a first name: q

می‌بینیم که نام‌های تولیدشده درست‌اند. حال فرض کنید که می‌خواهیم تابع get_formatted_name() را طوری تغییر دهیم که بتواند نام‌های میانی را هم مدیریّت کند. همچنان‌که این کار را انجام می‌دهیم می‌خواهیم مطمئن باشیم که نحوه‌ی عملکرد تابع هنگام کار کردن با نام‌هایی که تنها از نام و نام خانوادگی تشکیل شده، به هم نمی‌خورد. می‌توانیم هر بار که get_formatted_name() را تغییر دادیم، names.py را اجرا کنیم و با وارد کردن نامی مانند Janis Joplin آن را آزمایش کنیم، امّا این کار به تدریج تکراری می‌شود. خوشبختانه پایتون راهی موثّر برای خودکارسازی فرآیندِ آزمایشِ خروجیِ یک تابع ارائه می‌کند. اگر آزمودن get_formatted_name() را خودکار کنیم، می‌توانیم همیشه مطمئن باشیم که تابع ما با انواع نام‌هایی که برای آن‌ها آزمون نوشته‌ایم کار می‌کند.

آزمون 103 102 ‌های واحد و موارد آزمایشی

ماژول unittest در کتابخانه‌ی استاندارد پایتون ابزارهایی برای آزم}103 یش کد در اختیار شما قرار می‌دهد. آزمون واحد درستیِ یک جنبه از رفتار یک تابع را می‌سنجد. مورد آزمایشی مجموعه‌ای از آزمون‌های واحد است که در کنار هم ثابت می‌کنند تابع در بازه‌ی کاملی از وضعیّت‌هایی که باید مدیرّیت کند، آن‌گونه که انتظار می‌رود کار می‌کند. یک مورد آزمایشی خوب تمام انواع ورودی‌های ممکن یک تابع را در نظر می‌گیرد و آزمون‌هایی را برای نمایش هر کدام از آن‌ها دربرمی‌گیرد. مورد آزمایشی با پوشش کامل گستره‌ی کاملی از آزمون‌های واحد را شامل می‌شود که تمام راه‌های ممکن استفاده از یک تابع را پوشش می‌دهد. دست‌یافتن به پوشش کامل در یک پروژه بزرگ ممکن است ترسناک باشد. اغلب کافی است که آزمون‌هایی برای رفتارهای حیاتی کد بنویسیم و تنها زمانی به سراغ پوشش کامل برویم که پروژه برای کاربری بسیار گسترده آماده می‌شود.

1.11.2 - یک آزمون موفّق

کمی زمان می‌برد که به روش برپایی یک مورد آزمایشی عادت کنید امّا به محض راه انداختنِ آن، افزودن آزمون‌های واحد بیشتر برای تابع‌ها سرراست خواهد بود. به منظور نوشتن یک مورد آزمایشی برای تابعی، ماژول unittest و تابعِ مورد آزمایش را وارد کنید. سپس کلاسی بسازید که از unittest.TestCase ارث می‌برد و تعدادی متد بنویسید که جنبه‌های مختلف رفتار آن تابع را می‌آزماید.

این یک مورد آزمایشی با یک متد است که بررسی می‌کند آیا تابع get_formatted_name() با دریافت یک نام و نام خانوادگی به درستی کار می‌کند یا نه:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import unittest

from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
    """Tests for 'name_function.py'."""

    def test_first_last_name(self):
        """Do names like 'Janis Joplin' work?"""
        formatted_name = get_formatted_name('janis', 'joplin')
        self.assertEqual(formatted_name, 'Janis Joplin')

if __name__ == '__main__':
    unittest.main()

ابتدا unittest و تابعی را که می‌خواهیم آزمایش کنیم، get_formatted_name() وارد می‌کنیم. کلاسی به نام NamesTestCase می‌سازیم که تعدادی آزمایش واحد را برای get_formatted_name() شامل می‌شود. شما می‌توانید هر نامی برای این کلاس انتخاب کنید امّا بهتر است نامی بگذارید که به تابع تحت آزمایش مربوط باشد و از واژه‌ی Test در آن استفاده کنید. کلاس باید از کلاس unittest.TestCase ارث ببرد تا پایتون بداند چگونه آزمون‌هایی را که نوشته‌اید اجرا کند.

NamesTestCase متدی دارد که یک جنبه از عملکردِ get_formatted_name() را می‌آزماید. آن را test_first_last_name() می‌نامیم زیرا در آن تنها نام‌هایی را که از نام و نام خانوادگی تشکیل شده باشند، از منظر درستیِ شکل خروجی بررسی می‌کنیم. هر متدی که با test_ شروع شده باشد، هنگام اجرای test_name_function.py به طور خودکار اجرا خواهد شد. داخل این متد آزمون، تابعی را که قرار است آزمایش شود صدا می‌کنیم. در این مثال get_formatted_name() را با نشان‌دهنده‌های 'janis' و 'joplin' صدا می‌کنیم و نتیجه‌ی آن را به formatted_name نسبت می‌دهیم.

از یکی از سودمندترین ویژگی‌های unittest بهره می‌گیریم: متد اثبات. متدهای اثبات بررسی می‌کنند که آیا نتیجه‌ای که گرفتید یا آن‌چه انتظار دارید یکی است یا نه. در این مورد چون می‌دانیم که get_formatted_name() قرار است نام کاملی با فاصله و شکلِ مناسبِ حرف‌ها برگرداند، انتظار داریم که مقدار formatted_name برابر با Janis Joplin باشد. برای آزمودن درستیِ این مورد از متد assertEqual() در unittest استفاده کرده و formatted_name و 'Janis Joplin' را به آن می‌دهیم. خطِ

1
self.assertEqual(formatted_name, 'Janis Joplin')

می‌گوید: «مقدار formatted_name را به رشته‌ی 'Janis Joplin' مقایسه کن. اگر مطابقِ انتظار، برابر بودند، همه چیز درست است. وگرنه به من اطّلاع بده.»

این فایل را به طور مستقیم اجرا می‌کنیم امّا لازم ب106 ه ذکر است که بسیاری از ابزارها یا چهارچوب‌های آزمون، فایل‌های آزمونِ شما را پیش از اجرا وارد می‌کنند. مفسّر همزمان با وارد شدنِ چنین فایلی، آن را اجرا می‌کند. بلوک if به متغّیر ویژه‌ای، __name__ نگاه می‌کند. این متغّیر هنگام اجرای برنامه تعیین می‌شود. اگر این فایل به عنوان برنامه‌ی اصلی اجرا شود، مقدار __name__ برابر با '__main__' قرار داده می‌شود. در این مورد ما unittest.main() را صدا می‌کنیم که مورد آزمایشی را اجرا می‌کند. وقتی ابزارِ آزمون این فایل را وارد می‌کند مقدار __name__ برابر با '__main__' نخواهد بود و این بلوک اجرا نمی‌شود.

وقتی test_name_function.py را اجرا می‌کنیم به این خروجی می‌رسیم:

1
2
3
4
5
6
.

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

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

خروجی نشان می‌دهد که تابع get_formatted_name() همیشه برای نام‌هایی که از نام و نام خانوادگی تشکیل شده‌اند کار می‌کند؛ مگر این که تابع را عوض کنیم. اگر get_formatted_name() را تغییر دهیم می‌توانیم آزمون را دوباره اجرا کنیم. اگر آزمون موفقیّت‌آمیز بود می‌فهمیم که تابع همچنان برای نام‌هایی مانند Janis Joplin کار می‌کند.

1.11.3 - آزمون ناموفّق

یک آزمون ناموفّق چه شکلی است؟ بیایید get_formatted_name() را تغییر دهیم تا بتواند نام‌های میانی را هم مدیرّیت کند، امّا این کار طوری انجام دهیم که تابع در صورت وارد کردن نام و نام خانوادگی به تنهایی، مانند Janis Joplin، از کار بیفتد.

این نسخه‌ی جدید get_formatted_name() است که نام میانی را هم به عنوان نشان‌دهنده می‌پذیرد:

1
2
3
4
def get_formatted_name(first, middle, last):
    """Generate a neatly formatted full name."""
    full_name = f"{first} {middle} {last}"
    return full_name.title()

این نسخه برای افرادی که نام میانی دارند کار می‌کند امّا هنگام آزمودن آن متوجّه می‌شویم که کارکرد تابع با وارد کردنِ نام افرادی که فقط نام و نام خانوادگی دارند خراب می‌شود. این بار اجرا کردن test_name_function.py این خروجی را می‌دهد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
E
===================================================================
ERROR: test_first_last_name (__main__.NamesTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_name_function.py", line 8, in test_first_last_name
    formatted_name = get_formatted_name('janis', 'joplin')
TypeError: get_formatted_name() missing 1 required positional argument: 'last'
----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)

اینجا اطّلاعات زیادی وجود دارد زیرا هنگام رد شدن یک آزمون چیزهایی زیادی هست که باید بدانیم. اولّین مورد در خروجی، یک E تنهاست که نشان می‌دهد یک آزمون واحد در این مورد آزمایشی به خطا خورده است. پس از آن می‌بینیم که test_first_last_name() در NamesTestCase موجب خطا شده است. زمانی که آزمایش شما شامل چندین آزمون واحد است، دانستن این که کدام آزمون شکست خورده حیاتی است. یک traceback استاندارد می‌بینیم که می‌گوید فراخوانی تابع get_formatted_name('janis', 'joplin') دیگر کار نمی‌کند زیرا یک نشان‌وندِ مکان‌دارِ الزامی را ندارد. در نهایت می‌بینیم که یک آزمون واحد اجرا شده است. در نهایت پیامی مبنی بر شکست خوردن کلّی مورد آزمایشی می‌بینیم و این که هنگام اجرای آن یک خطا رخ داده است. این اطّلاعات در پایان خروجی پدیدار می‌شود بنابراین آن را بلافاصله می‌بینید بدون این که نیاز داشته باشید کل خروجی طولانی را بپیمایید تا بفهمید چند آزمون شکست خورده است.

پاسخ دادن به یک آزمون ناموفّق

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

در این مورد، get_formatted_name() در ابتدا تنها دو پارامتر داشت: نام و نام خانوادگی. اکنون نام، نام میانی و نام خانوادگی را نیاز دارد. افزودن پارامترِ اجباریِ نامِ میانی، رفتارِ مطلوب get_formatted_name() را مختل کرده است. اینجا بهترین گزینه اختیاری کردن نام میانی است. به محض انجام این کار آزمون ما برای نام‌هایی مانند Janis Joplin هم موفقیّت‌آمیز خواهد بود و می‌توانیم نام‌های میانی را هم بپذیریم. بیایید get_formatted_name() را طوری تغییر دهیم که در آن نام‌های میانی اختیاری باشند و سپس آزمون را دوباره اجرا کنیم. اگر موفّق باشد، در ادامه مطمئن می‌شویم که تابع، نام‌های میانی را هم به خوبی مدیریّت می‌کند.

برای اختیاری کردن نام‌های میانی پارامتر middle را به انتهای فهرست پارامترها منتقل کرده و به آن یک مقدار پیش‌فرض خالی می‌دهیم. همچنین یک آزمون if اضافه می‌کنیم که بسته به وجود داشتن یا نداشتن نام میانی، نام کامل را به درستی ایجاد می‌کند:

1
2
3
4
5
6
7
def get_formatted_name(first, last, middle=''):
    """Generate a neatly formatted full name."""
    if middle:
        full_name = f"{first} {middle} {last}"
    else:
        full_name = f"{first} {last}"
    return full_name.title()

در نسخه‌ی جدید get_formatted_name() نام میانی اختیاری است. اگر نام میانی به تابع داده شود نام کامل شامل نام، نام میانی و نام خانوادگی خواهد بود. در غیر این صورت نام کامل تنها از نام و نام خانوادگی درست خواهد شد. حال تابع باید با هر دو نوع اسم کار کند. برای دریافتنِ این که تابع همچنان با نام‌هایی مانند Janis Joplin کار می‌کند، test_name_function.py را دوباره اجرا کنید:

1
2
3
4
5
6
.

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

آزمون اکنون موفّق است. این حالت ایده‌آل است زیرا بدون آن که نیاز باشد تابع را به صورت دستی آزمایش کنیم، می‌فهمیم که تابع برای نام‌هایی مانند Janis Joplin هم کار می‌کند. اصلاحِ تابع به این دلیل ساده بود که شکست خوردن در آزمون به ما کمک کرد تا بفهمیم کد جدید رفتار پیشین را مختل کرده است.

1.11.4 - افزودن آزمون‌های جدید

حال که مطمئن شدیم get_formatted_name() با نام‌های ساده هم کار می‌کند، آزمون دیگری برای افرادی می‌نویسیم که نام میانی دارند. این کار را با اضافه کردن متدی دیگر به کلاس NamesTestCase انجام می‌دهیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class NamesTestCase(unittest.TestCase):
    """Tests for 'name_function.py'."""

    def test_first_last_name(self):
        --snip--

    def test_first_last_middle_name(self):
        """Do names like 'Wolfgang Amadeus Mozart' work?"""
        formatted_name = get_formatted_name(
            'wolfgang', 'mozart', 'amadeus')
        self.assertEqual(formatted_name, 'Wolfgang Amadeus Mozart')

if __name__ == '__main__':
    unittest.main()

نام این متد جدید را test_first_last_middle_name() می‌گذاریم. نام متد باید با test_ شروع شود تا هنگام اجرای test_name_function.py به طور خودکار اجرا شود. متد را طوری نام‌گذاری می‌کنیم که مشخّص باشد کدام رفتار get_formatted_name() را آزمایش می‌کنیم. در نتیجه اگر آزمون شکست بخورد بلافاصله می‌فهمیم که کدام نوع نام‌ها تحت تاثیر قرار گرفته‌اند. طولانی بودن نام متدها در کلاس‌های TestCase اشکالی ندارد. نام آن‌ها باید گویا باشد تا هنگام شکست خوردن آزمون خروجی برای شما معنادار باشد؛ به جز این، چون پایتون آن‌ها را به طور خودکار صدا می‌کند، هیچ‌گاه به نوشتن کدی برای صدا کردن آن‌ها نیاز نداریم.

برای آزمودن تابع get_formatted_name()، آن را با نام، نام میانی و نام خانوادگی فراخوانی می‌کنیم و پس از آن از assertEqual() استفاده می‌کنیم تا تطابق خروجی تابع و نام کاملی را که انتظار داریم، بررسی کنیم. زمانی که دوباره test_name_function.py را اجرا می‌کنیم هر دو آزمون قبول می‌شوند:

1
2
3
4
5
6
..

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

بسیار خوب! اکنون می‌دانیم که تابع همچنان با نام‌هایی مانند Janis Joplin کار می‌کند و علاوه بر آن مطمئنیم که نام‌هایی چون Wolfgang Amadeus Mozart هم در عملکرد آن خللی وارد نمی‌کنند.

خودآزمایی

۱۱-۱. شهر، کشور: تابعی بنویسید که دو پارامتر دریافت کند: نام یک شهر و نام یک کشور. این تابع باید رشته‌ای به شکل City, Country برگرداند، مانند Santiago, Chile. تابع را در ماژولی به نام city_functions.py ذخیره کنید.

فایلی به نام test_cities.py برای آزمودنِ تابعی که هم‌اکنون نوشته‌اید ایجاد کنید. (به خاطر بیاورید که باید unittest و تابع خود را وارد کنید.) متدی به نام test_city_country() بنویسید که فراخوانی تابع را با مقدارهایی مثل 'santiago' و 'chile' بیازماید تا بررسی کند که رشته‌ی صحیح را تولید می‌کند یا نه. test_cities.py را اجرا کنید و مطمئن شوید که test_city_country() موفّقیّت‌آمیز باشد.

۱۱-۲. جمعیّت: تابع را تغییر دهید تا پارامتر سومی را به نام population بپذیرد. این تابع الان باید رشته‌ای به شکل City, Country – population xxx برگرداند، مانند Santiago, Chile – population 5000000. فایل test_cities.py را دوباره اجرا کنید و مطمئن شوید که این بار شکست می‌خورد.

تابع را طوری تغییر دهید که پارامتر population در آن اختیاری باشد. فایل test_cities.py را دوباره اجرا کنید و مطمئن شوید که test_city_country() دوباره قبول می‌شود.

آزمون دومی به نام test_city_country_population() بنویسید که امکان فراخوانی تابع را با مقدارهای 'santiago'، 'chile' و population=5000000 درستی‌سنجی کند. فایل test_cities.py را دوباره اجرا کنید و مطمئن شوید که آزمون جدید موفّق است.

1.11.5 - آزمودن یک کلاس

در بخش یکم این فصل آزمون‌هایی برای یک تابع تکّی نوشتید. اکنون آزمون‌هایی برای یک کلاس می‌نویسید. شما در برنامه‌های خود از کلاس‌ها به دفعات استفاده می‌کنید بنابراین امکانِ ثابت کردنِ کارکردِ درستِ کلاس‌ها سودمند است. اگر آزمون‌های کلاسی که با آن کار می‌کنید موفّق باشد، می‌توانید مطمئن باشید توسعه دادن کلاس به طور ناگهانی رفتار فعلی آن را با مشکل رو‌به‌رو نمی‌کند.

گستره‌ای از متدهای اثبات

پایتون در کلاس unittest.TestCase چند متد اثبات ارائه می‌کند. چنان‌که پیش‌تر اشاره شد آزمون‌های اثبات بررسی می‌کنند که آیا شرطی که در نقطه‌ی خاصّی از برنامه به درستیِ آن اعتقاد دارید، واقعاً درست است یا نه. اگر شرط، مطابقِ انتظارِ شما درست باشد فرضیّه‌ی شما در باره‌ی رفتار آن بخش از برنامه تایید می‌شود پس می‌توانید مطمئن باشید که خطایی وجود ندارد. اگر شرطی که فرض می‌کنید درست است، نادرست باشد پایتون اعلام استثنا می‌کند.

جدول ۱۱-۱ شش متد اثبات متداول را شرح می‌دهد. با این متدها می‌توانید بررسی کنید که مقدارهای بازگشتی مساوی یا مخالف مقدارهای مورد انتظار است، یا مقدارها درست یا نادرست‌اند، یا در یک لیست وجود دارد (in) یا نه (not in). از این متدها تنها در کلاسی می‌توانید استفاده کنید که از unittest.TestCase ارث برده باشد، بنابراین اجازه دهید که در بستر آزمودن یک کلاس واقعی به نحوه‌ی کاربرد یکی از این متدها، نگاهی بیندازیم.

متد کاربرد
assertEqual(a, b) درستی‌سنجی a == b
assertNotEqual(a, b) درستی‌سنجی a != b
assertTrue(x) درستی‌سنجی این که x درست است
assertFalse(x) درستی‌سنجی این که x نادرست است
assertIn(item, list) درستی‌سنجی این که item در list هست یا نه
assertNotIn(item, list) درستی‌سنجی این که item در list نیست

کلاسی برای آزمودن

آزمودن یک کلاس مشابه آزمودن یک تابع است— بیشتر کار شما به آزمودن متدهای یک کلاس معطوف می‌شود. با این حال تفاوت‌های کوچکی نیز وجود دارد بنابراین بیاید کلاسی برای آزموده‌شدن بنویسیم. کلاسی را در نظر بگیرید که به مدیریّت نظرات ناشناس کمک می‌کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class AnonymousSurvey:
    """Collect anonymous answers to a survey question."""

    def __init__(self, question):
        """Store a question, and prepare to store responses."""
        self.question = question
        self.responses = []

    def show_question(self):
        """Show the survey question."""
        print(self.question)

    def store_response(self, new_response):
        """Store a single response to the survey."""
        self.responses.append(new_response)

    def show_results(self):
        """Show all the responses that have been given."""
        print("Survey results:")
        for response in self.responses:
            print(f"- {response}")

این کلاس با پرسشی که برای نظرسنجی ارائه می‌کنید، شروع می‌شود و شامل لیستی خالی برای ذخیره کردن پاسخ‌هاست. این کلاس متدهایی برای چاپ کردن سوال نظرسنجی، افزودن پاسخ جدید به لیست پاسخ‌ها و چاپ کردن تمام پاسخ‌های موجود در لیست دارد. برای ساختن نمونه‌ای از کلاس کافی است به آن پرسشی ارائه کنید. با وجود یک نمونه که بیانگر نظرسنجی خاصّی باشد، می‌توانید پرسش نظرسنجی را با show_question() نشان دهید، پاسخ را به کمک store_response() ذخیره کنید و نتایج را با show_results() نمایش دهید.

برای نمایش کارکرد درست AnonymousSurvey برنامه‌ای می‌نویسیم که از آن استفاده کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from survey import AnonymousSurvey

# Define a question, and make a survey.
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)

# Show the question, and store responses to the question.
my_survey.show_question()
print("Enter 'q' at any time to quit.\n")

while True:
    response = input("Language: ")
    if response == 'q':
        break
    my_survey.store_response(response)

# Show the survey results.
print("\nThank you to everyone who participated in the survey!")
my_survey.show_results()

این برنامه پرسشی طرح می‌کند ("What language did you first learn to speak?") و یک شیء AnonymousSurvey با آن پرسش می‌سازد. برنامه show_question() را برای نمایش پرسش فراخوانی و سپس پاسخ‌ها را دریافت می‌کند. هر کدام از پاسخ‌ها به محض دریافت ذخیره می‌شوند. پس از وارد شدنِ تمام پرسش‌ها (وقتی کاربر q را برای خروج وارد می‌کند)، متد show_results() نتایج نظرسنجی را چاپ می‌کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
What language did you first learn to speak?

Enter 'q' at any time to quit.

Language: English
Language: Spanish
Language: English
Language: Mandarin
Language: q

Thank you to everyone who participated in the survey!
Survey results:
- English
- Spanish
- English
- Mandarin

این کلاس برای نظرسنجی‌های ناشناسِ ساده کارآمد است. امّا فرض کنید می‌خواهید AnonymousSurvey و ماژول دربرگیرنده‌ی آن survey را توسعه دهیم. می‌توانیم به هر کاربر اجازه‌ی وارد کردن بیش از یک پاسخ بدهیم. می‌شود متدی نوشت که تنها پاسخ‌های یکتا را فهرست کند و گزارش کند که هر پاسخ چند بار تکرار شده است. می‌توانیم کلاس دیگری برای مدیریّت نظرسنجی‌های با نام بنویسیم.

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

آزمودن کلاس AnonymousSurvey

اکنون آزمونی می‌نویسیم که یک جنبه از رفتار AnonymousSurvey را درستی‌سنجی کند. آزمونی که می‌نویسیم بررسی می‌کند که آیا پاسخِ داده‌شده به پرسش نظرسنجی، به درستی ذخیره می‌شود یا نه. از متد assertIn() استفاده می‌کنیم تا پس از ذخیره کردن، وجود پاسخ را در لیستِ پاسخ‌ها بررسی کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import unittest

from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """Tests for the class AnonymousSurvey"""

    def test_store_single_response(self):
        """Test that a single response is stored properly."""
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        my_survey.store_response('English')
        self.assertIn('English', my_survey.responses)

if __name__ == '__main__':
    unittest.main()

از وارد کردنِ ماژول unittest و کلاس مورد آزمایش، AnonymousSurvey، شروع می‌کنیم. نام مورد آزمایشی را TestAnonymousSurvey می‌گذاریم که مجدّداً از unittest.TestCase ارث می‌برد. اوّلین متد آزمایش بررسی می‌کند که پاسخ ارائه‌شده به پرسش نظرسنجی، پس از ذخیره شدن، در لیست پاسخ‌های نظرسنجی می‌آید یا نه. یک نام گویای خوب برای چنین متدی test_store_single_response() است. اگر این آزمون شکست بخورد از نام متدی که در خروجی آزمون می‌آید می‌فهمیم که مشکل در ذخیره کردن پاسخ‌های تکّی نظرسنجی بوده است.

برای آزمودن رفتار کلاس باید نمونه‌ای از آن بسازیم. نمونه‌ای به نام my_survey با پرسش "What language did you first learn to speak?" می‌سازیم. یک پاسخ تکّی، English، را به کمک متد store_response() ذخیره می‌کنیم. پس از آن با اثبات وجود English در لیست my_survey.responses، ذخیره شدن درست پاسخ را درستی‌سنجی می‌کنیم.

اجرای آزمون test_survey.py همراه با موفّقیّت خواهد بود:

1
2
3
4
5
6
.

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

این خوب است امّا نظرسنجی زمانی سودمند است که بیش از یک پاسخ تولید کند. اکنون بررسی می‌کنیم که آیا سه پاسخ هم به درستی ذخیره می‌شود یا نه. برای چنین کاری متدی دیگر به TestAnonymousSurvey اضافه می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import unittest

from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """Tests for the class AnonymousSurvey"""

    def test_store_single_response(self):
        --snip--

    def test_store_three_responses(self):
        """Test that three individual responses are stored properly."""
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        responses = ['English', 'Spanish', 'Mandarin']
        for response in responses:
            my_survey.store_response(response)
        for response in responses:
            self.assertIn(response, my_survey.responses)

if __name__ == '__main__':
    unittest.main()

متد جدید test_store_three_responses() را صدا می‌کنیم. یک شیء نظرسنجی مانند آن‌چه در test_store_single_response() دیدیم ایجاد می‌کنیم. لیستی شامل سه پاسخ مختلف درست می‌کنیم، سپس برای هر کدام store_response() را فرامی‌خوانیم. به محض ذخیره‌شدن پاسخ‌ها حلقه‌ی دیگری می‌نویسیم که وجود هر پاسخ را در my_survey.responses تایید کند.

حال test_survey.py را دوباره اجرا می‌کنیم و هر دو آزمون (برای پاسخ تکّی و پاسخ سه‌تایی) قبول می‌شود:

1
2
3
4
5
6
..

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

این آزمون بسیار عالی کار می‌کند. با این حال این آزمون‌ها کمی تکراری‌اند بنابراین از ویژگی دیگر unittest برای کاراتر کردن آن‌ها استفاده می‌کنیم.

1.11.6 - متد setUp()

در test_survey.py در هر متدِ آزمون، یک نمونه از AnonymousSurvey و مجموعه‌ای از پاسخ‌های جدید را در هر متد ساختیم. کلاس unittest.TestCase متدی به نام setUp() دارد که به شما مکان می‌دهد این اشیا را یک بار بسازید و سپس از آن‌ها در هر کدام از متدهای آزمون استفاده کنید. زمانی که متد setUp() در یک کلاس TestCase می‌آورید، پایتون آن را پیش از اجرای هر متد آزمونی که با test_ شروع می‌شود، اجرا می‌کند. هر شیء‌ای پس از ساخته‌شدن در متد setUp()، در تمام متدهای آزمون در دسترس خواهد بود.

بیایید از setUp() برای ساختن یک نمونه‌ی نظرسنجی و مجموعه‌ای از پاسخ‌ها استفاده کنیم تا بتوانیم از آن‌ها در test_store_single_response() و test_store_three_responses() استفاده کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import unittest

from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """Tests for the class AnonymousSurvey."""

    def setUp(self):
        """Create a survey and a set of responses for use in all test methods."""
        question = "What language did you first learn to speak?"
        self.my_survey = AnonymousSurvey(question)
        self.responses = ['English', 'Spanish', 'Mandarin']

    def test_store_single_response(self):
        """Test that a single response is stored properly."""
        self.my_survey.store_response(self.responses[0])
        self.assertIn(self.responses[0], self.my_survey.responses)

    def test_store_three_responses(self):
        """Test that three individual responses are stored properly."""
        for response in self.responses:
            self.my_survey.store_response(response)
        for response in self.responses:
            self.assertIn(response, self.my_survey.responses)

if __name__ == '__main__':
    unittest.main()

متد setUp() دو کار انجام می‌دهد: یک شیء نمونه‌ی نظرسنجی می‌سازد و لیستی از پاسخ‌ها ایجاد می‌کند. هر دو پیشوند self گرفته‌اند پس می‌توان از آن‌ها هر جای کلاس استفاده کرد. این کار نوشتنِ دو متد را ساده‌تر می‌کنند چون هیچ‌کدام دیگر به ساختن نمونه‌ی نظرسنجی یا لیست پاسخ‌ها نیازی ندارند. متد test_store_single_response() ذخیره شدن اوّلین پاسخ را در self.responses[0] بررسی می‌کند و test_store_three_responses() ذخیره شدن درست هر سه پاسخ موجود را در self.responses درستی‌سنجی می‌کند.

اگر test_survey.py را دوباره اجرا کنیم هر دو آزمون موفّق خواهند بود. این آزمون‌ها به ویژه زمانی سودمندند که بخواهیم AnonymousSurvey را برای رسیدگی به چند پاسخِ یک کاربر توسعه دهیم. پس از تغییر دادن کد برای دریافت چند پاسخ از یک کاربر، می‌توانید این آزمون‌ها را دوباره اجرا کنید تا مطمئن شوید که توانایی ذخیره‌سازی یک پاسخ تکّی یا مجموعه از پاسخ‌های جدا تحت‌الشعاع قرار نگرفته باشد.

وقتی کلاس‌های خود را آزمایش می‌کنید متد setUp() نوشتن متدهای آزمون را ساده‌تر می‌کند. یک مجموعه از نمونه‌ها و مشخّصه‌ها را در setUp() می‌سازید و از آن‌ها در تمام متدهای آزمون خود استفاده می‌کنید. این کار از ساختنِ چندباره‌ی این نمونه‌ها در هر متد آزمون ساده‌تر خواهد بود.

نکته
زمانی که مورد آزمایشی در حال اجراست، پایتون پس از پایان هر آزمون واحد یک کاراکتر چاپ می‌کند؛ برای آزمون موفّق یک نقطه، برای آزمونی که به خطا برمی‌خورد یک E و برای آزمونی که به رد شدن یک متد اثبات می‌انجامد یک F. به این دلیل است که هنگام اجرای موردهای آزمایشی تعداد مختلفی نقطه و کاراکتر در خطّ اوّل می‌بینید. اگر اجرای یک مورد آزمایشی به دلیل تعدّد متدهای آزمون زمان زیادی ببرد می‌توانید با نگاه کردن به این نتایج درکی کلّی از تعداد آزمون‌های در حال انجام و قبول یا رد شدنِ آن‌ها پیدا کنید.

خودآزمایی

۱۱-۳. کارمند: کلاسی به نام Employee بنویسید. متد __init__() باید یک نام، نام خانوادگی و حقوق سالیانه دریافت کند و آن‌ها را به عنوان مشخّصه ذخیره نماید. متدی به نام give_raise() بنویسید که به طور پیش‌فرض ۵۰۰۰ دلار به حقوق سالیانه اضافه کند امّا مقادیر افزایشِ حقوقِ دیگر را هم در صورت وجود بپذیرد.

یک مورد آزمایشی برای Employee بنویسید. دو متد آزمون بنویسید، test_give_default_raise() و test_give_custom_raise(). از متد setUp() استفاده کنید تا مجبور نباشید در هر کدام از متدها یک شیء نمونه‌ی کارمند بسازید. مورد آزمایشی خود را اجرا کنید و مطمئن شوید که هر دو آزمون موفّق خواهند بود.

خلاصه

در این فصل آموختید که چگونه به کمک ابزارهای موجود در ماژول unittest برای تابع‌ها و کلاس‌های خود آزمون بنویسید. نوشتن کلاسی را که از unittest.TestCase ارث می‌برد یاد گرفتید و این که چگونه برای درستی‌سنجی رفتارهای خاصّ تابع‌ها و کلاس‌های خود آزمون بنویسید. به‌کارگیری متد setUp() را برای ساختن کارآمدتر نمونه‌ها و مشخّصه‌ها از کلاس خود آموختید تا از آن‌ها در تمام متدهای آزمون یک کلاس استفاده کنید.

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

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

سر و کلّه زدن با آزمون‌ها برای آشنا شدن با آن‌ها بخشی از فرآیند آزمون کد شماست. برای حیاتی‌ترین رفتارهای تابع‌ها و کلاس‌های خود آزمون بنویسید امّا در پروژه‌های اوّل خود دنبالِ پوشش کامل نباشید مگر این که دلیل خاصّی برای آن داشته باشید.


2 - بخش دوم - پروژه‌ها

تبریک می‌گویم. اکنون شما آن اندازه‌‌ از پایتون می‌دانید که بتوانید شروع به ساختن پروژه‌های تعاملی و معنادار کنید. با ساختنِ این پروژه‌ مهارت‌های جدیدی می‌آموزید و مفاهیم یادگرفته‌شده در بخش یکم تثبیت می‌شوند. بخش دوم سه نوع پروژه را در برمی‌گیرد و می‌توانید هر کدام از آن‌ها را به هر ترتیبی که خواستید انجام دهید. اینجا شرحی مختصر از هر کدام می‌آید تا به شما کمک کند در مورد انتخاب اوّلین پروژه تصمیم بگیرید.

تهاجمِ موجودات بیگانه: ساختنِ یک بازی با پایتون در پروژه‌ی تهاجم موجودات بیگانه (فصل ۱۲, فصل ۱۳ و فصل ۱۴) از بسته‌ی Pygame برای توسعه دادن یک بازی دوبعدی بهره می‌گیرید که هدف در آن، شلیک کردن به ناوگانی از موجودات بیگانه است. این موجودات مرحله به مرحله از صفحه پایین می‌آیند و در هر مرحله سرعت و میزان مشکل بودن بازی افزایش می‌یابد. در پایان پروژه مهارت‌هایی کسب خواهید کرد که به شما امکان خواهد داد بازی دوبعدی دلخواه خود را به کمک Pygame توسعه دهید.

بصری‌سازی داده‌ها پروژه‌ی بصری‌سازی داده‌ها از فصل ۱۵ شروع می‌شود و در آن می‌آموزید که چگونه داده‌ها را تولید کنید و سپس تعدادی نمایشِ بصریِ زیبا و کاربردی از آن‌ها به کمک Matplotlib و Plotly بسازید. فصل ۱۶ به شما یاد می‌دهد که به داده‌های منابع آنلاین دسترسی پیدا کنید و آن‌ها را به یک بسته‌ی بصری‌سازی بدهید تا نمودارهایی از داده‌های هواشناسی و یک نقشه‌ی لرزه‌نگار جهانی رسم کند. سرانجام در فصل ۱۷ خواهید دید که چگونه برنامه‌ای برای دریافت خودکار و بصری‌سازی داده بنویسید. آموختن ساختن نمایش‌های بصری به شما امکان می‌دهد که حوزه‌ی داده‌کاوی را بررسی کنید که یکی از مهارت‌های مورد توجّه در دنیای امروز است.

برنامه‌ی تحت وب در پروژه‌ برنامه‌های تحت وب (فصل ۱۸, فصل ۱۹ و فصل ۲۰) از بسته‌ی Django برای ساختن یک برنامه‌ی وب ساده استفاده می‌کنید که به کاربران امکان می‌دهد هر تعداد موضوع مورد مطالعه را در قالب یک نشریه ضبط کنند. کاربران یک حساب کاربری با نام کاربری و گذرواژه می‌سازند، موضوعی را وارد می‌کنند و سپس مواردی را که یاد گرفته‌اند مورد به مورد اضافه می‌کنند. همچنین یاد خواهید گرفت که چگونه برنامه‌ی خود را استقرار دهید تا از همه جای دنیا قابل دسترسی باشد. پس از تمام کردن این پروژه قادر خواهید بود که ساختن برنامه‌های تحت وب ساده‌ی خود را شروع کنید و این آمادگی را خواهید داشت که در منابع مربوط به ساختن برنامه‌ها با Django پژوهش کنید.

2.1 - فصل دوازدهم

2.1.1 - مرور پروژه و ساختن اوّلین موجود بیگانه

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

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

پیش از نوشتن هر کد جدیدی برای افزودن ناوگانی از بیگانگان به صفحه اجازه دهید که به پروژه نگاهی بیندازیم و طرح خود را به‌روز کنیم.

مرور پروژه

پیش از شروع فاز جدید توسعه در یک پروژه‌ی بزرگ، بازنگریِ طرح و روشن کردن هدف از نوشتن کد فکر خوبی است. در این فصل ما:

  • کد خود را بررسی می‌کنیم تا معلوم کنیم پیش از پیاده‌سازی ویژگی‌های جدید نیاز به بازآرایی دارد یا نه.
  • یک موجود بیگانه‌ی تکّی به گوشه‌ی بالا-چپ صفحه با فاصله‌گذاریِ مناسب در اطراف آن اضافه می‌کنیم.
  • بر اساس فاصله‌ی اطراف موجود اوّل و اندازه‌ی کلّی صفحه تعداد موجوداتی را که روی صفحه جا می‌گیرند حساب می‌کنیم. حلقه‌ای خواهیم نوشت که موجودات بیگانه را برای پر کردن بخش بالایی صفحه تولید کند.
  • ناوگان را به سمت پایین و اطراف حرکت می‌دهیم تا جایی که تمام ناوگان نابود شود، یکی از بیگانگان به سفینه بخورد و یا یکی از آن‌ها به زمین برسد. اگر تمام ناوگان نابود شود یک ناوگان جدید می‌سازیم. اگر یکی از موجودات بیگانه به سفینه یا زمین بخورد سفینه را نابود می‌کنیم و یک ناوگان جدید می‌سازیم.
  • تعداد سفینه‌های قابل استفاده‌ی هر بازیکن را محدود می‌کنیم و با تمام شدنِ سفینه‌های اختصاص‌یافته به بازیکن بازی را به پایان می‌رسانیم.

این طرح را همزمان با پیاده‌سازی ویژگی‌ها اصلاح می‌کنیم ولی همین برای شروع کافی‌ست.

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

ساختن اوّلین موجود بیگانه

قرار دادن یک موجود بیگانه روی صفحه مثلِ قرار دادن سفینه روی صفحه است. کلاسی به نام Alien رفتار هر موجود بیگانه را کنترل می‌کند که ساختار آن را شبیه کلاس Ship ایجاد می‌کنیم. برای سادگی به استفاده از تصویر‌های بیت‌مپ ادامه می‌دهیم. خودتان می‌توانید تصویری برای موجودات بیگانه پیدا کنید یا از نمونه‌ی نشان داده شده در شکل ۱۳-۱ استفاده کنید که از منابع کتاب به آدرس https://nostarch.com/pythoncrashcourse2e دریافت کنید. رنگ پس‌زمینه‌ی این تصویر خاکستری است که به رنگ پس‌زمینه‌ی صفحه می‌خورد. مطمئن شوید عکس انتخابی خود را در پوشه‌ی images ذخیره کنید.

![][image4]

شکل ۱۳-۱ تصویری که برای موجود بیگانه به کار خواهیم برد.

ساختن کلاس Alien

اکنون کلاسی به نام Alien می‌سازیم و آن را در alien.py ذخیره می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pygame

from pygame.sprite import Sprite

class Alien(Sprite):
    """A class to represent a single alien in the fleet."""

    def __init__(self, ai_game):
        """Initialize the alien and set its starting position."""
        super().__init__()
        self.screen = ai_game.screen

        # Load the alien image and set its rect attribute.
        self.image = pygame.image.load('images/alien.bmp')
        self.rect = self.image.get_rect()

        # Start each new alien near the top left of the screen.
        self.rect.x = self.rect.width
        self.rect.y = self.rect.height

        # Store the alien's exact horizontal position.
        self.x = float(self.rect.x)

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

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

ساختن نمونه‌ای از Alien

می‌خواهیم نمونه‌ای از Alien بسازیم تا اوّلین موجود بیگانه را روی صفحه ببینیم. چون این بخشی از بخش راه‌اندازی کار است، کد این نمونه را به انتهای متد __init__() در AlienInvasion اضافه می‌کنیم. ما در نهایت می‌خواهیم ناوگانی کامل از این موجودات بیگانه بسازیم، که این کمی کار خواهد برد، بنابراین متد کمکی جدیدی به نام _create_fleet() می‌سازیم.

ترتیب متدهای یک کلاس مهم نیست تا زمانی که در نحوه‌ی قرارگیری آن‌ها ثبات نسبی برقرار باشد. من متد _create_fleet() را درست قبل از _update_screen() قرار می‌دهم ولی هر جای دیگرِ AlienInvasion هم قرار بگیرد کار خواهد کرد. ابتدا کلاس Alien را وارد می‌کنیم.

اینجا عبارت‌های مربوط به وارد کردن ماژول‌ها را در alien_invasion.py به‌روز می‌کنیم:

–snip–

1
2
from bullet import Bullet
from alien import Alien

و اینجا متد __init__() را به‌روزرسانی می‌کنیم:

1
2
3
4
5
6
def __init__(self):
    --snip--
    self.ship = Ship(self)
    self.bullets = pygame.sprite.Group()
    self.aliens = pygame.sprite.Group()
    self._create_fleet()

گروهی برای نگهداری ناوگان موجودات بیگانه می‌سازیم و _create_fleet() را که الان می‌خواهیم بنویسیم، فرامی‌خوانیم.

این متد جدید _create_fleet() است:

1
2
3
4
5
def _create_fleet(self):
    """Create the fleet of aliens."""
    # Make an alien.
    alien = Alien(self)
    self.aliens.add(alien)

در این متد یک نمونه از Alien می‌سازیم و سپس آن را به گروهِ شامل ناوگان اضافه می‌کنیم. موجود بیگانه در جای پیش‌فرضِ بالا-چپ روی صفحه قرار می‌گیرد که برای اوّلین موجود بیگانه عالی است.

برای پدیدار کردن موجود بیگانه باید متد draw() گروه را در _update_screen() صدا کنیم:

1
2
3
4
5
6
7
8
def _update_screen(self):
    --snip--
    for bullet in self.bullets.sprites():
        bullet.draw_bullet()

    self.aliens.draw(self.screen)

    pygame.display.flip()

زمانی که draw() را بر روی یک گروه اعمال می‌کنید، پای‌گیم هر عنصر داخل آن گروه را در موقعیّتی که مشخّصه‌ی rect آن مشخّص کرده رسم می‌کند. متد draw() تنها یک نشان‌وند می‌خواهد: رویه‌ای که عنصرهای گروه قرار است روی آن رسم شوند. شکل ۱۳-۲ اوّلین موجود بیگانه را روی صفحه نمایش می‌دهد.

![][image5]

شکل ۱۳-۲ اوّلین موجود بیگانه نمایان می‌شود.

اکنون که اوّلین موجود بیگانه به درستی نمایش داده شد کد مربوط به رسم کل ناوگان را می‌نویسیم.

2.1.2 - ساختن ناوگان موجودات بیگانه

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

تعیین تعداد موجودات بیگانه‌ای که در یک سطر جا می‌شوند

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

1
available_space_x = settings.screen_width - (2 * alien_width)

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

1
number_aliens_x = available_space_x // (2 * alien_width)

هنگام ایجاد ناوگان از این محاسبات بهره خواهیم گرفت.

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

ساختن سطری از موجودات بیگانه

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def _create_fleet(self):
    """Create the fleet of aliens."""
    # Create an alien and find the number of aliens in a row.
    # Spacing between each alien is equal to one alien width.
    alien = Alien(self)
    alien_width = alien.rect.width
    available_space_x = self.settings.screen_width - (2 * alien_width)
    number_aliens_x = available_space_x // (2 * alien_width)

    # Create the first row of aliens.
    for alien_number in range(number_aliens_x):
        # Create an alien and place it in the row.
        alien = Alien(self)
        alien.x = alien_width + 2 * alien_width * alien_number
        alien.rect.x = alien.x
        self.aliens.add(alien)

پیش از این به بیشتر بخش‌های این کد فکر کرده‌ایم. پیش از انجام هر محاسبه‌ای نیاز داریم که طول و عرض هر موجود بیگانه را پیدا کنیم پس در ۱ یکی از این موجودات می‌سازیم. این یکی جزئی از ناوگان نخواهد بود پس آن را به گروه aliens اضافه نمی‌کنیم. در ۲ عرض موجود بیگانه را از روی مشخّصه‌ی rect آن محاسبه و مقدار آن را در alien_width ذخیره می‌کنیم تا هر بار نیاز نباشد با مشخّصه‌ی rect سر و کار داشته باشیم. در ۳ فضای افقی موجود برای موجودات را محاسبه و تعداد موجودات بیگانه‌ای را که در آن فضا جا می‌گیرند پیدا می‌کنیم.

در ادامه حلقه‌ای می‌نویسیم که از صفر تا تعداد موجودات بیگانه‌ی موردنیاز می‌شمارد (۴). در بدنه‌ی اصلی این حلقه یک موجود بیگانه‌ی جدید ساخته و مختصه‌ی x آن را برابر با محل قرارگیری آن در سطر قرار می‌دهیم (۵). هر موجود بیگانه به اندازه‌ی یک عرض خود از حاشیه‌ی سمت چپ فاصله می‌گیرد. بعد عرض موجود بیگانه را دوبرابر می‌کنیم تا فضای اشغال‌شده‌ی هر کدام، با در نظر گرفتنِ فضای خالی سمت راست آن، به دست بیاید و در نهایت این مقدار را در موقعّیتِ موجود بیگانه در سطر ضرب می‌کنیم. از مشخّصه‌ی x هر موجود برای تعیین موقعیّت مستطیل آن استفاده می‌کنیم. سپس هر موجود بیگانه‌ی جدید را به گروه aliens اضافه می‌کنیم.

اکنون که هجوم بیگانگان را اجرا می‌کنید باید اوّلین سطر موجود بیگانگان را همانند شکل ۱۳-۳ ببینید:

![][image6]

شکل ۱۳-۳: اوّلین سطر موجودات بیگانه

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

نکته
جاگیری سطر اوّل موجودات بیگانه در سیستم شما، بسته به عرض صفحه‌ای که انتخاب کرده‌اید ممکن است کمی متفاوت باشد.

بازآرایی

اگر کدی که نوشته‌ایم تمام کاری بود که برای ساختن ناوگان نیاز داشتیم، احتمالاً می‌توانستیم _create_fleet() را به همان شکلی که بود رها کنیم. امّا در ادامه کار بیشتری برای انجام داریم پس اجازه دهید که این متد را کمی مرتّب کنیم. یک متد کمکی جدید به نام _create_alien() می‌سازیم و آن را در _create_fleet() صدا می‌زنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def _create_fleet(self):
    --snip--
    # Create the first row of aliens.
    for alien_number in range(number_aliens_x):
        self._create_alien(alien_number)

def _create_alien(self, alien_number):
    """Create an alien and place it in the row."""
    alien = Alien(self)
    alien_width = alien.rect.width
    alien.x = alien_width + 2 * alien_width * alien_number
    alien.rect.x = alien.x
    self.aliens.add(alien)

متد _create_alien() به جز self یک پارامتر دیگر هم می‌خواهد: تعداد موجودات بیگانه‌ای که اکنون می‌سازیم. از همان بدنه‌ی _create_fleet() استفاده می‌کنیم به جز این که به جای منتقل کردن عرض یک موجود بیگانه به عنوان یک نشان‌وند، آن را داخل متد به دست می‌آوریم. این بازآرایی کمک می‌کند که سطرهای جدید و یک ناوگان کامل را آسان‌تر اضافه کنیم.

افزودن سطرها

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

1
available_space_y = settings.screen_height - (3 * alien_height) - ship_height

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

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

1
number_rows = available_space_y // (2 * alien_height)

حال که تعداد سطرهای هر ناوگان مشخّص شد، کد را برای ساختن یک سطر تکرار می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def _create_fleet(self):
    --snip--
    alien = Alien(self)
    alien_width, alien_height = alien.rect.size
    available_space_x = self.settings.screen_width - (2 * alien_width)
    number_aliens_x = available_space_x // (2 * alien_width)

    # Determine the number of rows of aliens that fit on the screen.
    ship_height = self.ship.rect.height
    available_space_y = (self.settings.screen_height -
        (3 * alien_height) - ship_height)
    number_rows = available_space_y // (2 * alien_height)

    # Create the full fleet of aliens.
    for row_number in range(number_rows):
        for alien_number in range(number_aliens_x):
            self._create_alien(alien_number, row_number)

def _create_alien(self, alien_number, row_number):
    """Create an alien and place it in the row."""
    alien = Alien(self)
    alien_width, alien_height = alien.rect.size
    alien.x = alien_width + 2 * alien_width * alien_number
    alien.rect.x = alien.x
    alien.rect.y = alien.rect.height + 2 * alien.rect.height * row_number
    self.aliens.add(alien)

ما عرض و ارتفاع یک موجود بیگانه را می‌خواهیم پس در ۱ از مشخّصه‌ی size استفاده می‌کنیم که یک دوتایی شامل عرض و ارتفاع شیء rect را در بر می‌گیرد. برای محاسبه‌ی تعداد سطرهای قابل نمایش محاسباتِ available_space_y را درست بعد از محاسباتِ available_space_x قرار می‌دهیم (۲). محاسبات داخل پرانتز قرار گرفته‌اند پس می‌توان آن را در خط نمایش داد. این کار که معمولاً توصیه می‌شود، باعث می‌شود تعداد کاراکترهای هر خط برنامه کمتر از ۷۹ باشد.

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

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

در تعریف _create_alien() به پارامتری نیاز داریم که تعداد سطر را نگه دارد. داخل _create_alien() مختصّات y یک موجود بیگانه را طوری تغییر می‌دهیم که در سطر اوّل نباشد (۴) و رسم سطرها را با در نظر گرفتن یک فضای خالی در بالای صفحه به اندازه‌ی ارتفاع یک موجود بیگانه شروع می‌کنیم. هر سطر از فاصله‌ای به اندازه‌ی دوبرابرِ ارتفاع یک موجود بیگانه تا سطر قبلی شروع می‌شود بنابراین ارتفاع موجود بیگانه را در ۲ و سپس در تعداد سطرها ضرب می‌کنیم. شماره‌ی اوّلین سطر ۰ است پس جاگیری عمودی اوّلین سطر دست نمی‌خورد. تمام سطرهای بعدی به ترتیب به سمت پایین صفحه قرار می‌گیرند.

اکنون با اجرای برنامه ناوگان کامل موجودات بیگانه را خواهید دید، چنانکه در شکل ۱۳-۴ هم نشان داده شده است.

![][image7]

شکل ۱۳-۴: ناوگان کامل پدیدار می‌شود.

در بخش بعدی ناوگان را به حرکت درخواهیم آورد!

خودآزمایی

۱۳-۱ ستاره‌ها: تصویر یک ستاره را پیدا کنید. تعدادی ستاره را به صورت شبکه‌ای روی صفحه نشان دهید.

۱۳-۲ ستاره‌های بهتر: می‌توانید با قرار دادن هر ستاره در مکانی تصادفی، الگوی نمایشی واقعی‌تری ایجاد کنید. به یاد بیاورید که تولید اعداد تصادفی به این شکل انجام می‌گرفت:

1
2
from random import randint
random_number = randint(-10, 10)

این کد عددی تصادفی بین ۱۰- و ۱۰ برمی‌گرداند. با استفاده از کد خود در تمرین ۱۳-۱ موقعّیت هر ستاره را به صورت تصادفی تعیین کنید.

2.1.3 - به راه انداختن ناوگان

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

به راست راندن ناوگان

برای حرکت دادن موجودات بیگانه از یک متد update() در alien.py استفاده می‌کنیم که آن را برای هر کدام از موجودات داخل گروه فرامی‌خوانیم. ابتدا تنظیمات مربوط به کنترل سرعت هر موجود بیگانه را اضافه می‌کنیم:

1
2
3
4
def __init__(self):
    --snip--
    # Alien settings
    self.alien_speed = 1.0

سپس از این تنظیمات برای پیاده‌سازی update() بهره می‌گیریم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def __init__(self, ai_game):
    """Initialize the alien and set its starting position."""
    super().__init__()
    self.screen = ai_game.screen
    self.settings = ai_game.settings
    --snip--

def update(self):
    """Move the alien to the right."""
    self.x += self.settings.alien_speed
    self.rect.x = self.x

در __init__() پارامتری برای تنظیمات ایجاد می‌کنیم تا در update() به سرعت موجود بیگانه دسترسی داشته باشیم. هر بار که موقعیّت موجود بیگانه را به‌روز می‌کنیم آن را به مقداری که در alien_speed ذخیره شده به سمت راست می‌بریم. موقعیّت دقیق هر موجود را از طریق مشخّصه‌ی self.x دنبال می‌کنیم که می‌تواند اعداد اعشاری را هم ذخیره کند (۱). سپس از مقدار self.x برای به‌روزرسانی موقعیّت rect هر موجود استفاده می‌کنیم (۲).

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

1
2
3
4
5
6
while True:
    self._check_events()
    self.ship.update()
    self._update_bullets()
    self._update_aliens()
    self._update_screen()

ما قصد داریم کدی برای مدیریّت حرکت ناوگان بنویسیم بنابراین متد جدیدی به نام _update_aliens() می‌سازیم. ما موقعیّت موجود بیگانه را پس از به‌روزرسانی گلوله‌ها به‌روز می‌کنیم زیرا به زودی برخورد گلوله به هر یک از موجودات بیگانه را هم بررسی خواهیم کرد.

محل قرارگیری این متد در ماژول خیلی مهم نیست. ولی من برای حفظ ساختار آن را درست بعد از _update_bullets قرار می‌دهم تا به ترتیب فراخوانی متدها در حلقه‌ی while بخورد. این اوّلین نسخه‌ی _update_aliens() است:

1
2
3
def _update_aliens(self):
    """Update the positions of all aliens in the fleet."""
    self.aliens.update()

متد update() را بر روی گروه aliens اعمال می‌کنیم که متد update() هر موجود بیگانه را فراخوانی می‌کند. اگر اکنون هجوم بیگانگان را اجرا کنید باید ناوگان را در حالت حرکت به سمت راست ببینید تا جایی که از صفحه محو شوند.

ایجاد تنظیمات برای جهت حرکت ناوگان

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

1
2
3
4
5
# Alien settings
self.alien_speed = 1.0
self.fleet_drop_speed = 10
# fleet_direction of 1 represents right; -1 represents left.
self.fleet_direction = 1

متغیّر fleet_drop_speed سرعت پایین افتادن هر موجود را پس از رسیدن به هر دو سمت صفحه را کنترل می‌کند. جدا کردن این سرعت از سرعت افقی موجودات بیگانه کمک می‌کند که بتوانیم این دو را جداگانه تنظیم کنیم.

برای پیاده‌سازی fleet_direction می‌توانیم از مقدار متنی مانند 'left' یا 'right' استفاده کنیم امّا در نهایت باید از عبارت‌های if-elif برای آزمودن جهت حرکت ناوگان استفاده کنیم. پس به جای این و چون تنها دو جهت برای حرکت داریم، از مقدارهای 1 و -1 برای تعیین جهت استفاده می‌کنیم و هر بار خواستیم جهت ناوگان را تغییر دهیم یکی از این دو عدد را انتخاب می‌کنیم. (به‌کارگیری اعداد در اینجا معنادار هم هست زیرا حرکت به سمت راست متناظر با افزوده شدن به مقدار مختصه‌ی x هر موجود و حرکت به چپ متناظر با کم کردن این مقدار است.)

بررسی برخورد یک موجود بیگانه با کناره‌های صفحه

به متدی احتیاج داریم که مشخّص کند آیا موجود بیگانه‌ای به کناره‌های صفحه برخورد کرده است یا نه. همچنین باید update() را طوری تغییر دهیم که امکان حرکت در جهت درست را فراهم کند. این کد بخشی از کلاس Alien است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def check_edges(self):
    """Return True if alien is at edge of screen."""
    screen_rect = self.screen.get_rect()
    if self.rect.right >= screen_rect.right or self.rect.left <= 0:
        return True

def update(self):
    """Move the alien right or left."""
    self.x += (self.settings.alien_speed *
                self.settings.fleet_direction)
    self.rect.x = self.x

برای بررسی این که هر کدام از موجودات بیگانه به سمت چپ یا راست صفحه رسیده‌اند یا نه می‌توانیم متد check_edges() را صدا کنیم. اگر مشخّصه‌ی right مربوط به rect بزرگتر یا مساوی مشخّصه‌ی right مربوط به rect صفحه باشد، موجود بیگانه در ضلع راست صفحه قرار دارد. اگر مقدار left کمتر یا مساوی صفر باشد موجود بیگانه در ضلع چپ صفحه قرار دارد (۱).

برای ممکن ساختن حرکت به چپ یا راست متد update() را تغییر می‌دهیم و سرعت موجود بیگانه را در مقدار fleet_direction ضرب می‌کنیم (۲). اگر fleet_direction برابر با 1 باشد مقدار alien_speed به مقدار فعلی موقعیّت اضافه می‌شود و موجود بیگانه به راست می‌رود؛ اگر fleet_direction برابر -1 باشد این مقدار از موقعّیت فعلی کسر خواهد شد که موجب حرکت به سمت چپ می‌شود.

پایین آوردن ناوگان و تغییر جهت آن

زمانی که یک موجود بیگانه به کناره‌ی صفحه می‌رسد کل ناوگان باید به سمت پایین بیاید و جهت حرکت خود را تغییر دهد. بنابراین باید چند خط کد به AlienInvasion اضافه کنیم زیرا در آن جا باید بررسی کنیم که آیا هیچ موجود بیگانه‌ای به ضلع چپ یا راست صفحه رسیده است یا نه. این کار را با نوشتن دو متد _check_fleet_edges() و _change_fleet_direction() و تغییر دادن _update_aliens() انجام می‌دهیم. من متدهای جدید را پس از _create_alien() قرار می‌دهم امّا دوباره تاکید می‌کنم که محل قرارگیری متدها در کلاس مهم نیست.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def _check_fleet_edges(self):
    """Respond appropriately if any aliens have reached an edge."""
    for alien in self.aliens.sprites():
        if alien.check_edges():
            self._change_fleet_direction()
            break

def _change_fleet_direction(self):
    """Drop the entire fleet and change the fleet's direction."""
    for alien in self.aliens.sprites():
        alien.rect.y += self.settings.fleet_drop_speed
    self.settings.fleet_direction *= -1

در متد _check_fleet_edges() در ناوگان حلقه می‌زنیم و check_edges() را برای هر موجود بیگانه صدا می‌کنیم (۱). اگر check_edges() درست (True) برگرداند می‌فهمیم که موجود بیگانه به کناره‌ی صفحه رسیده است و کل ناوگان باید تغییر جهت انجام دهد؛ بنابراین متد _change_fleet_direction() را فراخوانی کرده و حلقه را می‌شکنیم (۲). از _change_fleet_direction() بین تمام موجودات بیگانه حلقه می‌زنیم و هر کدام را به کمک تنظیمات fleet_drop_speed پایین می‌آوریم (۳)؛ سپس مقدار fleet_direction را با ضرب کردن مقدار آن در -1 تغییر می‌دهیم. خطّی که در آن جهت حرکت تغییر می‌کند جزء حلقه‌ی for نیست. ما می‌خواهیم موقعیّت تمامِ موجودات بیگانه را تغییر دهیم امّا جهت حرکت کل ناوگان را یک بار تغییر می‌دهیم.

این تغییراتی است که در _update_aliens() ایجاد می‌کنیم:

1
2
3
4
5
6
7
def _update_aliens(self):
    """
    Check if the fleet is at an edge,
      then update the positions of all aliens in the fleet.
    """
    self._check_fleet_edges()
    self.aliens.update()

تغییری که ایجاد کرده‌ایم فراخوانی _check_fleet_edges() پیش از به‌روزرسانی موقعیّت هر موجود بیگانه است.

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

خودآزمایی

۱۳-۳ قطره‌های باران: تصویر یک قطره‌ی باران را پیدا کنید و شبکه‌ای از قطره‌های باران بسازید. کاری کنید که قطره‌ها به سمت پایین صفحه حرکت کنند تا جایی که ناپدید شوند.

۱۳-۴ باران مداوم: کد خود در تمرین ۱۳-۳ را جوری تغییر دهید که با ناپدید شدن هر ردیف قطره‌های باران در پایین صفحه، یک ردیف جدید در بالای آن اضافه شود و شروع به حرکت کند.

2.1.4 - شلّیک به موجودات بیگانه

تا اینجا سفینه و ناوگان بیگانگان را ساخته‌ایم ولی گلوله‌ها با رسیدن به موجودات بیگانه صرفاً از آن‌ها رد می‌شوند زیرا برخوردها را بررسی نمی‌کنیم. در برنامه‌نویسی بازی برخورد زمانی رخ می‌دهد که عناصر بازی با هم هم‌پوشانی پیدا کنند. برای این که کاری کنیم گلوله‌ها موجودات بیگانه را نابود کنند از متد sprite.groupcollide() استفاده می‌کنیم تا برخورد بین اعضای دو گروه را تشخیص دهیم.

تشخیص برخورد گلوله

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

تابع sprite.groupcollide() مستطیل‌های تک‌تک اعضای یک گروه را با اعضای گروه دیگر مقایسه می‌کند. در مورد مسأله‌ی ما این تابع rect هر گلوله را با rect هر موجود بیگانه مقایسه می‌کند و آن‌هایی را که با هم برخورد داشته‌اند در قالب یک دیکشنری برمی‌گرداند. هر کلید دیکشنری یک گلوله و مقدار متناظرش موجود بیگانه‌ای خواهد بود که گلوله با آن برخورد کرده است. (از این دیکشنری در فصل ۱۴ برای پیاده‌سازی سیستم امتیازدهی استفاده خواهیم کرد.)

کد زیر را به آخر _update_bullets() اضافه کنید تا برخورد بین گلوله‌ها و موجودات بیگانه را تشخیص دهید:

1
2
3
4
5
6
7
def _update_bullets(self):
    """Update position of bullets and get rid of old bullets."""
    --snip--
    # Check for any bullets that have hit aliens.
    # If so, get rid of the bullet and the alien.
    collisions = pygame.sprite.groupcollide(
            self.bullets, self.aliens, True, True)

کد جدیدی که اضافه کرده‌ایم موقعیّت تمام گلوله‌های self.bullets را با تمام بیگانه‌ها در self.aliens مقایسه می‌کند و هر کدام را که هم‌پوشانی داشته باشند تشخیص می‌دهد. هر گاه مستطیل‌های یک گلوله و موجود بیگانه روی هم بیفتند groupcollide() یک جفت کلید-مقدار به دیکشنری برگشتی خود اضافه می‌کند. دو نشان‌وند True به پای‌گیم می‌گوید که گلوله‌ها و موجودات بیگانه‌ای را که با هم برخورد کرده‌اند حذف کند. (اگر بخواهید گلوله‌ای پُرتوان درست کنید که هنگام حرکت به سمت بالای صفحه تمام موجودات بیگانه‌ی سر راهش را نابود کند، می‌توانید نشان‌وند بولی اوّل را False کنید و دومی را درست (True) نگه دارید. موجودات بیگانه‌ای که مورد اصابت قرار گرفته‌اند ناپدید می‌شوند امّا گلوله‌ها تا زمان خارج شدن از بالای صفحه باقی می‌مانند.)

اگر اکنون هجوم بیگانگان را اجرا کنید، بیگانگانی که مورد اصابت قرار گرفته‌اند باید ناپدید شوند. شکل ۱۳-۵ ناوگانی را نشان می‌دهد که بخشی از آن نابود شده است.

![][image8]

شکل ۱۳-۵: می‌توانیم به بیگانگان شلیّک کنیم!

بزرگ کردن آزمایشی گلوله‌ها

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

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

تغییر دلخواه من در هجوم بیگانگان استفاده از یک گلوله‌ی بسیار عریض است که پس از برخورد با موجود بیگانه فعّال باقی می‌ماند (شکل ۱۳-۶ را ببینید.) مقدار bullet_width را ۳۰۰ یا حتّی ۳۰۰۰ قرار دهید و ببینید که چه‌قدر سریع می‌توانید ناوگان را نابود کنید!

![][image9]

شکل ۱۳-۶: گلوله‌های بسیار قدرتمند آزمودن برخی از جنبه‌های بازی را ساده‌تر می‌کند.

بازسازی ناوگان

یکی از ویژگی‌های اصلی هجوم بیگانگان بی‌امان بودن هجوم موجودات بیگانه است: هر بار که ناوگانی نابود می‌شود ناوگانی جدید باید ظاهر شود.

برای ساختن ناوگان جدید پس از نابود قبلی، ابتدا بررسی می‌کنیم که آیا گروه aliens خالی است یا نه. اگر چنین باشد _create_fleet() را فراخوانی می‌کنیم. این بررسی را در پایان _update_bullets() انجام می‌دهیم زیرا جایی است که موجودات بیگانه نابود شده‌اند.

1
2
3
4
5
6
def _update_bullets(self):
    --snip--
    if not self.aliens:
        # Destroy existing bullets and create new fleet.
        self.bullets.empty()
        self._create_fleet()

در ۱ خالی بودن گروه aliens را بررسی می‌کنیم. یک گروه خالی False ارزیابی می‌شود پس این راه ساده‌ای برای بررسی خالی بودن گروه است. اگر چنین باشد تمام گلوله‌های موجود را به کمک متد empty() حذف می‌کنیم. این متد اسپرایت‌های باقی مانده در یک گروه را حذف می‌کند (۲). ما همچنین _create_fleet() را فرا می‌خوانیم تا صفحه را دوباره با موجودات بیگانه پر کند.

حال اگر ناوگانی را نابود کنید بلافاصله ناوگان جدیدی ظاهر می‌شود.

سرعت دادن به گلوله‌ها

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

سرعت گلوله‌ها را با تنظیم مقدار bullet_speed در settings.py تغییر می‌دهیم. من در سیستم خودم این مقدار را 1.5 گذاشته‌ام تا گلوله کمی سریع‌تر حرکت کند:

1
2
3
4
# Bullet settings
self.bullet_speed = 1.5
self.bullet_width = 3
--snip--

بهترین مقدار برای این تنظیمات به سرعت سیستم شما بستگی دارد پس مقداری را بیاید که برای شما به کار بیاید. تنظیمات دیگر را هم می‌توانید عوض کنید.

بازآرایی _update_bullets()

اکنون _update_bullets() را بازآرایی می‌کنیم تا این همه وظیفه‌های متنوّع را بر عهده نداشته باشد. کد مربوط به بررسی برخورد گلوله و بیگانه را به متدی جدا منتقل می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def _update_bullets(self):
    --snip--
    # Get rid of bullets that have disappeared.
    for bullet in self.bullets.copy():
        if bullet.rect.bottom <= 0:
            self.bullets.remove(bullet)
    self._check_bullet_alien_collisions()

def _check_bullet_alien_collisions(self):
    """Respond to bullet-alien collisions."""
    # Remove any bullets and aliens that have collided.
    collisions = pygame.sprite.groupcollide(
            self.bullets, self.aliens, True, True)
    if not self.aliens:
        # Destroy existing bullets and create new fleet.
        self.bullets.empty()
        self._create_fleet()

متد جدیدی به نام _check_bullet_alien_collisions() ساخته‌ایم که به دنبال برخورد بین گلوله‌ها و موجودات بیگانه می‌گردد و در صورت نابودی کامل یک ناوگان پاسخ مناسب می‌دهد. انجام چنین کاری مانع طولانی شدن بیش از حد _update_bullets() می‌شود و توسعه را در آینده ساده‌تر می‌کند.

خودآزمایی

۱۳-۵ تیرانداز جانبی بخش ۲: از تمرین ۱۲-۶ (تیراندازان جانبی) تا اینجا راه درازی آمده‌ایم. برای این تمرین آن بازی را به جایی برسانید که هجوم بیگانگان را توسعه داده‌ایم. ناوگانی از موجودات بیگانه اضافه کنید و کاری کنید که در عرض صفحه به سمت سفینه حرکت کند. یا کدی بنویسید که موجودات بیگانه را در موقعّیت‌های تصادفی در سمت راست صفحه قرار دهد و آن را به سمت سفینه بفرستد. همچنین کدی بنویسید که موجودات بیگانه را پس از اصابت ناپدید کند.

2.1.5 - پایان دادن به بازی

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

تشخیص برخورد موجود بیگانه و سفینه

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

1
2
3
4
5
6
def _update_aliens(self):
    --snip--
    self.aliens.update()
    # Look for alien-ship collisions.
    if pygame.sprite.spritecollideany(self.ship, self.aliens):
        print("Ship hit!!!")

متد spritecollideany() دو نشان‌وند می‌پذیرد: یک اسپرایت و یک گروه. این تابع به دنبال هر عضوی از گروه می‌گردد که با اسپرایت هم‌پوشانی پیدا کرده و به محض پیدا کردن اوّلین مورد جست‌وجو را متوقّف می‌کند. اینجا این تابع در گروه aliens می‌گردد و اوّلین موجود بیگانه‌ای را که با ship برخورد داشته برمی‌گرداند.

اگر هیچ برخوردی رخ ندهد این تابع None برمی‌گرداند و بلوک if در ۱ اجرا نخواهد شد. اگر برخوردی بین یک موجود بیگانه و سفینه پیدا شود، آن موجود برگردانده می‌شود و بلوک if اجرا می‌شود که روی صفحه چاپ می‌کند Ship hit!!! (۲). اگر برخورد رخ دهد باید چند کار انجام دهیم: باید تمام گلوله‌ها و موجودات بیگانه را حذف کنیم، سفینه را به وسط برگردانیم و ناوگانی جدید بسازیم. بیش از نوشتن کد برای انجام این کارها باید بدانیم که رویکرد ما در تشخیص برخورد موجودات بیگانه و سفینه درست کار می‌کند یا نه. نوشتن print() راهی ساده است که مطمئن شویم برخوردها را درست تشخیص داده‌ایم.

اکنون اگر هجوم بیگانگان را اجرا کنید باید پس از برخورد یک موجود بیگانه با سفینه پیام Ship hit!!! در ترمینال ظاهر شود. زمانی که ویژگی را آزمایش می‌کنید مقدار fleet_drop_speed را بیشتر کنید، مثلاً ۵۰ یا ۱۰۰، تا موجودات بیگانه سریع‌تر به سفینه شما برسند.

پاسخ دادن به برخورد موجودات بیگانه و سفینه

اکنون باید بفهمیم که هنگام برخورد یک موجود بیگانه با سفینه دقیقاً چه اتّفاقی رخ می‌دهد. به جای نابود کردن نمونه‌ی ship و ساختن یکی دیگر، تعداد سفینه‌های نابودشده را با پیگیری آمارهای بازی می‌شماریم. ثبت این آمارها برای امتیازدهی هم مفید خواهد بود.

بیایید کلاسی جدید به نام GameStats بنویسیم که آمار بازی را دنبال می‌کند و آن را در game_stats.py ذخیره کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class GameStats:
    """Track statistics for Alien Invasion."""

    def __init__(self, ai_game):
        """Initialize statistics."""
        self.settings = ai_game.settings
        self.reset_stats()

    def reset_stats(self):
        """Initialize statistics that can change during the game."""
        self.ships_left = self.settings.ship_limit

یک نمونه از این کلاس برای کل زمانی که هجوم بیگانگان اجرا می‌شود می‌سازیم. تنها زمانی این آمار را نو می‌کنیم که بازیکن بازی جدیدی را شروع کند. برای این کار بیشتر آمارها را به جای متد __init__(), در متد reset_stats() مقداردهی اوّلیّه می‌کنیم تا آمارها هنگام ساخته شدن اوّلین نمونه‌ی GameStats درست تنظیم شوند (۱). در عین حال هر وقت که بازیکن بازی جدیدی را شروع کردیم می‌توانیم که reset_stats() را فراخوانی کنیم.

الان فقط یک آمار داریم، ships_left، که مقدار آن در طول بازی عوض خواهد شد. تعداد سفینه‌های بازیکن در شروع بازی باید در settings.py به عنوان ship_limit ذخیره شود:

1
2
3
# Ship settings
self.ship_speed = 1.5
self.ship_limit = 3

باید چند تغییر کوچک در alien_invasion.py انجام دهیم که نمونه‌ای از GameStats بسازیم. ابتدا عبارت‌های import را در بالای فایل به‌روز می‌کنیم:

1
2
3
4
5
6
7
8
import sys
from time import sleep
import pygame

from settings import Settings
from game_stats import GameStats
from ship import Ship
--snip--

تابع sleep() را از ماژول time کتابخانه‌ی استاندارد پایتون وارد می‌کنیم تا بتوانیم هنگام اصابت به سفینه، بازی را برای چند لحظه متوقّف کرد. ما همچنین GameStats را هم وارد می‌کنیم.

نمونه‌ای از GameStats در __init__() می‌سازیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def __init__(self):
    --snip--
    self.screen = pygame.display.set_mode(
        (self.settings.screen_width, self.settings.screen_height))
    pygame.display.set_caption("Alien Invasion")

    # Create an instance to store game statistics.
    self.stats = GameStats(self)

    self.ship = Ship(self)
    --snip--

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

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

بیایید بخشی از این کد را در متدی جدید به نام _ship_hit() قرار دهیم. هنگامی که یک موجود بیگانه به سفینه برخورد می‌کند، این متد را از _update_aliens() صدا می‌زنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def _ship_hit(self):
    """Respond to the ship being hit by an alien."""
    # Decrement ships_left.
    self.stats.ships_left -= 1

    # Get rid of any remaining aliens and bullets.
    self.aliens.empty()
    self.bullets.empty()

    # Create a new fleet and center the ship.
    self._create_fleet()
    self.ship.center_ship()

    # Pause.
    sleep(0.5)

متد جدید _ship_hit() هنگام برخورد یک موجود بیگانه به سفینه پاسخ بازی را هماهنگ می‌کند. داخل _ship_hit() تعداد سفینه‌ها یکی کاهش می‌یابد (۱) و پس از آن گروه aliens و bullets را در ۲ خالی می‌کنیم.

سپس یک ناوگان جدید می‌سازیم و سفینه را به وسط برمی‌گردانیم (۳) (متد center_ship() را چند لحظه‌ی دیگر به Ship اضافه خواهیم کرد.) در ادامه بعد از به‌روزرسانی‌های انجام گرفته در تمام عنصرهای بازی وقفه‌ای ایجاد می‌کنیم تا بازیکن ببیند که سفینه او مورد اصابت قرار گرفته است (۴). فراخوانی sleep() اجرای برنامه را برای نیم ثانیه متوقّف می‌کند که برای اطّلاع یافتنِ کاربر از نابود شدن سفینه خود کافی است. پس از پایان کار تابع sleep() اجرای برنامه ادامه می‌یابد و متد _update_screen() ناوگانی جدید روی صفحه می‌کشد.

در _update_aliens() هنگام برخورد موجود بیگانه به سفینه، فراخوانی تابع print() را با _ship_hit() جایگزین می‌کنیم:

1
2
3
4
def _update_aliens(self):
    --snip--
    if pygame.sprite.spritecollideany(self.ship, self.aliens):
        self._ship_hit()

این متد جدید center_ship() است؛ آن را به انتهای ship.py اضافه کنید:

1
2
3
4
def center_ship(self):
    """Center the ship on the screen."""
    self.rect.midbottom = self.screen_rect.midbottom
    self.x = float(self.rect.x)

به همان روشی که در __init__() عمل کردیم سفینه را به وسط برمی‌گردانیم. پس از این کار مقدار مشخّصه self.x را از نو می‌نویسیم تا پیگیری دقیق موقعیّت سفینه ممکن شود.

نکته
توجّه کنید که ما هیچ‌گاه بیش از یک سفینه نمی‌سازیم؛ بلکه برای کلّ بازی تنها یک نمونه ایجاد می‌کنیم و هر گاه مورد اصابت قرار گرفت آن را به وسط برمی‌گردانیم. اطّلاعات آماری ships_left به ما می‌گوید که سفینه‌های بازیکن به پایان رسیده است.

بازی را اجرا کنید و چند بیگانه را هدف قرار دهید و اجازه دهید که یکی از آن‌ها به سفینه بخورد. باید در بازی وقفه‌ای ایجاد شود و سپس ناوگانی جدید پدیدار شود و سفینه دوباره در وسط-پایین صفحه قرار گیرد.

بیگانگانی که به پایین صفحه می‌رسند

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

1
2
3
4
5
6
7
8
def _check_aliens_bottom(self):
    """Check if any aliens have reached the bottom of the screen."""
    screen_rect = self.screen.get_rect()
    for alien in self.aliens.sprites():
        if alien.rect.bottom >= screen_rect.bottom:
            # Treat this the same as if the ship got hit.
            self._ship_hit()
            break

متد _check_aliens_bottom() بررسی می‌کند که آیا هیچ موجود بیگانه‌ای به انتهای صفحه رسیده یا نه. هنگامی یک بیگانه به انتهای صفحه می‌رسد که مقدار rect.bottom آن بزرگ‌تر یا مساوی مشخّصه‌ی rect.bottom صفحه شود (۱). اگر بیگانه‌ای به انتها رسید _ship_hit() را صدا می‌کنیم. اگر فقط یک موجود بیگانه هم به انتهای صفحه رسیده باشد نیازی به بررسی وضعّیت باقی بیگانگان نداریم پس بعد از فراخوانی _ship_hit() حلقه را می‌شکنیم.

این متد را از _update_aliens() صدا می‌کنیم:

1
2
3
4
5
6
7
def _update_aliens(self):
    --snip--
    # Look for alien-ship collisions.
    if pygame.sprite.spritecollideany(self.ship, self.aliens):
        self._ship_hit()
    # Look for aliens hitting the bottom of the screen.
    self._check_aliens_bottom()

متد _check_aliens_bottom() را پس از به‌روزرسانی موقعیّت‌های تمام موجودات بیگانه و همچنین پس از بررسی برخورد آن‌ها و سفینه صدا می‌کنیم (۲). اکنون با برخورد موجود بیگانه‌ای با سفینه یا رسیدن یکی از آن‌ها به پایین صفحه، ناوگانی جدید روی صفحه ظاهر می‌شود.

باختید!

اکنون هجوم بیگانگان کامل‌تر به نظر می‌رسد امّا بازی هرگز به پایان نمی‌رسد. مقدار ships_left منفی و منفی‌تر می‌شود. اکنون پرچم game_active را به عنوان مشخّصه‌ای به GameStats اضافه می‌کنیم تا با تمام شدن سفینه‌ها به بازی پایان دهد. این پرچم را در پایان متد __init__() در GameStats مقداردهی می‌کنیم:

1
2
3
4
def __init__(self, ai_game):
    --snip--
    # Start Alien Invasion in an active state.
    self.game_active = True

اکنون کدی به _ship_hit() اضافه می‌کنیم که با پایان یافتن تعداد سفینه‌های کاربر مقدار game_active را False کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def _ship_hit(self):
    """Respond to ship being hit by alien."""
    if self.stats.ships_left > 0:
        # Decrement ships_left.
        self.stats.ships_left -= 1
        --snip--
        # Pause.
        sleep(0.5)
    else:
        self.stats.game_active = False

بیشتر _ship_hit() بدون تغییر باقی مانده است. تمام کد موجود را داخل یک بلوک if برده‌ایم که بررسی می‌کند آیا بازیکن حدّاقل یک سفینه دارد یا نه. اگر چنین باشد ناوگان جدید ساخته می‌شود و بازی ادامه می‌یابد. اگر بازیکن سفینه دیگری نداشته باشد مقدار game_active را False می‌کنیم.

تشخیص زمان اجرای هر بخش بازی

ما باید تشخیص دهیم که کدام بخش بازی باید همیشه اجرا شود و کدام بخش‌ها تنها زمان فعّال بودن بازی اجرا می‌شوند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def run_game(self):
    """Start the main loop for the game."""
    while True:
        self._check_events()

        if self.stats.game_active:
            self.ship.update()
            self._update_bullets()
            self._update_aliens()

        self._update_screen()

باید در حلقه‌ی اصلی همیشه _check_events() را صدا کنیم حتّی اگر بازی غیرفعّال باشد. برای مثال ما همچنان نیاز داریم که بفهمیم کاربر Q را فشار داده تا به بازی خاتمه دهد یا کلید بستن پنجره را زده است. همچنین باید هنگام صبر کردن برای تصمیم بازیکن به شروع بازی جدید، صفحه را همچنان به‌روزرسانی کنیم. باقیِ تابع‌ها تنها زمانی فراخوانی می‌شوند که بازی فعّال باشد زیرا زمانی که بازی غیرفعّال است نیازی به به‌روزرسانی موقعیّت عناصر آن نداریم.

اکنون اگر هجوم بیگانگان را اجرا کنید، باید پس از تمام شدن سفینه‌ها بازی متوقّف شود.

خودآزمایی

۱۳-۶ پایان بازی: در تیرانداز جانبی، تعداد دفعات مورد اصابت قرار گرفتن سفینه و تعداد موجودات بیگانه‌ی نابودشده را ذخیره کنید. برای شرط پایان بازی تصمیم‌گیری کنید و بازی را به رخ دادن آن شرایط متوقّف کنید.

خلاصه

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

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

2.2 - فصل سیزدهم

2.2.1 - افزودن دکمه‌ی Play

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

در این بخش یک دکمه‌ی Play اضافه می‌کنیم که پیش از شروع بازی و پس از پایان یافتن آن پدیدار می‌شود تا بازیکن بتواند بازی را دوباره شروع کند.

فعلاً بازی به محض اجرای alien_invasion.py شروع می‌شود. اجازه دهید که بازی را در حالت غیرفعّال آغاز کنیم و به کاربر اعلام کنید که برای شروع بازی باید دکمه‌ای را فشار دهد. برای چنین کاری متد __init__() کلاس GameStats را تغییر می‌دهیم:

1
2
3
4
5
6
7
8
def __init__(self, ai_game):
    """Initialize statistics."""
    self.settings = ai_game.settings

    self.reset_stats()

    # Start game in an inactive state.
    self.game_active = False

حال بازی در حالت غیرفعّال شروع می‌شود و تا زمانی‌که دکمه‌ی Play را اضافه نکنیم بازیکن هیچ راهی برای شروع آن ندارد.

ساختن کلاس Button

از‌آن‌جایی‌که پای‌گیم روشی درون‌ساخته برای ساختن دکمه‌ها ندارد کلاس Button را می‌نویسیم تا مستطیلی توپر با یک عنوان درست کند. می‌توانید از این کد برای ساختن هر دکمه‌ای در یک بازی استفاده کنید. این بخش اوّل کلاس Button است، آن را به عنوان button.py ذخیره کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import pygame.font

class Button:
    def __init__(self, ai_game, msg):
        """Initialize button attributes."""
        self.screen = ai_game.screen
        self.screen_rect = self.screen.get_rect()

        # Set the dimensions and properties of the button.
        self.width, self.height = 200, 50
        self.button_color = (0, 255, 0)
        self.text_color = (255, 255, 255)
        self.font = pygame.font.SysFont(None, 48)

        # Build the button's rect object and center it.
        self.rect = pygame.Rect(0, 0, self.width, self.height)
        self.rect.center = self.screen_rect.center

        # The button message needs to be prepped only once.
        self._prep_msg(msg)

ابتدا ماژول pygame.font را وارد می‌کنیم که به پای‌گیم امکان نمایش متن را روی صفحه می‌دهد. متد __init__() پارامترهای self، شیء ai_game و msg را دریافت می‌کند که آخری متن روی دکمه را شامل می‌شود. ابعاد دکمه را تعیین می‌کنیم و button_color را برای تعیین رنگ شیء rect دکمه، سبز روشن قرار می‌دهیم و با تعیین text_color رنگ متن را سفید می‌کنیم.

یک مشخّصه‌ی font برای نمایش متن آماده می‌کنیم. نشان‌وند None به پای‌گیم می‌گوید که از فونت پیش‌فرض استفاده کند و 48 اندازه‌ی متن را معلوم می‌کند. یک rect برای دکمه می‌سازیم تا آن را وسط صفحه قرار دهیم و مشخّصه‌ی center آن را برابر با مشخّصه‌ی مشابه صفحه قرار می‌دهیم.

پای‌گیم برای نمایش متن‌های شما آن‌ها را به تصویر تبدیل می‌کند. متد _prep_msg() را به این منظور صدا می‌کنیم.

این کد _prep_msg() است:

1
2
3
4
5
6
def _prep_msg(self, msg):
    """Turn msg into a rendered image and center text on the button."""
    self.msg_image = self.font.render(msg, True, self.text_color,
        self.button_color)
    self.msg_image_rect = self.msg_image.get_rect()
    self.msg_image_rect.center = self.rect.center

متد _prep_msg() به پارامتر self و متنی برای تبدیل به تصویر (msg) نیاز دارد. فراخوانی font.render() متن ذخیره‌شده در msg را به تصویر تبدیل می‌کند که سپس آن را در self.msg_image ذخیره می‌کنیم. متد font.render() همچنین یک مقدار بولی را نیز می‌پذیرد تا خوش‌نماسازی را فعّال یا غیرفعّال کند. (خوش‌نماسازی لبه‌های حروف متن را هموارتر می‌کند.) باقی نشان‌وندها به رنگ متن و پس‌زمینه اختصاص دارد. ما خوش‌نماسازی را فعّال (True) و رنگ پس‌زمینه را مطابق رنگ پس‌زمینه‌ی دکمه قرار می‌دهیم. (اگر رنگی برای پس‌زمینه تعیین نکنید پای‌گیم تلاش خواهد کرد که متن را پس‌زمینه‌ی شفّاف نمایش دهد.)

یک rect از عکس ساخته و با تنظیم مشخّصه‌ی center آن برای تطبیق با همین مشخّصه‌ی دکمه، تصویرِ متن را وسطِ دکمه قرار می‌دهیم.

در نهایت متد draw_button() را می‌سازیم که با فراخوانی آن می‌توان دکمه‌ای را روی صفحه نمایش داد:

1
2
3
4
def draw_button(self):
    # Draw blank button and then draw message.
    self.screen.fill(self.button_color, self.rect)
    self.screen.blit(self.msg_image, self.msg_image_rect)

متد screen.fill() را برای رسم بخش مستطیلی دکمه صدا می‌کنیم. سپس screen.blit() را فرامی‌خوانیم تا تصویرِ متن را روی صفحه نشان دهد و یک تصویر و شیء rect مرتبط با تصویر را به آن می‌دهیم. این کار کلاس Button را تکمیل می‌کند.

2.2.2 - رسم دکمه روی صفحه و آغاز بازی

از کلاس Button برای ساختن دکمه‌ی Play در AlienInvasion استفاده می‌کنیم. ابتدا عبارت‌های import را به‌روز می‌کنیم:

1
2
3
4
--snip--

from game_stats import GameStats
from button import Button

چون فقط یک دکمه‌ی Play نیاز داریم آن را در متد __init__() کلاس AlienInvasion می‌سازیم. این کد را در پایان __init__() قرار می‌دهیم:

1
2
3
4
5
6
def __init__(self):
    --snip--
    self._create_fleet()

    # Make the Play button.
    self.play_button = Button(self, "Play")

این کد نمونه‌ای از Button با عنوان Play می‌سازد امّا این کار دکمه را روی تصویر رسم نمی‌کند. متد draw_button() را در _update_screen() صدا می‌کنیم:

1
2
3
4
5
6
7
8
9
def _update_screen(self):
    --snip--
    self.aliens.draw(self.screen)

    # Draw the play button if the game is inactive.
    if not self.stats.game_active:
        self.play_button.draw_button()

    pygame.display.flip()

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

اکنون اگر هجوم بیگانگان را اجرا کنید دکمه‌ی Play را وسط صفحه خواهید دید، مشابه شکل ۱۴-۱.

شکل ۱۴-۱

شکل ۱۴-۱: در زمان غیرفعّال بودن بازی دکمه‌ی Play پدیدار می‌شود.

آغازِ بازی

برای آغاز شدنِ بازی با فشرده شدن Play، بلوک elif زیر را به انتهای _check_events() اضافه کنید تا رخداد‌های ماوس را روی دکمه زیر نظر بگیرید:

1
2
3
4
5
6
7
8
9
def _check_events(self):
    """Respond to keypresses and mouse events."""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            --snip--

        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_pos = pygame.mouse.get_pos()
            self._check_play_button(mouse_pos)

اگر بازیکن روی هر جایی از صفحه کلیک کند پای‌گیم یک رخداد MOUSEBUTTONDOWN تشخیص می‌دهد ولی ما می‌خواهیم تنها زمانی که به این رخدادها پاسخ بدهد که بر روی دکمه‌ی Play رخ داده باشند. برای رسیدن به چنین هدفی از pygame.mouse.get_pos() استفاده می‌کنیم که با فشرده شدن دکمه‌ی ماوس، یک چندتایی شامل مختصّات x و y نشانگر آن برمی‌گرداند. این مقدارها را به متد جدید _check_play_button() می‌فرستیم.

این کد _check_play_button() است که ترجیح دادم آن را بعد از _check_events() بیاورم:

1
2
3
4
def _check_play_button(self, mouse_pos):
    """Start a new game when the player clicks Play."""
    if self.play_button.rect.collidepoint(mouse_pos):
        self.stats.game_active = True

برای بررسی تلاقی محل کلیک شدن ماوس و محدوده‌ی تعریف‌شده در rect دکمه‌ی Play از متد collidepoint() در rect استفاده می‌کنیم. اگر چنین اتّفاقی افتاده باشد مقدار game_active را True می‌کنیم و بازی آغاز می‌شود!

تا اینجا باید قادر باشید که یک بازی کامل را شروع کنید. با پایان یافتنِ بازی مقدار game_active باید False شود و دکمه‌ی Play دوباره پدیدار شود.

2.2.3 - بازنشانی بازی و مدیریت دکمه

دکمه‌ی Play که اکنون نوشته‌اید تنها اوّلین بازی که بازیکن آن را فشار می‌دهد کار می‌کند. ولی پس از پایان اوّلین دور بازی دیگر کار نمی‌کند زیرا شرط‌هایی که موجب پایان بازی شده به مقدار اوّلیّه‌ی خود برنگشته‌اند.

برای بازنشانی بازی پس از هر بار فشرده شدن دکمه، باید آمار بازی را صفر کنیم، گلوله‌ها و موجودات بیگانه‌ی قدیمی را پاک کنیم، ناوگان جدیدی بسازیم و سفینه را به وسط برگردانیم، چنانکه اینجا می‌بینیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def _check_play_button(self, mouse_pos):
    """Start a new game when the player clicks Play."""
    if self.play_button.rect.collidepoint(mouse_pos):
        # Reset the game statistics.
        self.stats.reset_stats()

        self.stats.game_active = True

        # Get rid of any remaining aliens and bullets.
        self.aliens.empty()
        self.bullets.empty()

        # Create a new fleet and center the ship.
        self._create_fleet()
        self.ship.center_ship()

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

حال بازی پس از هر بار فشرده شدن دکمه‌ی Play بازنشانی می‌شود و شما امکان دارید که هر چند بار که بخواهید بازی کنید!

غیرفعّال کردن دکمه‌ی Play

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

برای رفع این مشکل کاری می‌کنیم که بازی تنها هنگام False بودن game_active شروع شود:

1
2
3
4
5
6
7
8
def _check_play_button(self, mouse_pos):
    """Start a new game when the player clicks Play."""
    button_clicked = self.play_button.rect.collidepoint(mouse_pos)

    if button_clicked and not self.stats.game_active:
        # Reset the game statistics.
        self.stats.reset_stats()
        --snip--

پرچم button_clicked مقدار True یا False نگه می‌دارد و بازی تنها زمانی دوباره شروع خواهد شد که دکمه‌ی Play زده شده باشد و بازی در حال حاضر در حالت فعّال نباشد. برای آزمودن این رفتار بازی جدیدی شروع کنید و پشت سر هم روی محلّی که دکمه‌ی Play باید در آن‌جا باشد کلیک کنید. اگر همه‌چیز مطابق انتظار باشد کلیک کردن روی این ناحیه نباید تاثیری بر روند بازی داشته باشد.

پنهان کردن نشانگر ماوس

نمایان بودن نشانگر ماوس برای شروع بازی مطلوب ماست امّا با آغاز بازی این نشانگر جلوی دست و پا را می‌گیرد. برای حل این مشکل با فعّال شدن بازی نشانگر را غیرفعال می‌کنیم. این کار را می‌توانیم در پایان بلوک if در _check_play_button() انجام دهیم:

1
2
3
4
5
6
7
8
9
def _check_play_button(self, mouse_pos):
    """Start a new game when the player clicks Play."""
    button_clicked = self.play_button.rect.collidepoint(mouse_pos)

    if button_clicked and not self.stats.game_active:
        --snip--

        # Hide the mouse cursor.
        pygame.mouse.set_visible(False)

set_visible() با دریافت False به پای‌گیم می‌گوید که نشانگر ماوس را هنگام حرکت روی پنجره‌ی بازی پنهان کند.

پس از پایان یافتن بازی نشانگر را مجدّداً نمایش می‌دهیم تا بازیکن بتواند با زدنِ دوباره‌ی Play بازی جدیدی را شروع کند. این کدی است که این کار را انجام می‌دهد:

1
2
3
4
5
6
7
8
def _ship_hit(self):
    """Respond to ship being hit by alien."""
    if self.stats.ships_left > 0:
        --snip--

    else:
        self.stats.game_active = False
        pygame.mouse.set_visible(True)

به محض غیرفعّال شدن بازی نشانگر را دوباره نشان می‌دهیم. این کار در _ship_hit() رخ می‌دهد. پرداختن به چنین جزئیاتی باعث می‌شود بازی شما خیلی حرفه‌ای به نظر بیاید و به بازیکنان امکان می‌دهد به جای فکر کردن به رابط کاربری روی بازی کردن تمرکز کنند.

2.2.4 - خودآزمایی و بالا بردن سطح بازی

خودآزمایی

۱۴-۱- فشردن P برای شروع بازی: از آن‌جایی که هجوم بیگانگان از ورودی صفحه‌کلید برای هدایت سفینه استفاده می‌کند، ایجاد امکان آغاز بازی با فشردن یک کلید سودمند خواهد بود. کدی اضافه کنید که به کاربر اجازه دهد بازی را فشار دادن P شروع کند. جابه‌جا کردن کدهایی از _check_play_button() به متد _start_game() ممکن است مفید باشد تا بتوان آن‌ها را از _check_play_button() و _check_keydown_events() فراخواند.

۱۴-۲- تمرین هدف‌گیری: در کناره‌ی راست صفحه مستطیلی بسازید که با سرعت ثابت بالا و پایین می‌رود. سپس یک سفینه سمت چپ تصویر ظاهر کنید که بازیکن بتواند آن را بالا و پایین کند و به مستطیل متحرّک شلّیک کند. یک دکمه‌ی Play برای شروع بازی اضافه کنید و زمانی که تیر بازیکن سه بار خطا رفت بازی را تمام کنید و دکمه را دوباره نمایش دهید. به بازیکن اجازه دهید که بازی را با دکمه‌ی Play دوباره شروع کند.

بالا بردن سطحِ بازی

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

تغییر دادن تنظیمات سرعت

ابتدا ساختار کلاس Settings را دوباره طوری مرتّب می‌کنیم که تنظیمات بازی به دو بخش ثابت و متغیّر تقسیم شوند. همچنین مطمئن می‌شویم که تنظیمات متغیّر با آغاز یک بازی جدید بازنشانی می‌شوند. این متد __init__() برای settings.py است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def __init__(self):
    """Initialize the game's static settings."""
    # Screen settings
    self.screen_width = 1200
    self.screen_height = 800
    self.bg_color = (230, 230, 230)

    # Ship settings
    self.ship_limit = 3

    # Bullet settings
    self.bullet_width = 3
    self.bullet_height = 15
    self.bullet_color = 60, 60, 60
    self.bullets_allowed = 3

    # Alien settings
    self.fleet_drop_speed = 10

    # How quickly the game speeds up
    self.speedup_scale = 1.1

    self.initialize_dynamic_settings()

مقداردهی اوّلیّه‌ی تنظیمات ثابت را مانند قبل در متد __init__() انجام می‌دهیم. تنظیمی به نام speedup_scale اضافه می‌کنیم که میزان سریع شدن بازی را کنترل می‌کند: مقدار 2 سرعت بازی را با بالا رفتن مرحله‌ی بازی دوبرابر می‌کند؛ مقدار 1 سرعت بازی را ثابت نگه می‌دارد. مقداری مثل 1.1 سرعت بازی را به اندازه‌ای افزایش می‌دهد که بازی چالشی باشد امّا غیرممکن نشود. سرانجام متد initialize_dynamic_settings() را فرامی‌خوانیم تا مشخّصه‌هایی را که باید در طول برنامه تغییر کنند مقداردهی اوّلیّه کنیم.

این کد متد initialize_dynamic_settings() است:

1
2
3
4
5
6
7
8
def initialize_dynamic_settings(self):
    """Initialize settings that change throughout the game."""
    self.ship_speed = 1.5
    self.bullet_speed = 3.0
    self.alien_speed = 1.0

    # fleet_direction of 1 represents right; -1 represents left.
    self.fleet_direction = 1

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

برای این که با ورود بازیکن به مرحله‌ای جدید سرعت سفینه، گلوله‌ها و موجودات بیگانه را افزایش دهیم متد جدیدی به نام increase_speed() می‌نویسیم:

1
2
3
4
5
def increase_speed(self):
    """Increase speed settings."""
    self.ship_speed *= self.speedup_scale
    self.bullet_speed *= self.speedup_scale
    self.alien_speed *= self.speedup_scale

برای افزایش سرعت هر یک از این عناصر بازی سرعت آن‌ها را در speedup_scale ضرب می‌کنیم.

با منهدم شدن آخرین موجود بیگانه‌ی یک ناوگان ضرباهنگ بازی را با فراخوانی increase_speed() در _check_bullet_alien_collisions() افزایش می‌دهیم:

1
2
3
4
5
6
7
8
def _check_bullet_alien_collisions(self):
    --snip--

    if not self.aliens:
        # Destroy existing bullets and create new fleet.
        self.bullets.empty()
        self._create_fleet()
        self.settings.increase_speed()

تغییر مقدار تنظیمات سرعت ship_speed، alien_speed و bullet_speed برای سرعت بخشیدن به کلّ بازی کافی‌ست!

2.2.5 - بازنشانی سرعت و آغاز امتیازدهی

بازنشانی سرعت

پیش از این که بازیکن بازی جدیدی شروع کند باید تمام تنظیمات تغییریافته را به مقدار اوّلیّه خود برگردانیم؛ وگرنه بازی هر بار با سرعت افزایش‌یافته‌ی بازی قبلی شروع می‌شود:

1
2
3
4
5
6
7
8
def _check_play_button(self, mouse_pos):
    """Start a new game when the player clicks Play."""
    button_clicked = self.play_button.rect.collidepoint(mouse_pos)

    if button_clicked and not self.stats.game_active:
        # Reset the game settings.
        self.settings.initialize_dynamic_settings()
        --snip--

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

خودآزمایی

۱۴-۳- تمرین هدف‌گیری چالشی: کار خود را از تمرین ۱۴-۲ شروع کنید. حرکت هدف را با پیشرفت بازی سریع‌تر کنید و پس از فشرده شدن دکمه‌ی Play سرعت هدف را به مقدار اوّلیّه برگردانید.

۱۴-۴- سطح‌های سختی: مجموعه‌ای از دکمه‌ها برای هجوم بیگانگان بسازید که به بازیکن امکان دهد سختی مناسب را برای شروع بازی تنظیم کند. هر دکمه باید مقداری مناسب به مشخّصه‌های Settings بدهد تا سطح‌های مشکل بودن مختلف ایجاد کند.

امتیازدهی

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

امتیاز به آمار بازی مربوط‌است پس مشخّصه‌ی score را به GameStats اضافه می‌کنیم:

1
2
3
4
5
6
7
8
class GameStats:
    --snip--

    def reset_stats(self):
        """Initialize statistics that can change during the game."""
        self.ships_left = self.ai_settings.ship_limit

        self.score = 0

برای بازنشانی امتیاز پس از هر بار شروع یک بازی جدید، مقدار score را به جای __init__() در reset_stats() مقداردهی اوّلیّه می‌کنیم.

2.2.6 - نمایش امتیازها و ساختن تابلوی امتیازات

نمایش امتیازها

برای نمایش امتیازات روی صفحه ابتدا کلاسی به نام Scoreboard می‌سازیم. فعلاً این کلاس تنها امتیاز کنونی را نمایش می‌دهد امّا در نهایت از آن برای نمایش بیشترین امتیاز، سطح و تعداد سفینه‌های باقیمانده هم استفاده خواهیم کرد.

این اوّلین بخش کلاس است؛ آن را به نام scoreboard.py ذخیره کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import pygame.font

class Scoreboard:
    """A class to report scoring information."""

    def __init__(self, ai_game):
        """Initialize scorekeeping attributes."""
        self.screen = ai_game.screen
        self.screen_rect = self.screen.get_rect()
        self.settings = ai_game.settings
        self.stats = ai_game.stats

        # Font settings for scoring information.
        self.text_color = (30, 30, 30)
        self.font = pygame.font.SysFont(None, 48)

        # Prepare the initial score image.
        self.prep_score()

چون قرار است کلاس متن‌هایی را روی صفحه بنویسد کار را به وارد کردن ماژول pygame.font شروع می‌کنیم. سپس به متد __init__() پارامتر ai_game را می‌دهیم تا به شیءهای settings، screen و stats دسترسی داشته باشد و به کمک آن‌ها مقدارهای مورد نظر ما را گزارش کند. سپس رنگ متن را تعیین کرده و نمونه‌ای از شیء فونت می‌سازیم.

متنی را که قرار است نمایش داده شود با فراخوانی prep_score() به تصویر تبدیل می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def prep_score(self):
    """Turn the score into a rendered image."""
    score_str = str(self.stats.score)
    self.score_image = self.font.render(score_str, True,
        self.text_color, self.settings.bg_color)

    # Display the score at the top right of the screen.
    self.score_rect = self.score_image.get_rect()
    self.score_rect.right = self.screen_rect.right - 20
    self.score_rect.top = 20

در prep_score() مقدار عددی stats.score را به رشته تبدیل می‌کنیم، سپس این رشته را به render() می‌دهیم که آن را به تصویر تبدیل می‌کند. برای نمایش واضح امتیاز روی صفحه رنگ پس‌زمینه و رنگ متن را به render() می‌دهیم.

امتیاز را در گوشه‌ی بالا-راست صفحه قرار می‌دهیم و با بیشتر شدن آن و افزایش عرض عدد، آن را به سمت چپ گسترش می‌دهیم. برای این مطمئن باشیم که امتیاز با کناره‌ی راست صفحه هم‌تراز است، یک rect به نام score_rect می‌سازیم و حاشیه‌ی راست آن را در فاصله‌ی ۲۰ پیکسلی کناره‌ی راست صفحه قرار می‌دهیم. سپس لبه‌ی بالای آن را ۲۰ پیکسل پایین‌تر از کناره‌ی بالایی صفحه تنظیم می‌کنیم.

سپس متدی به نام show_score() می‌سازیم تا تصویر تولیدشده‌ی امتیاز را نشان دهد:

1
2
3
def show_score(self):
    """Draw score to the screen."""
    self.screen.blit(self.score_image, self.score_rect)

این متد تصویر امتیاز را در موقعیّتی که score_rect تعیین می‌کند، روی صفحه رسم می‌کند.

ساختن تابلوی امتیازات

برای نمایش امتیاز در AlienInvasion نمونه‌ای از Scoreboard می‌سازیم. ابتدا عبارت‌های import را به‌روز می‌کنیم:

1
2
3
4
--snip--

from game_stats import GameStats
from scoreboard import Scoreboard

سپس نمونه‌ای از Scoreboard در __init__() می‌سازیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def __init__(self):
    --snip--
    pygame.display.set_caption("Alien Invasion")

    # Create an instance to store game statistics,
    # and create a scoreboard.
    self.stats = GameStats(self)
    self.sb = Scoreboard(self)

    --snip--

سپس در _update_screen() تابلوی امتیازات را روی صفحه نمایش می‌دهیم:

1
2
3
4
5
6
7
8
9
def _update_screen(self):
    --snip--
    self.aliens.draw(self.screen)

    # Draw the score information.
    self.sb.show_score()

    # Draw the play button if the game is inactive.
    --snip--

متد show_score() را درست پیش از رسم دکمه‌ی Play می‌کشیم. اگر اکنون هجوم بیگانگان را اجرا کنید عدد صفر باید در گوشه‌ی بالا سمت راست صفحه نمایان شود. (تا اینجا پیش از توسعه‌ی سیستم امتیازدهی فقط مطمئن می‌شویم که امتیاز به درستی روی صفحه نمایش داده می‌شود.) شکل ۱۴-۲ امتیاز را هنگام نمایش پیش از شروع بازی نشان می‌دهد.

شکل ۱۴-۲

شکل ۱۴-۲: امتیاز در گوشه‌ی بالا راست صفحه نمایان می‌شود.

در ادامه به هر موجود بیگانه امتیازی نسبت می‌دهیم!

2.2.7 - به‌روزرسانی و بازنشانی امتیاز

به‌روزرسانی امتیاز با نابودی موجودات بیگانه

برای نمایش زنده‌ی امتیاز روی صفحه مقدار stats.score را با نابودی هر موجود بیگانه به‌روزرسانی می‌کنیم و سپس prep_score() را برای به‌روزرسانی تصویرِ امتیاز صدا می‌کنیم. امّا ابتدا اجازه امتیازی را که هر بازیکن با زدن هر موجود بیگانه کسب می‌کند تعیین کنیم:

1
2
3
4
5
def initialize_dynamic_settings(self):
    --snip--

    # Scoring
    self.alien_points = 50

با پیشرفت بازی امتیاز هر موجود بیگانه را افزایش خواهیم داد. برای اطمینان از بازنشانی این امتیاز با شروع دوباره‌ی بازی، مقدار آن را در initialize_dynamic_settings() تعیین می‌کنیم.

با هر بار نابود شدن یک موجود بیگانه امتیاز را در _check_bullet_alien_collisions() به‌روز می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def _check_bullet_alien_collisions(self):
    """Respond to bullet-alien collisions."""
    # Remove any bullets and aliens that have collided.
    collisions = pygame.sprite.groupcollide(
            self.bullets, self.aliens, True, True)

    if collisions:
        self.stats.score += self.settings.alien_points
        self.sb.prep_score()

    --snip--

زمانی که گلوله‌ای به یک موجود بیگانه اصابت می‌کند پای‌گیم دیکشنری collisions را برمی‌گرداند. ما وجود این دیکشنری را بررسی می‌کنیم و در صورت وجود مقدار ارزش موجود بیگانه را به امتیاز اضافه می‌کنیم. سپس prep_score() را برای ساختن تصویر جدیدی از امتیاز به‌روزشده صدا می‌کنیم.

اکنون هنگامی که هجوم بیگانگان را بازی می‌کنید باید بتوانید امتیاز خود را حساب کنید!

بازنشانی امتیاز

تا اینجا ما امتیاز جدید را پس از نابودی یک موجود بیگانه نمایش می‌دهیم که در بیشتر قسمت‌های بازی همین‌طور باید باشد. امّا در آغاز یک بازی جدید و تا پیش از این که اوّلین موجود بیگانه کشته شود، همچنان امتیاز قدیمی را می‌بینیم.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def _check_play_button(self, mouse_pos):
    --snip--

    if button_clicked and not self.stats.game_active:
        --snip--

        # Reset the game statistics.
        self.stats.reset_stats()

        self.stats.game_active = True

        self.sb.prep_score()

    --snip--

پس از بازنشانی آمارهای بازی هنگام شروع یک بازی جدید متد prep_score() را فرامی‌خوانیم. این کار امتیاز ۰ را روی تابلوی امتیازات نمایش می‌دهد.

اطمینان یافتن از ثبت امتیاز تمام شلّیک‌های موفّق

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

در _check_bullet_alien_collisions() هر گلوله‌ای که به یک موجود بیگانه برخورد کند به کلیدی در دیکشنری collisions تبدیل خواهد شد. مقدار مرتبط با هر کلید لیستی از موجودات بیگانه است که گلوله به آن‌ها اصابت کرده است. در مقدارهای دیکشنری collisions حلقه می‌زنیم تا مطمئن شویم که به کشتنِ هر موجود بیگانه امتیازی اختصاص داده‌ایم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def _check_bullet_alien_collisions(self):
    --snip--

    if collisions:
        for aliens in collisions.values():
            self.stats.score += self.settings.alien_points * len(aliens)

        self.sb.prep_score()

    --snip--

اگر دیکشنری collisions تعریف شده باشد در تمام مقدارهای آن حلقه می‌زنیم. به یاد داریم که هر مقدار لیستی از موجودات بیگانه‌ای است که با یک گلوله کشته شده‌اند. ارزش هر کدام از موجودات بیگانه را در تعداد آن‌ها در هر لیست ضرب کرده و این مقدار را به امتیاز فعلی اضافه می‌کنیم. برای آزمایش درستی این کد عرض هر گلوله را به ۳۰۰ پیکسل افزایش دهید و امتیازهای کسب‌شده برای کشتن هر موجود بیگانه را با این گلوله‌ی عریض بررسی کنید؛ سپس عرض گلوله را به مقدار اوّلیّه‌ی خود برگردانید.

افزایش مقدار امتیازها

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Settings:
    """A class to store all settings for Alien Invasion."""

    def __init__(self):
        --snip--

        # How quickly the game speeds up
        self.speedup_scale = 1.1

        self.initialize_dynamic_settings()

    def initialize_dynamic_settings(self):
        --snip--

    def increase_speed(self):
        """Increase speed settings and alien point values."""
        self.ship_speed *= self.speedup_scale
        self.bullet_speed *= self.speedup_scale
        self.alien_speed *= self.speedup_scale

        self.alien_points = int(self.alien_points * self.score_scale)

نرخی به نام score_scale تعریف می‌کنیم که امتیازها بر اساس آن افزایش می‌یابد. افزایش کوچکی در سرعت (1.1) بازی را به سرعت چالشی می‌کند. امّا برای ایجاد تغییر قابل توجّه‌تری در امتیازدهی باید ارزش امتیاز هر موجود بیگانه را با مقدار بزرگتری (1.5) تغییر دهیم. حال همزمان با افزایش سرعت بازی ارزش امتیاز هر شلّیک موفّق را هم افزایش می‌دهیم. از تابع int() برای افزایش ارزش امتیاز بر اساس یک عدد صحیح استفاده می‌کنیم.

برای دیدن ارزش امتیاز کشتن هر موجود بیگانه یک print() به متد increase_speed() در Settings اضافه می‌کنیم:

1
2
3
4
5
6
def increase_speed(self):
    --snip--

    self.alien_points = int(self.alien_points * self.score_scale)

    print(self.alien_points)
نکته
حتماً پس از بررسی صحّت افزایش ارزش امتیاز فراخوانی print() را حذف کنید زیرا ممکن است بر عملکرد بازی تاثیر بگذارد و حواس بازیکن را پرت کند.

2.2.8 - گرد کردن امتیاز و بالاترین امتیاز

گرد کردن امتیاز

در بیشتر بازی‌های شبه‌آرکید امتیازات به صورت مضرب‌هایی از ۱۰ گزارش می‌شوند بنابراین ما نیز همین رویّه را پی می‌گیریم. علاوه بر این به امتیازهای بزرگ جداکننده‌ی کاما اضافه می‌کنیم. این تغییر را در Scoreboard اضافه می‌کنیم:

1
2
3
4
5
6
7
8
def prep_score(self):
    """Turn the score into a rendered image."""
    rounded_score = round(self.stats.score, -1)
    score_str = "{:,}".format(rounded_score)
    self.score_image = self.font.render(score_str, True,
        self.text_color, self.settings.bg_color)

    --snip--

تابع round() معمولاً یک عدد اعشاری را تا تعداد رقمی که در نشان‌وند دوم آن تعیین شده گرد می‌کند. امّا اگر عددی منفی را به عنوان نشان‌وند دوم وارد کنیم این تابع عدد را به نزدیک‌ترین ۱۰، ۱۰۰، ۱۰۰۰ و … گرد می‌کند. کد به پایتون می‌گوید که مقدار stats.score را به نزدیک‌ترین مضرب ۱۰ گرد و در rounded_score ذخیره کن.

از یک راهنمای شکل‌دهی رشته استفاده می‌کنیم تا پایتون هنگام تبدیل عدد به رشته، آن را سه رقم سه رقم با کاما جدا کند تا مثلاً به جای 1000000، 1,000,000 را نمایش دهد. حال اگر بازی را اجرا کنید امتیاز را به شکلی مرتّب و همچنین گردشده مشاهده خواهید کرد حتّی اگر امتیازهای خیلی زیادی کسب کرده باشید، چنانکه در شکل ۱۴-۳ دیده می‌شود.

شکل ۱۴-۳

شکل ۱۴-۳: امتیاز گردشده با جداکننده‌ی کاما

بالاترین امتیاز

هر بازیکنی دوست دارد که بالاترین رکورد بازی را بشکند بنابراین باید بالاترین امتیاز کسب‌شده را ذخیره و گزارش کنیم تا بازیکنان انگیزه‌ای برای پیشرفت در بازی داشته باشند. بیشترین امتیازها را در GameStats ذخیره می‌کنیم:

1
2
3
4
5
def __init__(self, ai_game):
    --snip--

    # High score should never be reset.
    self.high_score = 0

از آن‌جایی‌که بیشترین امتیاز هیچ‌گاه نباید بازنشانی شود آن را به جای reset_stats() در __init__() مقداردهی اوّلیّه می‌کنیم.

سپس Scoreboard را طوری تغییر می‌دهیم که بالاترین امتیاز را نشان دهد. از متد __init__() شروع می‌کنیم:

1
2
3
4
5
6
def __init__(self, ai_game):
    --snip--

    # Prepare the initial score images.
    self.prep_score()
    self.prep_high_score()

بیشترین امتیاز را جدا از امتیاز نمایش می‌دهیم پس به متدی جدید، prep_high_score() احتیاج داریم تا تصویر بیشترین امتیاز را آماده کند.

این متد prep_high_score() است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def prep_high_score(self):
    """Turn the high score into a rendered image."""
    high_score = round(self.stats.high_score, -1)
    high_score_str = "{:,}".format(high_score)
    self.high_score_image = self.font.render(high_score_str, True,
        self.text_color, self.settings.bg_color)

    # Center the high score at the top of the screen.
    self.high_score_rect = self.high_score_image.get_rect()
    self.high_score_rect.centerx = self.screen_rect.centerx
    self.high_score_rect.top = self.score_rect.top

بیشترین امتیاز را به نزدیک‌ترین مضرب ۱۰ گرد کرده و آن را با کاما جدا می‌کنیم. سپس تصویری از بیشترین امتیاز می‌سازیم، rect آن را افقی وسط‌چین می‌کنیم و مشخّصه‌ی top آن را با بالای تصویر امتیازها هم‌تراز می‌کنیم.

اکنون متد show_score() امتیاز فعلی را در بالا-راست و بیشترین امتیاز را در بالا-وسط صفحه نشان می‌دهد:

1
2
3
4
def show_score(self):
    """Draw score to the screen."""
    self.screen.blit(self.score_image, self.score_rect)
    self.screen.blit(self.high_score_image, self.high_score_rect)

برای بررسی بیشترین امتیازها متد جدیدی به نام check_high_score() در Scoreboard می‌نویسیم:

1
2
3
4
5
def check_high_score(self):
    """Check to see if there's a new high score."""
    if self.stats.score > self.stats.high_score:
        self.stats.high_score = self.stats.score
        self.prep_high_score()

متد check_high_score() امتیاز فعلی را با بالاترین امتیاز مقایسه می‌کند. اگر بیشتر باشد مقدار high_score را به‌روز می‌کنیم و برای به‌روزرسانی عکس بالاترین امتیاز prep_high_score() را صدا می‌کنیم.

باید متد check_high_score() را پس از مورد اصابت قرار گرفتن هر موجود بیگانه و پس از به‌روزرسانی امتیاز در _check_bullet_alien_collisions() به‌روز کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def _check_bullet_alien_collisions(self):
    --snip--

    if collisions:
        for aliens in collisions.values():
            self.stats.score += self.settings.alien_points * len(aliens)

        self.sb.prep_score()

        self.sb.check_high_score()

    --snip--

متد check_high_score() را زمانی فرامی‌خوانیم که دیکشنری collisions موجود باشد و این کار را پس از به‌روزرسانی امتیاز مربوط به کشتن تمام موجودات بیگانه انجام می‌دهیم.

اوّلین بازی که هجوم بیگانگان را بازی می‌کنیم امتیاز شما بالاترین امتیاز خواهد بود بنابراین به عنوان امتیاز فعلی و بالاترین امتیاز نمایش داده خواهد شد. ولی زمانی که بازی دیگری را شروع کنید بالاترین امتیاز در میانه‌ی صفحه و امتیاز فعلی‌تان در سمت راست دیده خواهید شد چنانکه در شکل ۱۴-۴ می‌بینیم.

شکل ۱۴-۴

شکل ۱۴-۴: بالاترین امتیاز در وسط-بالای صفحه نمایش داده می‌شود.

2.2.9 - نمایش مرحله و تعداد سفینه‌ها

نمایش دادن مرحله

برای نمایش مرحله‌ای از بازی که بازیکن در آن قرار دارد، باید ابتدا شاخصه‌ای به GameStats اضافه کنیم که بیانگر مرحله‌ی فعلی باشد. برای بازنشانی مرحله در ابتدای هر بازی جدید آن را در reset_stats() مقداردهی اوّلیّه می‌کنیم:

1
2
3
4
5
6
def reset_stats(self):
    """Initialize statistics that can change during the game."""
    self.ships_left = self.settings.ship_limit

    self.score = 0
    self.level = 1

برای این که Scoreboard مرحله‌ی فعلی را نمایش دهد تابعی جدید را به نام prep_level() از __init__() صدا می‌کنیم:

1
2
3
4
5
def __init__(self, ai_game):
    --snip--

    self.prep_high_score()
    self.prep_level()

این متد prep_level() است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def prep_level(self):
    """Turn the level into a rendered image."""
    level_str = str(self.stats.level)
    self.level_image = self.font.render(level_str, True,
        self.text_color, self.settings.bg_color)

    # Position the level below the score.
    self.level_rect = self.level_image.get_rect()
    self.level_rect.right = self.score_rect.right
    self.level_rect.top = self.score_rect.bottom + 10

متد prep_level() تصویری از مقدار ذخیره‌شده در stats.level می‌سازد و مقدار مشخّصه‌ی right آن را متناسب با مشخّصه‌ی right امتیازها تعیین می‌کند. سپس مشخّصه‌ی top تصویر را ۱۰ پیکسل پایین‌تر از انتهای تصویر امتیاز قرار می‌دهد تا بین امتیاز و مرحله فاصله‌ای بیفتد.

باید show_score() را هم به‌روز کنیم:

1
2
3
4
5
def show_score(self):
    """Draw scores and level to the screen."""
    self.screen.blit(self.score_image, self.score_rect)
    self.screen.blit(self.high_score_image, self.high_score_rect)
    self.screen.blit(self.level_image, self.level_rect)

خطّی که به تازگی اضافه شده تصویر مرحله را روی صفحه نشان می‌دهد.

در _check_bullet_alien_collisions() مقدار stats.level را افزایش داده و تصویر مرحله را به‌روز می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def _check_bullet_alien_collisions(self):
    --snip--

    if not self.aliens:
        # Destroy existing bullets and create new fleet.
        self.bullets.empty()
        self._create_fleet()
        self.settings.increase_speed()

        # Increase level.
        self.stats.level += 1
        self.sb.prep_level()

اگر ناوگانی نابود شود مقدار stats.level را افزایش می‌دهیم و prep_level() را فرامی‌خوانیم تا مطمئن شویم که مرحله‌ی جدید به درستی نمایش داده می‌شود.

برای اطمینان از به‌روزرسانی درست تصویر مرحله در آغاز یک بازی جدید، هنگامی که بازیکن دکمه‌ی Play را فشار می‌دهد نیز یک بار متد prep_level() را صدا می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def _check_play_button(self, mouse_pos):
    --snip--

    if button_clicked and not self.stats.game_active:
        --snip--

        self.sb.prep_score()
        self.sb.prep_level()

    --snip--

prep_level() را درست پس از فراخوانی prep_score() صدا می‌کنیم.

حال تعداد مراحلی را که به پایان رسانده‌اید خواهید دید، مانند شکل ۱۴-۵.

شکل ۱۴-۵

شکل ۱۴-۵: مرحله‌ی فعلی درست زیر امتیاز فعلی نمایان می‌شود.

نکته
در برخی بازی‌های کلاسیک امتیازها برچسب دارند مانند امتیاز، بالاترین امتیاز و مرحله. چون معنای هر عدد به محض این که بازیکن بازی را شروع می‌کند معلوم می‌شود ما این برچسب‌ها را حذف کرده‌ایم. برای افزودن آن‌ها، آن‌ها را به رشته‌های مربوط به امتیازها درست پیش از فراخوانی font.render() در Scoreboard اضافه کنید.

نمایش تعداد سفینه‌ها

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

ابتدا باید کاری کنیم که Ship از Sprite ارث ببرد تا بتوانیم گروهی از سفینه‌ها بسازیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import pygame
from pygame.sprite import Sprite

class Ship(Sprite):
    """A class to manage the ship."""

    def __init__(self, ai_game):
        """Initialize the ship and set its starting position."""
        super().__init__()

        --snip--

اینجا Sprite را وارد می‌کنیم، مطمئن می‌شویم که Ship از Sprite ارث می‌برد و super() را در ابتدای __init__() فرامی‌خوانیم.

سپس باید Scoreboard را طوری تغییر دهیم تا گروهی از سفینه‌ها برای نمایش بسازد. این عبارت‌های import در Scoreboard است:

1
2
3
4
import pygame.font
from pygame.sprite import Group

from ship import Ship

چون گروهی از سفینه‌ها می‌سازیم Group و Ship را وارد می‌کنیم.

این __init__() است:

1
2
3
4
5
6
7
8
def __init__(self, ai_game):
    """Initialize scorekeeping attributes."""
    self.ai_game = ai_game
    self.screen = ai_game.screen
    --snip--

    self.prep_level()
    self.prep_ships()

نمونه‌ی بازی را به یک مشخّصه نسبت می‌دهیم زیرا آن را برای ساختن چند سفینه نیاز داریم. prep_ships() را پس از فراخوانی prep_level() صدا می‌کنیم.

این prep_ships() است:

1
2
3
4
5
6
7
8
9
def prep_ships(self):
    """Show how many ships are left."""
    self.ships = Group()

    for ship_number in range(self.stats.ships_left):
        ship = Ship(self.ai_game)
        ship.rect.x = 10 + ship_number * ship.rect.width
        ship.rect.y = 10
        self.ships.add(ship)

متد prep_ships() گروهی خالی به نام self.ships می‌سازد تا نمونه‌های سفینه را در آن نگه دارد. برای پر کردن این گروه حلقه‌ای یک بار برای هر کدام از سفینه‌های باقیمانده اجرا می‌شود. داخل حلقه سفینه جدیدی می‌سازیم و مقدار مختصّه‌ی x آن را طوری تنظیم می‌کنیم تا سفینه‌ها کنار یکدیگر و با یک حاشیه‌ی ۱۰ پیکسلی از سمت چپ گروه سفینه‌ها دیده شوند. مقدار مختصّه‌ی y را ده پیکسل پایین‌تر از بالای صفحه می‌گذاریم تا سفینه‌ها در گوشه‌ی بالا-چپ صفحه دیده شوند. سپس هر سفینه جدید را به گروه ships اضافه می‌کنیم.

حال باید سفینه‌ها را روی صفحه بکشیم:

1
2
3
4
5
6
def show_score(self):
    """Draw scores, level, and ships to the screen."""
    self.screen.blit(self.score_image, self.score_rect)
    self.screen.blit(self.high_score_image, self.high_score_rect)
    self.screen.blit(self.level_image, self.level_rect)
    self.ships.draw(self.screen)

برای نمایش سفینه‌ها روی صفحه draw() را روی هر گروه اعمال می‌کنیم و پای‌گیم هر سفینه را رسم می‌کند.

برای این که به بازیکن نشان دهیم که چند سفینه برای شروع بازی در اختیار دارد، prep_ships() را هنگام شروع یک بازی جدید فرامی‌خوانیم. این کار را در _check_play_button() در AlienInvasion انجام می‌دهیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def _check_play_button(self, mouse_pos):
    --snip--

    if button_clicked and not self.stats.game_active:
        --snip--

        self.sb.prep_score()
        self.sb.prep_level()
        self.sb.prep_ships()

    --snip--

این متد را هنگام مورد اصابت قرار گرفتن یک سفینه هم صدا می‌کنیم تا تصویر سفینه‌ها را هنگام از دست رفتن یک سفینه به‌روز کنیم:

1
2
3
4
5
6
7
8
9
def _ship_hit(self):
    """Respond to ship being hit by alien."""
    if self.stats.ships_left > 0:
        # Decrement ships_left, and update scoreboard.
        self.stats.ships_left -= 1

        self.sb.prep_ships()

    --snip--

متد prep_ships() را پس از کاهش دادن مقدار ships_left صدا می‌کنیم تا هنگام نابود شدن هر سفینه تعداد درست سفینه‌ها نمایش داده شود.

شکل ۱۴-۶ نظام امتیازدهی کامل را به همراه تعداد سفینه‌های باقیمانده در گوشه‌ی بالا-چپ تصویر نشان می‌دهد.

شکل ۱۴-۶

شکل ۱۴-۶: نظام کامل امتیازدهی برای هجوم بیگانگان

2.2.10 - خودآزمایی و خلاصه

خودآزمایی

۱۴-۵- بالاترین امتیاز تمام دوران: پس از هربار بسته شدن بازی و اجرای مجدّد آن، بالاترین امتیاز از دست می‌رود. این مشکل را با نوشتن بالاترین امتیاز در یک فایل پیش از فراخوانی sys.exit() و خواندن آن هنگام مقداردهی اوّلیّه‌ی آن در GameStats برطرف کنید.

۱۴-۶- بازآرایی: دنبال متدهایی بگردید که بیش از یک وظیفه را انجام می‌دهند و آن را برای سازمان‌دهی و بهینه‌تر کردن کد خود، بازآرایی کنید. برای مثال بخشی از کد _check_bullet_alien_collisions() را که با نابودی کامل یک ناوگان مرحله‌ی جدیدی را آغاز می‌کند، به تابعی به نام start_new_level() منتقل کنید. همچنین چند فراخوانی مجزّای موجود در __init__() در Scoreboard را به متد جدیدی به نام prep_images() منتقل کنید تا __init__() را کوتاه‌تر کنید. همچنین اگر متد _check_play_button() را قبلاً بازآرایی کرده باشید، متد prep_images() می‌تواند همچنین باعث ساده‌تر شدن متدهای _check_play_button() یا start_game() شود.

نکته
بیش از تلاش برای بازآرایی پروژه پیوست ت را ببینید تا یاد بگیرید که در صورت برخوردن به اشکال هنگام بازآرایی، چگونه پروژه را به یک حالت سالم برگردانید.

۱۴-۷- گسترش دادن بازی: به راهی برای گسترش دادن هجوم بیگانگان فکر کنید. برای مثال می‌توانید برنامه را جوری بنویسید که موجودات بیگانه امکان شلیک گلوله به سمت سفینه داشته باشند یا محافظ‌هایی برای سفینه‌ها اضافه کنید تا پشت آن‌ها پنهان شوند ولی با برخورد گلوله از هر طرف نابود شوند. یا از چیزی مثل ماژول pygame.mixer استفاده کنید تا صدای انفجار یا تیراندازی به بازی اضافه کند.

۱۴-۸- نسخه‌ی نهایی تیرانداز حاشیه‌ای: توسعه‌ی تیرانداز حاشیه‌ای را با هر آن‌چه در این پروژه انجام داده‌اید ادامه دهید. دکمه‌ی Play اضافه کنید، سرعت بازی را در امتیازهای خاصّی افزایش دهید و یک نظام امتیازدهی توسعه دهید. مطمئن شوید که همزمان با کار خود کد را بازآرایی کنید و به دنبال فرصت‌هایی بگردید تا بازی را فراتر از آن‌چه در این فصل نشان داده شده توسعه دهید.

خلاصه

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


2.3 - فصل چهاردهم

2.3.1 - مقدّمه و نصب Matplotlib

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

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

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

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

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

روش نصب Matplotlib

برای استفاده از Matplotlib در اوّلین مجموعه از بصری‌سازی‌های خود باید آن را به کمک pip نصب کنید. pip ماژولی برای دریافت و نصب بسته‌های پایتون است. فرمان زیر را در یک اعلان ترمینال وارد کنید:

1
$ python -m pip install --user matplotlib

این فرمان از پایتون می‌خواهد تا ماژول pip را اجرا کرده و بسته‌ی matplotlib را به مجموعه‌ی پایتون نصب‌شده برای کاربر فعلی اضافه کند. اگر شما در سیستم خود از فرمان دیگری به جز python، مانند python3، برای اجرای برنامه‌ها یا آغاز یک نشست ترمینال استفاده می‌کنید، دستور شما به شکل زیر خواهد بود:

1
$ python3 -m pip install --user matplotlib
نکته
این فرمان در macOS کار نمی‌کند، به جای آن همین فرمان را بدون پرچم –user امتحان کنید.

برای بررسی انواع بصری‌سازی قابل ایجاد با Matplotlib سری به گالری نمونه‌های آن در https://matplotlib.org/gallery/ بزنید. اگر روی هر کدام از بصری‌سازی‌های موجود در گالری کلیک کنید کدی که آن را ایجاد کرده خواهید دید.

2.3.2 - رسم یک نمودار خطّی ساده

بیایید به کمک Matplotlib یک نمودار خطّی ساده رسم کنیم و سپس آن را برای ساختن یک بصری‌سازی داده‌ی گویاتر شخصی‌سازی کنیم. از یک دنباله‌ی اعداد مربّع ۱، ۴، ۹، ۱۶ و ۲۵ به عنوان داده‌ی این نمودار استفاده می‌کنیم.

کافی است اعداد را به Matplotlib بدهید، چنان‌که اینجا می‌بینید و Matplotlib باقی کارها را برای شما خواهد کرد:

1
2
3
4
5
6
7
8
9
import matplotlib.pyplot as plt

squares = [1, 4, 9, 16, 25]

fig, ax = plt.subplots()

ax.plot(squares)

plt.show()

ابتدا ماژول pyplot را با نام مستعار plt وارد می‌کنیم تا مجبور نباشیم هر دفعه pyplot را بنویسیم. (این نامگذاری را به صورت یک قرارداد در مثال‌های آنلاین خواهید دید بنابراین ما هم از همین استفاده می‌کنیم.) ماژول پای‌پلات شامل تعدادی تابع برای تولید نمودار است.

لیستی به نام squares می‌سازیم تا داده‌ها را برای رسم ذخیره کند. سپس با فراخوانی تابع subplots در ۱ از یکی دیگر از قراردادهای Matplotlib پیروی می‌کنیم. این تابع یکی تا چند نمودار را در یک شکل واحد رسم می‌کند. متغیّر fig بیانگر شکل کلّی یا همان مجموعه‌ی نمودارهایی است که تولید می‌شوند. متغیّر ax یکی از نمودارهای داخل شکل را نشان می‌دهد و متغیّری است که بیشتر اوقات از آن استفاده می‌کنیم.

سپس از متد plot() بهره می‌گیریم که تلاش می‌کند داده‌ی ورودی را به روشی معنادار رسم کند. تابع plt.show() نمایشگر Matplotlib را باز می‌کند و نمودار را نشان می‌دهد. نمایشگر به شما امکان بزرگنمایی و پیمایش نمودار را می‌دهد و اگر روی آیکون ذخیره کلید کنید می‌توانید هر نموداری را که خواستید ذخیره کنید.

2.3.3 - تغییر دادن نوشته‌ی برچسب و ضخامت خط

هرچند نمودار قبلی افزایشی بودن اعداد را نشان می‌دهد امّا نوشته‌ی روی محورها کوچک و خط رسم‌شده کمی نازک است که باعث خوانایی پایین می‌شود. خوشبختانه Matplotlib به شما امکان می‌دهد که تمام ویژگی‌های یک بصری‌سازی را تغییر دهید.

ما تعداد کمی از تغییرات ممکن را اعمال می‌کنیم تا خوانایی نمودار را افزایش دهیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import matplotlib.pyplot as plt

squares = [1, 4, 9, 16, 25]

fig, ax = plt.subplots()

ax.plot(squares, linewidth=3)

# Set chart title and label axes.

ax.set_title("Square Numbers", fontsize=24)

ax.set_xlabel("Value", fontsize=14)

ax.set_ylabel("Square of Value", fontsize=14)

# Set size of tick labels.

ax.tick_params(axis='both', labelsize=14)

plt.show()

پارامتر linewidth در ۱ ضخامت خطی را تنظیم می‌کند که plot() تولید می‌کند. متد set_title() در ۲ عنوانی برای نمودار تعیین می‌کند. پارامترهای fontsize که چند بار در کد تکرار شده‌اند اندازه‌ی متن را در اجزای مختلف نمودار تغییر می‌دهد.

متدهای set_xlabel() و set_ylabel() به شما امکان می‌دهند که برای هر محور عنوانی تعیین کنید (۳) و متد tick_params() نشانه‌گذاری‌های روی محور را شکل می‌دهد (۴). نشان‌وندهایی که اینجا آمده بر روی هر دو محور تأثیر می‌گذارد(axis=‘both’) و اندازه‌ی برچسب این نشانه‌گذاری‌ها را ۱۴ قرار می‌دهد (labelsize=14).

خواندن نمودار حاصل بسیار راحت‌تر شده است. نوشته‌ی برچسب‌ها بزرگتر و خط نمودار ضخیم‌تر شده است. اغلب می‌ارزد که با این مقدارها بازی کنید تا به ایده‌ای کلّی درباره‌ی بهتر به نظر رسیدن نمودار نهایی برسید.

2.3.4 - اصلاح نمودار

حال که می‌توانیم نمودار را بهتر بخوانیم می‌بینیم که داده به درستی رسم نشده است. به انتهای نمودار توجّه کنید که مربّع 4.0، 25 نشان داده شده است! بیایید این مشکل را رفع کنیم.

زمانی که به plot() دنباله‌ای از اعداد می‌دهیم، فرض می‌کند که اوّلین داده متناظر با مقدار 0 در محور x است امّا اوّلین داده‌ی ما مربوط به مقدار ۱ در این محور است. می‌توانیم این رفتار پیش‌فرض را با دادن مقدارهای ورودی و خروجی به‌کاررفته برای محاسبه‌ی مربّعات به این متد تغییر دهیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import matplotlib.pyplot as plt

input_values = [1, 2, 3, 4, 5]

squares = [1, 4, 9, 16, 25]

fig, ax = plt.subplots()

ax.plot(input_values, squares, linewidth=3)

# Set chart title and label axes.

--snip--

اکنون plot() داده‌ها را درست رسم می‌کند زیرا مقدارهای ورودی و خروجی را به آن داده‌ایم و دیگر در مورد روش تولید خروجی‌ها فرضی نمی‌کند. نمودار حاصل درست است.

هنگام استفاده از plot() می‌توانید نشان‌وندهای متعدّدی را تعیین کنید و از تعداد زیادی تابع برای شخصی‌سازی نمودارهای خود بهره بگیرید. این تابع‌های شخصی‌سازی را هنگام کار کردن با مجموعه‌داده‌های جذّاب‌تر در طول فصل بیشتر بررسی خواهیم کرد.

2.3.5 - به‌کارگیری شیوه‌های پیش‌ساخته

Matplotlib تعداد شکل و شیوه‌ی ازپیش‌تعریف‌شده در اختیار می‌گذارد که تنظیمات اوّلیّه‌ی خوبی برای رنگ پس‌زمینه، خطوط شبکه‌بندی، ضخامت خط‌ها، فونت‌ها، اندازه‌ی فونت‌ها و بسیاری دیگر از ویژگی‌ها دارد که بصری‌سازی‌های شما را بدون نیاز به شخصی‌سازی خاصّی جذّاب جلوه می‌دهد. برای دیدن این شکل و شیوه‌ها در سیستم خود خط‌های زیر را در یک نشست ترمینال اجرا کنید:

1
2
3
4
>>> import matplotlib.pyplot as plt
>>> plt.style.available
['seaborn-dark', 'seaborn-darkgrid', 'seaborn-ticks', 'fivethirtyeight',
--snip--

برای به‌کارگیری هرکدام از این‌ها یک خط کد را پیش از آغاز ایجاد نمودار اضافه کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import matplotlib.pyplot as plt

input_values = [1, 2, 3, 4, 5]

squares = [1, 4, 9, 16, 25]

plt.style.use('seaborn')

fig, ax = plt.subplots()

--snip--

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

2.3.6 - رسم و شکل‌دهی نقاط با scatter()

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

برای رسم یک نقطه از متد scatter() استفاده کنید. مقدارهای (x, y) نقطه‌ی مورد نظر خود را به این متد بدهید تا آن‌ را رسم کند:

1
2
3
4
5
6
7
8
9
import matplotlib.pyplot as plt

plt.style.use('seaborn')

fig, ax = plt.subplots()

ax.scatter(2, 4)

plt.show()

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import matplotlib.pyplot as plt

plt.style.use('seaborn')

fig, ax = plt.subplots()

ax.scatter(2, 4, s=200)

# Set chart title and label axes.

ax.set_title("Square Numbers", fontsize=24)

ax.set_xlabel("Value", fontsize=14)

ax.set_ylabel("Square of Value", fontsize=14)

# Set size of tick labels.

ax.tick_params(axis='both', which='major', labelsize=14)

plt.show()

در ۱ scatter() را فرامی‌خوانیم و از نشان‌وند s برای تنظیم اندازه‌ی نقطه‌های به‌کاررفته برای رسم نمودار استفاده می‌کنیم. اگر اکنون scatter_squares.py را اجرا کنید باید نقطه‌ای تنها در وسط نمودار ببینید.

رسم مجموعه‌ای از نقاط با scatter()

برای رسم مجموعه‌ای از نقطه‌ها می‌توانیم لیستی جداگانه از مقدارهای x و y به آن بدهیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import matplotlib.pyplot as plt

x_values = [1, 2, 3, 4, 5]

y_values = [1, 4, 9, 16, 25]

plt.style.use('seaborn')

fig, ax = plt.subplots()

ax.scatter(x_values, y_values, s=100)

# Set chart title and label axes.

--snip--

لیست x_values شامل اعدادی است که باید مربّع شوند و y_values شامل مربّع هر عدد است. زمانی که این دو لیست را به scatter() می‌دهید، Matplotlib هنگام رسم هر نقطه از هر کدام یک مقدار را می‌خواند. نقطه‌هایی که باید رسم شوند (1,1)، (2,4)، (3,9)، (4, 16) و (5, 25) است.

محاسبه‌ی خودکار داده‌ها

نوشتن دستی لیست‌ها کارآمد نیست به ویژه زمانی که با داده‌های بسیار زیادی سر و کار داریم. به جای انتقال نقطه‌ها به شکل لیست، از یک حلقه استفاده می‌کنیم تا پایتون محاسبات را برای ما انجام دهد.

اینجا این روش را با وجود ۱۰۰۰ نقطه می‌بینیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import matplotlib.pyplot as plt

x_values = range(1, 1001)

y_values = [x**2 for x in x_values]

plt.style.use('seaborn')

fig, ax = plt.subplots()

ax.scatter(x_values, y_values, s=10)

# Set chart title and label axes.

--snip--

# Set the range for each axis.

ax.axis([0, 1100, 0, 1100000])

plt.show()

از گستره‌ی مقدارهای x شامل اعداد ۱ تا ۱۰۰۰ شروع می‌کنیم (۱). سپس به کمک روش دربرداری (for x in x_values) در مقدارهای x حلقه می‌زنیم، هر کدام را به توان ۲ می‌رسانیم (x**2) و نتیجه‌ها را در y_values ذخیره می‌کنیم. سپس لیست‌های ورودی و خروجی را به scatter() می‌دهیم (۲). چون تعداد داده‌ها در این مجموعه زیاد است اندازه‌ی نقطه‌ها را کوچک‌تر می‌کنیم.

در ۳ از متد axis() برای تعیین گستره‌ی هر محور استفاده می‌کنیم. این متد ۴ مقدار می‌پذیرد: کمینه و بیشینه‌ی مقدارهای محور x و y. اینجا محور x را بین ۰ و ۱۱۰۰ و محور y را بین ۰ و ۱،۱۰۰،۰۰۰ قرار می‌دهیم.

2.3.7 - رنگ‌آمیزی و ذخیره‌سازی نمودارها

تعریف کردن رنگ‌های دلخواه

برای تغییر رنگ نقطه‌ها c را به همران نام رنگ در علامت نقل قول به scatter() بدهید، مانند زیر:

1
ax.scatter(x_values, y_values, c='red', s=10)

علاوه بر این می‌توانید رنگ‌های دلخواه را به کمک مدل رنگی RGB هم تعیین کنید. برای این کار نشان‌وند c را به همراه یک چندتایی شامل سه مقدار اعشاری بین ۰ و ۱ (به ترتیب برای رنگ قرمز، سبز و آبی) وارد کنید. برای مثال خطّ زیر نموداری با نقطه‌های سبز کم‌رنگ ایجاد می‌کند:

1
ax.scatter(x_values, y_values, c=(0, 0.8, 0), s=10)

مقدارهای نزدیک‌تر به ۰ رنگ‌های تیره و مقدارهای نزدیک‌تر به ۱ رنگ‌های روشن‌تر تولید می‌کنند.

استفاده از نقشه‌ی رنگی

نقشه‌ی رنگی دنباله‌ای از رنگ‌های محوشونده است که از یک رنگ آغاز به رنگی ختم می‌شوند. کاربرد نقشه‌های رنگی در بصری‌سازی پررنگ کردن الگویی در داده‌هاست. برای مثال می‌توانید مقدارهای کم را با رنگ روشن و مقادیر زیاد را با رنگی تیره نمایش دهید.

ماژول pyplot مجموعه‌ای از نقشه‌های رنگی پیش‌ساخته دارد. برای استفاده کردن از آن‌ها باید مشخّص کنید که pyplot چگونه به هر نقطه‌ی مجموعه‌داده رنگی اختصاص دهد. اینجا روش اختصاص رنگ را بر اساس مقدار y نقطه می‌بینیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import matplotlib.pyplot as plt

x_values = range(1, 1001)

y_values = [x**2 for x in x_values]

ax.scatter(x_values, y_values, c=y_values, cmap=plt.cm.Blues, s=10)

# Set chart title and label axes.

--snip--

لیستی از مقدارهای y را به c نسبت می‌دهیم و سپس با نشان‌وند cmap به pyplot می‌گوییم که از کدام نقشه‌ی رنگی استفاده کند. این کد نقاطی را که مقدار y آن‌ها کمتر است با آبی روشن و آن‌هایی را که مقدار y بیشتری دارند با آبی تیره رنگ‌آمیزی می‌کند.

نکته
می‌توانید تمام نقشه‌های رنگی موجود در pyplot را در آدرس https://matplotlib.org ببینید؛ به قسمت Examples بروید، پایین بروید تا به Colors برسید و روی Colormap reference کلیک کنید.

ذخیره‌سازی خودکار نمودارها

اگر بخواهید که برنامه‌تان نمودار را به طور خودکار در فایلی ذخیره کند، می‌توانید فراخوانی plt.show() را با plt.savefig() جایگزین کنید:

1
plt.savefig('squares_plot.png', bbox_inches='tight')

نشانوند نخست نام فایلی است که برای تصویر نمودار انتخاب می‌کنید. این فایل در همان پوشه‌ی scatter_squares.py ذخیره خواهد شد. نشانوند دوم فاصله‌های خالی اضافی اطراف نمودار را حذف می‌کند. اگر می‌خواهید این فضای خالی باقی بماند کافی‌است این نشاوند را حذف کنید.

خودآزمایی

۱۵-۱- عددهای مکعب‌: توان سوم هر عدد را مکعّب آن می‌خوانند. ابتدا نمودار ۵ عدد مکعّب نخست را بکشید سپس این کار را برای ۵۰۰۰ عدد مکعّب نخست تکرار کنید.

۱۵-۲- اعداد مکعّب رنگی: یک نقشه‌ی رنگی روی نمودار اعداد مکعّب اعمال کنید.

2.3.8 - گردش تصادفی

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

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

ساختن کلاس RandomWalk()

برای ایجاد یک گردش تصادفی کلاس RandomWalk را ایجاد می‌کنیم که در مورد جهت حرکتی که باید انجام بگیرد تصمیم‌های تصادفی می‌گیرد. کلاس سه مشخّصه نیاز دارد: یک متغیّر برای ذخیره کردن تعداد نقطه‌ها در گردش و دو لیست برای ذخیره کردن مقدار مختصّات x و y هر نقطه‌ی حرکت.

تنها دو متد در کلاس RandomWalk مورد نیاز است: __init__() و fill_walk() که نقطه‌های حرکت را محاسبه می‌کند. از متد __init__() شروع می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from random import choice

class RandomWalk:
    """A class to generate random walks."""

    def __init__(self, num_points=5000):
        """Initialize attributes of a walk."""
        self.num_points = num_points

        # All walks start at (0, 0).
        self.x_values = [0]
        self.y_values = [0]

برای گرفتن تصمیم‌های تصادفی حرکت‌های ممکن را در لیستی ذخیره می‌کنیم و سپس از تابع choice() از ماژول random استفاده می‌کنیم تا تصمیم بگیریم که در هر گام کدام حرکت را انجام دهیم (۱). سپس تعداد پیش‌فرض نقطه‌ها را در یک گردش ۵۰۰۰ قرار می‌دهیم که برای تولید چند الگوی جالب کافی است و در عین حال برای تولید سریع گردش‌ها به اندازه‌ی کافی کوچک است (۲). سپس در ۳ دو لیست برای مقدارهای x و y می‌سازیم و شروع هر گردش را نقطه‌ی (0, 0) قرار می‌دهیم.

2.3.9 - انتخاب جهت و رسم گردش تصادفی

انتخاب جهت

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def fill_walk(self):
    """Calculate all the points in the walk."""

    # Keep taking steps until the walk reaches the desired length.
    while len(self.x_values) < self.num_points:

        # Decide which direction to go and how far to go in that direction.
        x_direction = choice([1, -1])
        x_distance = choice([0, 1, 2, 3, 4])
        x_step = x_direction * x_distance

        y_direction = choice([1, -1])
        y_distance = choice([0, 1, 2, 3, 4])
        y_step = y_direction * y_distance

        # Reject moves that go nowhere.
        if x_step == 0 and y_step == 0:
            continue

        # Calculate the new position.
        x = self.x_values[-1] + x_step
        y = self.y_values[-1] + y_step

        self.x_values.append(x)
        self.y_values.append(y)

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

از choice([1, -1]) برای انتخاب مقدار x_direction استفاده می‌کنیم که یا ۱ (برای حرکت به راست) برمی‌گرداند یا ۱- (برای رفتن به چپ) (۲). سپس choice([0, 1, 2, 3, 4]) به پایتون می‌گوید که با انتخاب عددی صحیح بین ۰ و ۴، در آن جهت (x_direction) چه‌قدر حرکت کند. (آوردن ۰ در این لیست به ما امکان می‌دهد که در جهت محور y حرکت کنیم و در عین حال بتوانیم گام‌هایی در جهت هر دو محور داشته باشیم.)

در ۳ و ۴ طول هر گام در جهت‌های x و y را با ضرب کردنِ جهتِ هر حرکت در فاصله‌ای که انتخاب‌شده حساب می‌کنیم. حاصل مثبت برای x_step به معنای حرکت به راست، حاصل منفی به معنای حرکت به چپ و صفر به معنای حرکت عمودی است. مقداری مثبت برای y_step به معنای حرکت به بالا، مقدار منفی به معنای حرکت به پایین و ۰ به معنای حرکت افقی است. اگر هر دو مقدار x_step و y_step صفر شود به جایی نمی‌رویم در نتیجه با صرف نظر از این حرکت حلقه را ادامه می‌دهیم. (۵)

برای به دست آوردن مقدار بعدی x، مقدار x_step را به آخرین مقدار ذخیره‌شده‌ی x_values اضافه می‌کنیم (۶) و همین کار را برای مقدارهای y تکرار می‌کنیم. زمانی که این مقدارها محاسبه شدند آن‌ها را به x_values و y_values الحاق می‌کنیم.

رسم گردش تصادفی

کد زیر تمام نقطه‌های یک گردش را رسم می‌کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import matplotlib.pyplot as plt

from random_walk import RandomWalk

# Make a random walk.
rw = RandomWalk()

rw.fill_walk()

# Plot the points in the walk.
plt.style.use('classic')

fig, ax = plt.subplots()

ax.scatter(rw.x_values, rw.y_values, s=15)

plt.show()

کار را با وارد کردن pyplot و RandomWalk شروع می‌کنیم. سپس گردشی تصادفی می‌سازیم و آن را در rw ذخیره می‌کنیم (۱) و سپس از فراخوانی fill_walk() مطمئن می‌شویم. در ۲ مقادیر x و y گردش را به scatter() می‌دهیم و اندازه‌ی مناسبی برای نقطه‌ها انتخاب می‌کنیم.


2.4 - فصل پانزدهم

2.4.1 - نصب Matplotlib و رسم نمودار خطّی

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

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

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

مردم از پایتون برای کارهایی با فشردگیِ بالای داده125 124 استفاده می‌کنند، مانند ژنتیک، پژو‌هش‌های اقلیمی، تحلیل‌های سیاسی و اقتصادی و بسیاری دیگر. دانشمندان علوم داده مجموعه‌ی چشمگیری از ابزارها را برای بصری‌سازی و تحلیل در پایتون نوشته‌اند که بسیاری از آن‌ها در دسترس شما نیز هستند. یکی از محبوب‌ترین ابزارها کتابخانه‌ای برای کشیدن نمودارهای ریاضی به نام Matplotlib است. از این کتابخانه برای ساختن نمودارهای ساده مانند نمودار خطّی و پراکندگی استفاده می‌کنیم. سپس مجموعه‌ی داده‌ی جالب‌تری بر پایه‌ی مفهوم گردش تصادفی خواهیم ساخت— بصری‌سازی‌ای تولیدشده بر اساس مجموعه‌ای از تصمیم‌های تصادفی.

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

نصب Matplotlib

برای استفاده از Matplotlib در اوّلین مجموعه از بصری‌سازی‌های خود باید آن را به کمک pip نصب کنید. pip ماژولی برای دریافت و نصب بسته‌های پایتون است. فرمان زیر را در یک اعلان ترمینال وارد کنید:

1
$ python -m pip install --user matplotlib

این فرمان از پایتون می‌خواهد تا ماژول pip را اجرا کرده و بسته‌ی matplotlib را به مجموعه‌ی پایتون نصب‌شده برای کاربر فعلی اضافه کند. اگر شما در سیستم خود از فرمان دیگری به جز python، مانند python3، برای اجرای برنامه‌ها یا آغاز یک نشست ترمینال استفاده می‌کنید، دستور شما به شکل زیر خواهد بود:

1
$ python3 -m pip install --user matplotlib
نکته
این فرمان در macOS کار نمی‌کند، به جای آن همین فرمان را بدون پرچم --user امتحان کنید.

برای بررسی انواع بصری‌سازی قابل ایجاد با Matplotlib سری به گالری نمونه‌های آن در https://matplotlib.org/gallery/ بزنید. اگر روی هر کدام از بصری‌سازی‌های موجود در گالری کلیک کنید کدی که آن را ایجاد کرده خواهید دید.

رسم یک نمودار خطّی ساده

بیایید به کمک Matplotlib یک نمودار خطّی ساده رسم کنیم و سپس آن را برای ساختن یک بصری‌سازی داده‌ی گویاتر شخصی‌سازی کنیم. از یک دنباله‌ی اعداد مربّع ۱، ۴، ۹، ۱۶ و ۲۵ به عنوان داده‌ی این نمودار استفاده می‌کنیم.

کافی است اعداد را به Matplotlib بدهید، چنان‌که اینجا می‌بینید و Matplotlib باقی کارها را برای شما خواهد کرد:

1
2
3
4
5
6
import matplotlib.pyplot as plt

squares = [1, 4, 9, 16, 25]
fig, ax = plt.subplots()
ax.plot(squares)
plt.show()

ابتدا ماژول pyplot را با نام مستعار plt وارد می‌کنیم تا مجبور نباشیم هر دفعه pyplot را بنویسیم. (این نامگذاری را به صورت یک قرارداد در مثال‌های آنلاین خواهید دید بنابراین ما هم از همین استفاده می‌کنیم.) ماژول پای‌پلات شامل تعدادی تابع برای تولید نمودار است.

لیستی به نام squares می‌سازیم تا داده‌ها را برای رسم ذخیره کند. سپس با فراخوانی تابع subplots() از یکی دیگر از قراردادهای Matplotlib پیروی می‌کنیم. این تابع یکی تا چند نمودار را در یک شکل واحد رسم می‌کند. متغیّر fig بیانگر شکل کلّی یا همان مجموعه‌ی نمودارهایی است که تولید می‌شوند. متغیّر ax یکی از نمودارهای داخل شکل را نشان می‌دهد و متغیّری است که بیشتر اوقات از آن استفاده می‌کنیم.

سپس از متد plot() بهره می‌گیریم که تلاش می‌کند داده‌ی ورودی را به روشی معنادار رسم کند. تابع plt.show() نمایشگر Matplotlib را باز می‌کند و نمودار را نشان می‌دهد، چنانکه در شکل ۱۵-۱ می‌بینیم. نمایشگر به شما امکان بزرگنمایی و پیمایش نمودار را می‌دهد و اگر روی آیکون ذخیره کلید کنید می‌توانید هر نموداری را که خواستید ذخیره کنید.

![][image16]

شکل ۱۵-۱: یکی از ساده‌ترین نمودارهایی که می‌توانید با Matplotlib تولید کنید.

تغییر دادن نوشته‌ی برچسب و ضخامت خط

هرچند نمودار شکل ۱۵-۱ افزایشی بودن اعداد را نشان می‌دهد امّا نوشته‌ی روی محورها کوچک و خط رسم‌شده کمی نازک است که باعث خوانایی پایین می‌شود. خوشبختانه Matplotlib به شما امکان می‌دهد که تمام ویژگی‌های یک بصری‌سازی را تغییر دهید.

ما تعداد کمی از تغییرات ممکن را اعمال می‌کنیم تا خوانایی نمودار را افزایش دهیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import matplotlib.pyplot as plt

squares = [1, 4, 9, 16, 25]
fig, ax = plt.subplots()
ax.plot(squares, linewidth=3)

# Set chart title and label axes.
ax.set_title("Square Numbers", fontsize=24)
ax.set_xlabel("Value", fontsize=14)
ax.set_ylabel("Square of Value", fontsize=14)

# Set size of tick labels.
ax.tick_params(axis='both', labelsize=14)

plt.show()

پارامتر linewidth ضخامت خطی را تنظیم می‌کند که plot() تولید می‌کند. متد set_title() عنوانی برای نمودار تعیین می‌کند. پارامترهای fontsize که چند بار در کد تکرار شده‌اند اندازه‌ی متن را در اجزای مختلف نمودار تغییر می‌دهد.

متدهای set_xlabel() و set_ylabel() به شما امکان می‌دهند که برای هر محور عنوانی تعیین کنید و متد tick_params() نشانه‌گذاری‌های روی محور را شکل می‌دهد. نشان‌وندهایی که اینجا آمده بر روی هر دو محور تاثیر می‌گذارد (axis='both') و اندازه‌ی برچسب این نشانه‌گذاری‌ها را ۱۴ قرار می‌دهد (labelsize=14).

چنانکه در شکل ۱۵-۲ می‌بینید خواندن نمودار حاصل بسیار راحت‌تر شده است. نوشته‌ی برچسب‌ها بزرگتر و خط نمودار ضخیم‌تر شده است. اغلب می‌ارزد که با این مقدارها بازی کنید تا به ایده‌ای کلّی درباره‌ی بهتر به نظر رسیدن نمودار نهایی برسید.

![][image17]

شکل ۱۵-۲: اکنون خواندن نمودار بسیار راحت‌تر است.

اصلاح نمودار

حال که می‌توانیم نمودار را بهتر بخوانیم می‌بینیم که داده به درستی رسم نشده است. به انتهای نمودار توجّه کنید که مربّع 4.0، 25 نشان داده شده است! بیایید این مشکل را رفع کنیم.

زمانی که به plot() دنباله‌ای از اعداد می‌دهیم، فرض می‌کند که اوّلین داده متناظر با مقدار 0 در محور x است امّا اوّلین داده‌ی ما مربوط به مقدار ۱ در این محور است. می‌توانیم این رفتار پیش‌فرض را با دادن مقدارهای ورودی و خروجی به‌کاررفته برای محاسبه‌ی مربّعات به این متد تغییر دهیم:

1
2
3
4
5
6
7
8
9
import matplotlib.pyplot as plt

input_values = [1, 2, 3, 4, 5]
squares = [1, 4, 9, 16, 25]
fig, ax = plt.subplots()
ax.plot(input_values, squares, linewidth=3)

# Set chart title and label axes.
--snip--

اکنون plot() داده‌ها را درست رسم می‌کند زیرا مقدارهای ورودی و خروجی را به آن داده‌ایم و دیگر در مورد روش تولید خروجی‌ها فرضی نمی‌کند. نمودار حاصل که در شکل ۱۵-۳ نشان داده شده درست است.

![][image18]

شکل ۱۵-۳: اکنون داده‌ها به درستی رسم شده‌اند.

هنگام استفاده از plot() می‌توانید نشان‌وندهای متعدّدی را تعیین کنید و از تعداد زیادی تابع برای شخصی‌سازی نمودارهای خود بهره بگیرید. این تابع‌های شخصی‌سازی را هنگام کار کردن با مجموعه‌داده‌های جذّاب‌تر در طول فصل بیشتر بررسی خواهیم کرد.

به‌کارگیری شیوه‌های پیش‌ساخته

Matplotlib تعداد شکل و شیوه‌ی ازپیش‌تعریف‌شده در اختیار می‌گذارد که تنظیمات اوّلیّه‌ی خوبی برای رنگ پس‌زمینه، خطوط شبکه‌بندی، ضخامت خط‌ها، فونت‌ها، اندازه‌ی فونت‌ها و بسیاری دیگر از ویژگی‌ها دارد که بصری‌سازی‌های شما را بدون نیاز به شخصی‌سازی خاصّی جذّاب جلوه می‌دهد. برای دیدن این شکل و شیوه‌ها در سیستم خود خط‌های زیر را در یک نشست ترمینال اجرا کنید:

1
2
3
4
>>> import matplotlib.pyplot as plt
>>> plt.style.available
['seaborn-dark', 'seaborn-darkgrid', 'seaborn-ticks', 'fivethirtyeight',
--snip--

برای به‌کارگیری هرکدام از این‌ها یک خط کد را پیش از آغاز ایجاد نمودار اضافه کنید:

1
2
3
4
5
6
7
import matplotlib.pyplot as plt

input_values = [1, 2, 3, 4, 5]
squares = [1, 4, 9, 16, 25]
plt.style.use('seaborn')
fig, ax = plt.subplots()
--snip--

این کد نموداری می‌کشد که در شکل ۱۵-۴ نشان داده شده است. گستره‌ی بزرگی از این شیوه‌های شکل‌دهی وجود دارد؛ با آن‌ها سر و کله بزنید تا چیزی مناسب سلیقه‌ی خود پیدا کنید.

![][image19]

شکل ۱۵-۴: شیوه‌ی شکل‌دهی پیش‌ساخته‌ی seaborn

2.4.2 - رسم نقاط با scatter() و ذخیره‌سازی نمودارها

رسم و شکل‌دهی نقاط مجزّا با scatter()

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

برای رسم یک نقطه از متد scatter() استفاده کنید. مقدارهای (x, y) نقطه‌ی مورد نظر خود را به این متد بدهید تا آن را رسم کند:

1
2
3
4
5
6
import matplotlib.pyplot as plt

plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.scatter(2, 4)
plt.show()

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import matplotlib.pyplot as plt

plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.scatter(2, 4, s=200)

# Set chart title and label axes.
ax.set_title("Square Numbers", fontsize=24)
ax.set_xlabel("Value", fontsize=14)
ax.set_ylabel("Square of Value", fontsize=14)

# Set size of tick labels.
ax.tick_params(axis='both', which='major', labelsize=14)

plt.show()

در اینجا scatter() را فرامی‌خوانیم و از نشان‌وند s برای تنظیم اندازه‌ی نقطه‌های به‌کاررفته برای رسم نمودار استفاده می‌کنیم. اگر اکنون scatter_squares.py را اجرا کنید باید نقطه‌ای تنها در وسط نمودار ببینید، مانند شکل ۱۵-۵.

![][image20]

شکل ۱۵-۵: رسم یک نقطه‌ی تنها

رسم مجموعه‌ای از نقاط با scatter()

برای رسم مجموعه‌ای از نقطه‌ها می‌توانیم لیستی جداگانه از مقدارهای x و y به آن بدهیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import matplotlib.pyplot as plt

x_values = [1, 2, 3, 4, 5]
y_values = [1, 4, 9, 16, 25]

plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.scatter(x_values, y_values, s=100)

# Set chart title and label axes.
--snip--

لیست x_values شامل اعدادی است که باید مربّع شوند و y_values شامل مربّع هر عدد است. زمانی که این دو لیست را به scatter() می‌دهید، Matplotlib هنگام رسم هر نقطه از هر کدام یک مقدار را می‌خواند. نقطه‌هایی که باید رسم شوند (1,1)، (2,4)، (3,9)، (4, 16) و (5, 25) است. شکل ۱۵-۶ نتیجه را نشان می‌دهد.

![][image21]

شکل ۱۵-۶: نمودار پراکندگی با چند نقطه

محاسبه‌ی خودکار داده‌ها

نوشتن دستی لیست‌ها کارآمد نیست به ویژه زمانی که با داده‌های بسیار زیادی سر و کار داریم. به جای انتقال نقطه‌ها به شکل لیست، از یک حلقه استفاده می‌کنیم تا پایتون محاسبات را برای ما انجام دهد.

اینجا این روش را با وجود ۱۰۰۰ نقطه می‌بینیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import matplotlib.pyplot as plt

x_values = range(1, 1001)
y_values = [x**2 for x in x_values]

plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.scatter(x_values, y_values, s=10)

# Set chart title and label axes.
--snip--

# Set the range for each axis.
ax.axis([0, 1100, 0, 1100000])

plt.show()

از گستره‌ی مقدارهای x شامل اعداد ۱ تا ۱۰۰۰ شروع می‌کنیم. سپس به کمک روش دربرداری (for x in x_values) در مقدارهای x حلقه می‌زنیم، هر کدام را به توان ۲ می‌رسانیم (x**2) و نتیجه‌ها را در y_values ذخیره می‌کنیم. سپس لیست‌های ورودی و خروجی را به scatter() می‌دهیم. چون تعداد داده‌ها در این مجموعه زیاد است اندازه‌ی نقطه‌ها را کوچک‌تر می‌کنیم.

از متد axis() برای تعیین گستره‌ی هر محور استفاده می‌کنیم. این متد ۴ مقدار می‌پذیرد: کمینه و بیشینه‌ی مقدارهای محور x و y. اینجا محور x را بین ۰ و ۱۱۰۰ و محور y را بین ۰ و ۱،۱۰۰،۰۰۰ قرار می‌دهیم. شکل ۱۵-۷ نتیجه را نشان می‌دهد.

![][image22]

شکل ۱۵-۷: پایتون ۱۰۰۰ نقطه را به همان آسانی ۵ نقطه رسم می‌کند.

تعریف کردن رنگ‌های دلخواه

برای تغییر رنگ نقطه‌ها c را به همراه نام رنگ در علامت نقل قول به scatter() بدهید، مانند زیر:

1
ax.scatter(x_values, y_values, c='red', s=10)

علاوه بر این می‌توانید رنگ‌های دلخواه را به کمک مدل رنگی RGB هم تعیین کنید. برای این کار نشان‌وند c را به همراه یک چندتایی شامل سه مقدار اعشاری بین ۰ و ۱ (به ترتیب برای رنگ قرمز، سبز و آبی) وارد کنید. برای مثال خطّ زیر نموداری با نقطه‌های سبز کم‌رنگ ایجاد می‌کند:

1
ax.scatter(x_values, y_values, c=(0, 0.8, 0), s=10)

مقدارهای نزدیک‌تر به ۰ رنگ‌های تیره و مقدارهای نزدیک‌تر به ۱ رنگ‌های روشن‌تر تولید می‌کنند.

استفاده از نقشه‌ی رنگی

نقشه‌ی رنگی دنباله‌ای از رنگ‌های محوشونده است که126 از یک رنگ آغاز به رنگی ختم می‌شوند. کاربرد نقشه‌های رنگی در بصری‌سازی پررنگ کردن الگویی در داده‌هاست. برای مثال می‌توانید مقدارهای کم را با رنگ روشن و مقادیر زیاد را با رنگی تیره نمایش دهید.

ماژول pyplot مجموعه‌ای از نقشه‌های رنگی پیش‌ساخته دارد. برای استفاده کردن از آن‌ها باید مشخّص کنید که pyplot چگونه به هر نقطه‌ی مجموعه‌داده رنگی اختصاص دهد. اینجا روش اختصاص رنگ را بر اساس مقدار y نقطه می‌بینیم:

1
2
3
4
5
6
7
8
9
import matplotlib.pyplot as plt

x_values = range(1, 1001)
y_values = [x**2 for x in x_values]

ax.scatter(x_values, y_values, c=y_values, cmap=plt.cm.Blues, s=10)

# Set chart title and label axes.
--snip--

لیستی از مقدارهای y را به c نسبت می‌دهیم و سپس با نشان‌وند cmap به pyplot می‌گوییم که از کدام نقشه‌ی رنگی استفاده کند. این کد نقاطی را که مقدار y آن‌ها کمتر است با آبی روشن و آن‌هایی را که مقدار y بیشتری دارند با آبی تیره رنگ‌آمیزی می‌کند.

نکته
می‌توانید تمام نقشه‌های رنگی موجود در pyplot را در آدرس https://matplotlib.org ببینید؛ به قسمت Examples بروید، پایین بروید تا به Colors برسید و روی Colormap reference کلیک کنید.

![][image23]

شکل ۱۵-۸: نموداری با نقشه‌ی رنگی Blues

ذخیره‌سازی خودکار نمودارها

اگر بخواهید که برنامه‌تان نمودار را به طور خودکار در فایلی ذخیره کند، می‌توانید فراخوانی plt.show() را با plt.savefig() جایگزین کنید:

1
plt.savefig('squares_plot.png', bbox_inches='tight')

نشان‌وند نخست نام فایلی است که برای تصویر نمودار انتخاب می‌کنید. این فایل در همان پوشه‌ی scatter_squares.py ذخیره خواهد شد. نشان‌وند دوم فاصله‌های خالی اضافی اطراف نمودار را حذف می‌کند. اگر می‌خواهید این فضای خالی باقی بماند کافی‌است این نشان‌وند را حذف کنید.

خودآزمایی

۱۵-۱. عددهای مکعب‌: توان سوم هر عدد را مکعّب آن می‌خوانند. ابتدا نمودار ۵ عدد مکعّب نخست را بکشید سپس این کار را برای ۵۰۰۰ عدد مکعّب نخست تکرار کنید.

۱۵-۲. اعداد مکعّب رنگی: یک نقشه‌ی رنگی روی نمودار اعداد مکعّب اعمال کنید.

2.4.3 - گردش تصادفی

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

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

ساختن کلاس RandomWalk()

برای ایجاد یک گردش تصادفی کلاس RandomWalk را ایجاد می‌کنیم که در مورد جهت حرکتی که باید انجام بگیرد تصمیم‌های تصادفی می‌گیرد. کلاس سه مشخّصه نیاز دارد: یک متغیّر برای ذخیره کردن تعداد نقطه‌ها در گردش و دو لیست برای ذخیره کردن مقدار مختصّات x و y هر نقطه‌ی حرکت.

تنها دو متد در کلاس RandomWalk مورد نیاز است: __init__() و fill_walk() که نقطه‌های حرکت را محاسبه می‌کند. از متد __init__() شروع می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from random import choice

class RandomWalk:
    """A class to generate random walks."""

    def __init__(self, num_points=5000):
        """Initialize attributes of a walk."""
        self.num_points = num_points

        # All walks start at (0, 0).
        self.x_values = [0]
        self.y_values = [0]

برای گرفتن تصمیم‌های تصادفی حرکت‌های ممکن را در لیستی ذخیره می‌کنیم و سپس از تابع choice() از ماژول random استفاده می‌کنیم تا تصمیم بگیریم که در هر گام کدام حرکت را انجام دهیم. سپس تعداد پیش‌فرض نقطه‌ها را در یک گردش ۵۰۰۰ قرار می‌دهیم که برای تولید چند الگوی جالب کافی است و در عین حال برای تولید سریع گردش‌ها به اندازه‌ی کافی کوچک است. سپس دو لیست برای مقدارهای x و y می‌سازیم و شروع هر گردش را نقطه‌ی (0, 0) قرار می‌دهیم.

انتخاب جهت

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def fill_walk(self):
    """Calculate all the points in the walk."""
    # Keep taking steps until the walk reaches the desired length.
    while len(self.x_values) < self.num_points:
        # Decide which direction to go and how far to go in that direction.
        x_direction = choice([1, -1])
        x_distance = choice([0, 1, 2, 3, 4])
        x_step = x_direction * x_distance

        y_direction = choice([1, -1])
        y_distance = choice([0, 1, 2, 3, 4])
        y_step = y_direction * y_distance

        # Reject moves that go nowhere.
        if x_step == 0 and y_step == 0:
            continue

        # Calculate the new position.
        x = self.x_values[-1] + x_step
        y = self.y_values[-1] + y_step

        self.x_values.append(x)
        self.y_values.append(y)

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

از choice([1, -1]) برای انتخاب مقدار x_direction استفاده می‌کنیم که یا ۱ (برای حرکت به راست) برمی‌گرداند یا -۱ (برای رفتن به چپ). سپس choice([0, 1, 2, 3, 4]) به پایتون می‌گوید که با انتخاب عددی صحیح بین ۰ و ۴، در آن جهت (x_direction) چه‌قدر حرکت کند. (آوردن ۰ در این لیست به ما امکان می‌دهد که در جهت محور y حرکت کنیم و در عین حال بتوانیم گام‌هایی در جهت هر دو محور داشته باشیم.)

طول هر گام در جهت‌های x و y را با ضرب کردنِ جهتِ هر حرکت در فاصله‌ای که انتخاب‌شده حساب می‌کنیم. حاصل مثبت برای x_step به معنای حرکت به راست، حاصل منفی به معنای حرکت به چپ و صفر به معنای حرکت عمودی است. مقداری مثبت برای y_step به معنای حرکت به بالا، مقدار منفی به معنای حرکت به سمت پایین و ۰ به معنای حرکت افقی است. اگر هر دو مقدار x_step و y_step صفر شود به جایی نمی‌رویم در نتیجه با صرف نظر از این حرکت حلقه را ادامه می‌دهیم.

برای به دست آوردن مقدار بعدی x، مقدار x_step را به آخرین مقدار ذخیره‌شده‌ی x_values اضافه می‌کنیم و همین کار را برای مقدارهای y تکرار می‌کنیم. زمانی که این مقدارها محاسبه شدند آن‌ها را به x_values و y_values الحاق می‌کنیم.

رسم گردش تصادفی

کد زیر تمام نقطه‌های یک گردش را رسم می‌کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import matplotlib.pyplot as plt
from random_walk import RandomWalk

# Make a random walk.
rw = RandomWalk()
rw.fill_walk()

# Plot the points in the walk.
plt.style.use('classic')
fig, ax = plt.subplots()
ax.scatter(rw.x_values, rw.y_values, s=15)

plt.show()

کار را با وارد کردن pyplot و RandomWalk شروع می‌کنیم. سپس گردشی تصادفی می‌سازیم و آن را در rw ذخیره می‌کنیم و سپس از فراخوانی fill_walk() مطمئن می‌شویم. مقادیر x و y گردش را به scatter() می‌دهیم و اندازه‌ی مناسبی برای نقطه‌ها انتخاب می‌کنیم. شکل ۱۵-۹ نمودار حاصل را با ۵۰۰۰ نقطه نشان می‌دهد. (در تصویرهای این بخش نمایشگر Matplotlib حذف شده‌اند ولی شما هنگام اجرای rw_visual.py آن را خواهید دید.)

![][image24]

شکل ۱۵-۹: یک گردش تصادفی با ۵۰۰۰ نقطه

تولید چند گردش تصادفی

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import matplotlib.pyplot as plt
from random_walk import RandomWalk

# Keep making new walks, as long as the program is active.
while True:
    # Make a random walk.
    rw = RandomWalk()
    rw.fill_walk()

    # Plot the points in the walk.
    plt.style.use('classic')
    fig, ax = plt.subplots()
    ax.scatter(rw.x_values, rw.y_values, s=15)

    plt.show()

    keep_running = input("Make another walk? (y/n): ")
    if keep_running == 'n':
        break

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

2.4.4 - شخصی‌سازی گردش تصادفی

شکل دادن به گردش

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

رنگ‌آمیزی نقطه‌ها

برای نمایش ترتیب نقاط در گردش از نقشه‌های رنگی استفاده می‌کنیم و سپس حاشیه‌ی سیاه هر نقطه را حذف می‌کنیم تا واضح‌تر به نظر بیایند. برای رنگ‌آمیزی نقطه‌ها بر اساس موقعیّت آن‌ها در گردش، لیستی را شامل جایگاه هر نقطه به عنوان نشان‌وند c به متد می‌دهیم. از آن‌جا که نقطه‌ها به ترتیب رسم می‌شوند این لیست تنها شامل اعداد ۰ تا ۴۹۹۹ است، مانند زیر:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
--snip--

while True:
    # Make a random walk.
    rw = RandomWalk()
    rw.fill_walk()

    # Plot the points in the walk.
    plt.style.use('classic')
    fig, ax = plt.subplots()
    point_numbers = range(rw.num_points)
    ax.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues,
        edgecolors='none', s=15)

    plt.show()

    keep_running = input("Make another walk? (y/n): ")
    --snip--

از range() برای تولید لیستی از اعداد به تعداد نقطه‌های موجود در گردش استفاده می‌کنیم. سپس آن‌ها را در لیست point_numbers ذخیره می‌کنیم که بعداً برای تعیین رنگ هر نقطه به کار خواهد آمد. point_numbers را به نشان‌وند c می‌دهیم، از نقشه‌ی رنگی Blues استفاده می‌کنیم و سپس edgecolors='none' را به تابع می‌دهیم تا از خط سیاه دور هر نقطه خلاص شویم. نتیجه نموداری از گردش است که رنگ آن به صورت تدریجی از آبی روشن به سمت تیره می‌رود، مانند شکل ۱۵-۱۰.

![][image25]

شکل ۱۵-۱۰: یک گردش تصادفی که به نقشه‌ی رنگی Blues رنگ‌آمیزی شده است.

رسم نقاط آغازی و انتهایی

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
--snip--

while True:
    --snip--
    ax.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues,
        edgecolors='none', s=15)

    # Emphasize the first and last points.
    ax.scatter(0, 0, c='green', edgecolors='none', s=100)
    ax.scatter(rw.x_values[-1], rw.y_values[-1], c='red', edgecolors='none',
        s=100)

    plt.show()
    --snip--

برای نمایش نقطه‌ی آغاز، نقطه‌ی (0, 0) را سبزرنگ و در اندازه‌ی بزرگ‌تر از دیگر نقاط (s=100) رسم می‌کنیم. برای مشخّص کردن نقطه‌ی پایانی آخرین مقدار x و y را با اندازه‌ی ۱۰۰ و رنگ قرمز می‌کشیم. مطمئن شوید که این کد را درست پیش از فراخوانی plt.show() بیاورید تا نقطه‌های آغاز و پایان روی دیگر نقطه‌ها رسم شوند.

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

پاک‌سازی محورها

محورها را از این نمودار حذف می‌کنیم تا حواس ما را از مسیر هر گردش پرت نکنند. برای پاک کردن محورها از این کد استفاده کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
--snip--

while True:
    --snip--
    ax.scatter(rw.x_values[-1], rw.y_values[-1], c='red', edgecolors='none',
        s=100)

    # Remove the axes.
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    plt.show()
    --snip--

برای تغییر دادن محورها از متدهای ax.get_xaxis() و ax.get_yaxis() استفاده می‌کنیم تا قابلیّت دیدن هر کدام را False کنیم. هنگامی که به کار بصری‌سازی ادامه می‌دهید به هم پیوستن متدها را به این شیوه به تناوب خواهید دید.

حال rw_visual.py را اجرا کنید؛ باید دنباله‌ای از نمودارها را بدون محور ببینید.

افزایش نقاط نمودار

بیایید تعداد نقطه‌ها را افزایش دهیم تا داده‌های بیشتری برای کار کردن داشته باشیم. برای این کار مقدار num_points را هنگام ساختن نمونه‌ی RandomWalk افزایش می‌دهیم و اندازه‌ی هر نقطه را هنگام رسم تنظیم می‌کنیم، مانند زیر:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
--snip--

while True:
    # Make a random walk.
    rw = RandomWalk(50_000)
    rw.fill_walk()

    # Plot the points in the walk.
    plt.style.use('classic')
    fig, ax = plt.subplots()
    point_numbers = range(rw.num_points)
    ax.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues,
        edgecolor='none', s=1)
    --snip--

این مثال گردشی تصادفی با ۵۰،۰۰۰ نقطه می‌سازد تا منعکس‌کننده‌ی داده‌های دنیای واقعی باشد و هر نقطه را با اندازه‌ی s=1 می‌کشد. همان‌طور که در شکل ۱۵-۱۱ می‌بینیم گردش نهایی ابری‌شکل است. چنان‌که می‌بینید که با یک نمودار پراکندگی ساده اثری هنری خلق کرده‌ایم!

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

![][image26]

شکل ۱۵-۱۱: گردشی با ۵۰،۰۰۰ نقطه

تغییر دادن اندازه برای پر کردن صفحه

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--snip--

while True:
    # Make a random walk.
    rw = RandomWalk(50_000)
    rw.fill_walk()

    # Plot the points in the walk.
    plt.style.use('classic')
    fig, ax = plt.subplots(figsize=(15, 9))
    --snip--

زمانی که نموداری می‌سازید می‌توانید نشان‌وند figsize را برای تعیین اندازه‌ی شکل به آن بدهید. پارامتر figsize یک چندتایی می‌گیرد که ابعاد پنجره‌ی نمودار را به اینچ به Matplotlib می‌گوید.

Matplotlib فرض می‌کند که وضوح صفحه‌ی شما ۱۰۰ پیکسل بر اینچ است؛ اگر این کد اندازه‌ی دقیقی برای نمودار فراهم نکرد اعداد را تغییر دهید. یا اگر وضوح تصویر سیستم خود را می‌دانید آن را با پارامتر dpi به plt.subplots() بدهید تا اندازه را طوری تعیین کند که از فضای موجود در صفحه نمایش شما به خوبی استفاده کند، مانند زیر:

1
fig, ax = plt.subplots(figsize=(10, 6), dpi=128)

خودآزمایی

۱۵-۳. جنبش مولکولی: برنامه rw_visual.py را تغییر دهید و plt.scatter() را با plt.plot() جایگزین کنید. برای شبیه‌سازی حرکت دانه‌ی گرده روی سطح یک قطره‌ی آب مقادیر rw.x_values و rw.y_values را به تابع بدهید و نشان‌وند linewidth را هم اضافه کنید. به جای ۵۰،۰۰۰ نقطه از ۵۰۰۰ نقطه استفاده کنید.

۱۵-۴. گردش‌های تصادفی تغییریافته: در کلاس RandomWalk، x_step و y_step با مجموعه‌ی مشابهی از شرط‌ها تولید می‌شوند. جهت به طور تصادفی از لیست [1, -1] و مسافت از لیست [0, 1, 2, 3, 4] انتخاب می‌شود. مقدارهای این دو لیست را عوض کنید تا ببینید در شکل کلی گردش‌ها چه تغییری ایجاد می‌شود. لیست طولانی‌تر را برای مسافت امتحان کنید (مثلاً از ۰ تا ۸) و -۱ را از لیست جهت x و y حذف کنید.

۱۵-۵. بازآرایی: متد fill_walk() طولانی است. متد جدیدی به نام get_step() بسازید تا جهت و مسافت هر گام را تعیین کند و سپس گام را محاسبه کند. شما در نهایت باید به دو فراخوانی get_step() در fill_walk() برسید:

1
2
x_step = self.get_step()
y_step = self.get_step()

این بازآرایی باید اندازه‌ی fill_walk() را کاهش داده و خواندن و درک متد را آسان‌تر کند.

2.4.5 - تاس انداختن با پلات‌لی

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

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

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

نصب کردن پلات‌لی

پلات‌لی را هم مانند Matplotlib به کمک pip نصب کنید:

1
$ python -m pip install --user plotly

اگر هنگام نصب Matplotlib از دستور python3 یا مشابه آن استفاده کردید، اکنون نیز از همان استفاده کنید.

برای بازدید از بصری‌سازی‌های قابل ساخت با پلات‌لی از گالری انواع نمودارهای آن در https://plot.ly/python/ دیدن کنید. هر مثال شامل کد برنامه نیز هست بنابراین می‌توانید ببینید که پلات‌لی چگونه آن بصری‌سازی را تولید کرده است.

ساختن کلاس تاس

کلاس Die زیر را برای شبیه‌سازی پرتاب یک تاس می‌سازیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from random import randint

class Die:
    """A class representing a single die."""

    def __init__(self, num_sides=6):
        """Assume a six-sided die."""
        self.num_sides = num_sides

    def roll(self):
        """Return a random value between 1 and number of sides."""
        return randint(1, self.num_sides)

متد __init__() یک نشان‌وند اختیاری می‌پذیرد. در کلاس Die هنگامی که نمونه‌ای از تاس ساخته می‌شود، اگر نشان‌وندی وارد نشود، تعداد وجه‌ها همیشه شش خواهد بود. امّا اگر نشان‌وندی ارائه شود، مقدار آن تعداد وجه‌های تاس را معلوم می‌کند. (تاس‌ها با تعداد وجه‌های آن‌ها نام‌گذاری می‌شوند: تاس شش‌وجهی D6 و تاس هشت‌وجهی D8 نامیده می‌شود و …)

متد roll() از تابع randint() برای برگرداندن یک عدد تصادفی بین ۱ و تعداد وجه‌های تاس استفاده می‌کند. این تابع ممکن است مقدار آغازین (۱)، مقدار پایانی (num_sides) و یا هر عددی صحیح بین این دو را برگرداند.

پرتاب تاس

پیش از آن که بر پایه‌ی کلاس Die بصری‌سازی تولید کنیم بیایید یک تاس D6 را پرتاب کنیم، نتایج را چاپ کنیم و ببینیم که آیا نتایج منطقی است یا نه:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from die import Die

# Create a D6.
die = Die()

# Make some rolls, and store results in a list.
results = []
for roll_num in range(100):
    result = die.roll()
    results.append(result)

print(results)

نمونه‌ای از Die با تعداد پیش‌فرض شش وجه می‌سازیم. این تاس را ۱۰۰ بار می‌اندازیم و نتایج هر پرتاب را در لیست results ذخیره می‌کنیم. این نمونه‌ای از مجموعه‌ی نتایج است:

1
2
3
4
5
[4, 6, 5, 6, 1, 5, 6, 3, 5, 3, 5, 3, 2, 2, 1, 3, 1, 5, 3, 6, 3, 6, 5, 4,
1, 1, 4, 2, 3, 6, 4, 2, 6, 4, 1, 3, 2, 5, 6, 3, 6, 2, 1, 1, 3, 4, 1, 4,
3, 5, 1, 4, 5, 5, 2, 3, 3, 1, 2, 3, 5, 6, 2, 5, 6, 1, 3, 2, 1, 1, 1, 6,
5, 5, 2, 2, 6, 4, 1, 4, 5, 1, 1, 1, 4, 5, 3, 3, 1, 3, 5, 4, 5, 6, 5, 4,
1, 5, 1, 2]

یک نگاه سریع به نتایج نشان می‌دهد که کلاس Die به نظر درست کار می‌کند. مقدارهای ۱ تا ۶ را می‌بینیم که می‌دانیم آن‌ها کوچک‌ترین و بزرگ‌ترین مقدارهای برگشتی ممکن هستند و چون اعداد ۱ یا ۷ را نمی‌بینیم می‌فهمیم که تمام نتایج در بازه‌ی مناسب قرار دارند. همچنین تمام اعداد بین ۱ تا ۶ را در خروجی می‌بینیم که نشان می‌دهد تمام رخ‌دادهای ممکن نمایش داده شده است. بیایید تعداد دقیق نمایان شدن هر عدد را معلوم کنیم.

تحلیل نتایج

نتایج حاصل از پرتاب تاس D6 را با شمردن تعداد رخ دادن هر عدد تحلیل می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
--snip--

# Make some rolls, and store results in a list.
results = []
for roll_num in range(1000):
    result = die.roll()
    results.append(result)

# Analyze the results.
frequencies = []
for value in range(1, die.num_sides+1):
    frequency = results.count(value)
    frequencies.append(frequency)

print(frequencies)

چون دیگر نتایج پرتاب تاس را چاپ نمی‌کنیم می‌توانیم تعداد پرتاب‌های تاس شبیه‌سازی‌شده را به ۱۰۰۰ افزایش دهیم. برای تحلیل پرتاب‌ها لیستی خالی به نام frequencies می‌سازیم تا تعداد رخ دادن هر عدد را در آن ذخیره کنیم. در نتایج ممکن (در این مورد ۱ تا ۶) حلقه می‌زنیم، تعداد نمایان شدن هر عدد را در results می‌شماریم و سپس این مقدار را به لیست frequencies اضافه می‌کنیم. سپس نتایج را پیش از ساختن بصری‌سازی چاپ می‌کنیم:

1
[155, 167, 168, 170, 159, 181]

این نتایج به نظر قابل قبول‌اند: شش بسامد داریم، هر کدام برای هر عدد ممکن در پرتاب تاس D6، و می‌بینیم که هیچ کدام به طرز محسوسی از دیگر بسامدها بیشتر نیستند. حالا بیایید این نتایج را بصری کنیم.

ساختن یک بافت‌نگار

با وجود لیست بسامدها می‌توانیم بافت‌نگار یا هیستوگرام128 نتایج را درست کنیم. بافت‌نگار نموداری میله‌ای است که تعداد رخ دادن هر نتیجه را نمایش می‌دهد. این کد ساختن بافت‌نگار است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from plotly.graph_objs import Bar, Layout
from plotly import offline
from die import Die

--snip--

# Analyze the results.
frequencies = []
for value in range(1, die.num_sides+1):
    frequency = results.count(value)
    frequencies.append(frequency)

# Visualize the results.
x_values = list(range(1, die.num_sides+1))
data = [Bar(x=x_values, y=frequencies)]

x_axis_config = {'title': 'Result'}
y_axis_config = {'title': 'Frequency of Result'}
my_layout = Layout(title='Results of rolling one D6 1000 times',
    xaxis=x_axis_config, yaxis=y_axis_config)

offline.plot({'data': data, 'layout': my_layout}, filename='d6.html')

برای ساختن یک بافت‌نگار برای هر نتیجه‌ی ممکن یک میله نیاز داریم. این میله‌ها را در لیستی به نام x_values ذخیره می‌کنیم که از ۱ آغاز و به تعداد وجه‌های تاس ختم می‌شود. پلات‌لی خروجی مستقیم تابع range() را نمی‌پذیرد پس باید این بازه را صریحاً به کمک تابع list() به لیست تبدیل کنیم. کلاس Bar() پلات‌لی بیانگر مجموعه‌داده‌ای است که به شکل نمودار میله‌ای درخواهد آمد. این کلاس به لیستی شامل مقدارهای x و لیستی شامل مقدارهای y نیاز دارد. این کلاس باید داخل کروشه قرار بگیرد زیرا یک مجموعه‌ی داده ممکن است عنصرهای متعدّدی داشته باشد.

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

برای تولید نمودار تابع offline.plot() را فرامی‌خوانیم. این تابع یک دیکشنری شامل داده و شیءهای طرح‌بندی و همچنین نام فایل ذخیره‌سازی نمودار را می‌پذیرد. خروجی را در فایلی به نام d6.html ذخیره می‌کنیم.

زمانی که برنامه‌ی die_visual.py را اجرا کنید احتمالاً مرورگری باز می‌شود که فایل d6.html را نشان می‌دهد. اگر چنین نشد برگه‌ی جدیدی در یک مرورگر باز کنید و سپس فایل d6.html را در آن باز کنید (فایل در همان پوشه‌ای است که die_visual.py را ذخیره کرده‌اید.) باید نموداری شبیه به آنچه در شکل ۱۵-۱۲ آمده ببینید.

![][image27]

شکل ۱۵-۱۲: نمودار میله‌ای ساده‌ای که با پلات‌لی درست شده است.

توجّه کنید که پلات‌لی نمودار را تعاملی کرده است: نشانگر ماوس را روی هر کدام از میله‌های نمودار نگه دارید و داده‌ی مربوط به آن را خواهید دید. این ویژگی به خصوص زمانی سودمند است که چند مجموعه‌داده را روی یک نمودار رسم کنید. همچنین به آیکون‌های بالا سمت راست توجّه کنید که به شما اجازه‌ی جابه‌جا کردن و بزرگنمایی این بصری‌سازی و ذخیره کردن آن به صورت یک عکس را می‌دهد.

2.4.6 - پرتاب تاس‌های مختلف و خلاصه

پرتاب دو تاس

پرتاب دو تاس به عددهای بزرگ‌تر و توزیع متفاوتی از نتایج منجر می‌شود. کد را برای ساختن دو تاس D6 تغییر می‌دهیم تا پرتاب یک جفت تاس را شبیه‌سازی کنیم. هر بار که جفت تاس را می‌اندازید دو عدد (هر یک مربوط به یکی از تاس‌ها) را با هم جمع و نتیجه را در results ذخیره می‌کنیم. نسخه‌ای از die_visual.py را به نام dice_visual.py ذخیره کنید و تغییرات زیر را اعمال کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from plotly.graph_objs import Bar, Layout
from plotly import offline
from die import Die

# Create two D6 dice.
die_1 = Die()
die_2 = Die()

# Make some rolls, and store results in a list.
results = []
for roll_num in range(1000):
    result = die_1.roll() + die_2.roll()
    results.append(result)

# Analyze the results.
frequencies = []
max_result = die_1.num_sides + die_2.num_sides
for value in range(2, max_result+1):
    frequency = results.count(value)
    frequencies.append(frequency)

# Visualize the results.
x_values = list(range(2, max_result+1))
data = [Bar(x=x_values, y=frequencies)]

x_axis_config = {'title': 'Result', 'dtick': 1}
y_axis_config = {'title': 'Frequency of Result'}
my_layout = Layout(title='Results of rolling two D6 dice 1000 times',
    xaxis=x_axis_config, yaxis=y_axis_config)

offline.plot({'data': data, 'layout': my_layout}, filename='d6_d6.html')

پس از ساختن دو نمونه از Die، تاس‌ها را پرتاب می‌کنیم و مجموع هر دو را در هر بار پرتاب محاسبه می‌کنیم. بزرگترین حاصل ممکن (۱۲) مجموع بزرگترین عدد دو تاس است که آن را در max_result ذخیره می‌کنیم. کوچکترین نتیجه‌ی ممکن (۲) مجموع کوچک‌ترین عددهای دو تاس است. هنگام تحلیل نتایج تعداد نتایج بین ۲ و max_result را می‌شماریم. (می‌توانستیم از range(2, 13) استفاده کنیم ولی تنها زمانی کاربرد داشت که فقط با دو تاس کار می‌کردیم. هنگام مدل‌سازی وضعیت‌های دنیای واقعی بهتر این است که برنامه را جوری بنویسیم تا به راحتی تغییر یافتن شرایط را مدل کند. این کد به ما امکان می‌دهد که انداختن دو تا با هر تعداد وجه را شبیه‌سازی کنیم.)

هنگام ساختن نمودار کلید dtick را به دیکشنری x_axis_config اضافه می‌کنیم. این تنظیمات فاصله‌ی بین علامت‌گذاری‌های محور x را کنترل می‌کند. حال که تعداد بیشتری میله در نمودار بافت‌نگار داریم، تنظیمات پیش‌فرض پلات‌لی تنها برچسب تعدادی از میله‌ها را نشان می‌دهد. تنظیم 'dtick': 1 به پلات‌لی می‌گوید که تمام علامت‌ها را برچسب بزند. علاوه بر این‌ها عنوان نمودار را هم به‌روز می‌کنیم و نام فایل خروجی را تغییر می‌دهیم.

پس از اجرای برنامه باید نموداری شبیه به شکل ۱۵-۱۳ ببینید.

![][image28]

شکل ۱۵-۱۳: نتایج شبیه‌سازی‌شده‌ی ۱۰۰۰ بار پرتابِ تاس‌های شش‌وجهی

این نمودار نتایج تقریبی پرتاب یک جفت تاس D6 را نشان می‌دهد. چنان‌که می‌بینید کمترین احتمال رخ دادن ۲ یا ۱۲ و بیشترین احتمال برای رخ دادن ۷ است. دلیل این اتّفاق این است که شش راه برای آمدنِ ۷ وجود دارد: ۱ و ۶، ۲ و ۵، ۳ و ۴، ۴ و ۳، ۵ و ۲ یا ۶ و ۱.

پرتاب تاس‌هایی با اندازه‌های متفاوت

حال یک تاس شش‌وجهی و یک تاس ده‌وجهی می‌سازیم و نتیجه‌ی ۵۰،۰۰۰ بار پرتاب آن‌ها را بررسی می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from plotly.graph_objs import Bar, Layout
from plotly import offline
from die import Die

# Create a D6 and a D10.
die_1 = Die()
die_2 = Die(10)

# Make some rolls, and store results in a list.
results = []
for roll_num in range(50_000):
    result = die_1.roll() + die_2.roll()
    results.append(result)

# Analyze the results.
--snip--

# Visualize the results.
x_values = list(range(2, max_result+1))
data = [Bar(x=x_values, y=frequencies)]

x_axis_config = {'title': 'Result', 'dtick': 1}
y_axis_config = {'title': 'Frequency of Result'}
my_layout = Layout(title='Results of rolling a D6 and a D10 50000 times',
    xaxis=x_axis_config, yaxis=y_axis_config)

offline.plot({'data': data, 'layout': my_layout}, filename='d6_d10.html')

برای ساختن تاس D10 نشان‌وند ۱۰ را به دومین نمونه‌ی Die می‌دهیم و حلقه‌ی اوّل را برای ۵۰،۰۰۰ پرتاب به جای ۱۰۰۰ بار تنظیم می‌کنیم. عنوان نمودار و نام فایل خروجی را نیز تغییر می‌دهیم.

شکل ۱۵-۱۴ نمودار حاصل را نشان می‌دهد. به جای یک نتیجه‌ی با بیشترین احتمال، پنج نتیجه‌ی محتمل داریم. دلیل این نتیجه این است که همچنان برای وقوع کمترین مقدار (۱ و ۱) و بیشترین مقدار (۶ و ۱۰) یک راه وجود دارد امّا تاس کوچک‌تر تعداد روش‌های تولید اعداد میانی را محدود می‌کند: شش راه برای وقوع ۷، ۸، ۹، ۱۰ و ۱۱ وجود دارد. بنابراین این اعداد محتمل‌ترین نتایج‌اند و شما به طور برابر شانس آوردن یکی از این اعداد را بیشتر از باقی اعداد دارید.

![][image29]

شکل ۱۵-۱۴: نتایج ۵۰،۰۰۰ بار پرتاب یک تاس شش‌وجهی و یک تاس ۱۰‌وجهی

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

خودآزمایی

۱۵-۶. دو تاس D8: یک شبیه‌سازی بسازید که نتایج ۱۰۰۰ بار پرتاب دو تاس هشت‌وجهی را نشان دهد. سعی کنید پیش از اجرای شبیه‌سازی تصور خود از نتیجه‌ی بصری‌سازی تصویر کنید؛ سپس ببینید که درک شهودی شما درست بوده یا نه. تعداد پرتاب‌ها را به تدریج افزایش دهید تا زمانی که به حد توانایی‌های سیستم خود برسید.

۱۵-۷. سه تاس: زمانی که سه تاس D6 پرتاب می‌کنیم کوچک‌ترین عدد ممکن ۳ و بزرگترین عدد ممکن ۱۸ خواهد بود. یک بصری‌سازی ایجاد کنید که نتایج پرتاب سه تاس D6 را نشان دهد.

۱۵-۸. ضرب: زمانی که دو تاس پرتاب می‌کنید معمولاً دو عدد حاصل با هم جمع می‌کنید. یک بصری‌سازی بسازید که نتایج پرتاب را در صورتی‌که دو عدد را به جای جمع کردن ضرب کنید نشان دهد.

۱۵-۹. دربرداری تاس: کدهای این بخش برای شفافیت بیشتر از شکل طولانی حلقه‌ی for استفاده می‌کند. اگر با به‌کارگیری روش دربرداری لیست راحت هستید، یکی یا هر دو حلقه‌ی for این برنامه‌ها را به کمک این روش بازنویسی کنید.

۱۵-۱۰. تمرین کردن با هر دو کتاب‌خانه: سعی کنید از Matplotlib برای بصری‌سازی پرتاب تاس و از پلات‌لی برای بصری‌سازی گردش تصادفی استفاده کنید. (برای تکمیل این تمرین نیاز به کمک گرفتن از مستندات هر کتابخانه دارید.)

خلاصه

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

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

در فصل ۱۶ داده‌ها را از منابع آنلاین دریافت می‌کنید و به استفاده از Matplotlib و پلات‌لی برای کاویدن آن داده‌ها ادامه خواهید داد.


2.5 - فصل شانزدهم

2.5.1 - شکل فایل CSV

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

دسترسی به داده‌ها و بصری‌سازی آن‌ها از طریق دو گون129 ه‌ی متداول ذخیره‌سازی داده، CSV و JSON خواهد بود. از ماژول csv پایتون برای پردازش داده‌های هواشناسی ذخیره‌شده به شکل CSV (مقدارهای جداشده با کاما) استفاده کرده و کمینه و بیشینه‌ی دما را در طول زمان و در دو موقعیّت مختلف تحلیل می‌کنیم. از Matplotlib برای تولید نمودارهایی بر اساس داده‌های بارگیری‌شده بهره می‌گیریم تا تغییرات دما را در دو محیط متفاوت، Sitka, Alaska و Death Valley, California نشان دهیم. در ادامه‌ی فصل از ماژول json برای دریافت داده‌های لرزه‌نگاری ذخیره‌شده به شکل JSON استفاده می‌کنیم و به کمک پلات‌لی یک نقشه‌ی جهان رسم می‌کنیم که موقعیّت‌ها و بزرگی‌های زمین‌لرزه‌های اخیر را نشان می‌دهد.

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

شکل فایل CSV

یک راه ساده برای ذخیره کردن داده‌های در فایل‌های متنی این است که آن‌ها در قالب دنباله‌ای از مقدارهای جداشده با کاما بنویسیم که مقدارهای جداشده با کاما یا CSV نامیده می‌شوند. به فایل‌های حاصل به این روش فایل‌های CSV گویند. برای مثال اینجا تکه‌ای از داده‌های هواشناسی را به شکل CSV می‌بینیم:

1
"USW00025333","SITKA AIRPORT, AK US","2018-01-01","0.45",,"48","38"

این گزیده‌ای از داده‌های هواشناسی تاریخ اول ژانویه‌ی ۲۰۱۸ در شهر سیتکای آلاسکا است و شامل بیشینه و کمینه‌ی دما و تعداد دیگری اندازه‌گیری در طول روز است. خواندن فایل‌های CSV ممکن است برای انسان‌ها مشکل باشد امّا برنامه‌های به سادگی مقدارها را از آن خوانده و پردازش می‌کنند که این امر موجب سرعت یافتن فرآیند تحلیل داده‌ها می‌شود.

ما کار را از مجموعه‌ی کوچکی از داده‌های هواشناسی به شکل CSV مربوط به شهر سیتکای آلاسکا آغاز می‌کنیم که در منابع کتاب به آدرس https://nostarch.com/pythoncrashcourse2e/ در دسترس است. پوشه‌ای به نام data داخل پوشه‌ی برنامه‌های این فصل بسازید. فایل sitka_weather_07-2018_simple.csv را در این پوشه‌ی جدید کپی کنید. (پس از بارگیری منابع کتاب به تمام فایل‌های مورد نیاز در این فصل دسترسی خواهید داشت.)

نکته
داده‌های هواشناسی این پروژه در اصل از http://ncdc.noaa.gov/cdo-web/ گرفته شده‌اند.

جداسازی عنوان‌های فایل CSV

ماژول csv پایتون کتابخانه‌ای استاندارد برای جداسازی خط‌های فایل130 CSV است که به ما امکان می‌دهد تا مقدارهایی را مد نظر داریم از آن استخراج کنیم. از بررسی اوّلین خط فایل شروع می‌کنیم که شامل دنباله‌ای از عنوان‌ها برای داده‌هاست. این عنوان‌ها به ما می‌گویند که چه اطّلاعاتی در داده‌ها وجود دارد:

1
2
3
4
5
6
7
8
9
import csv

filename = 'data/sitka_weather_07-2018_simple.csv'

with open(filename) as f:
    reader = csv.reader(f)
    header_row = next(reader)

print(header_row)

پس از وارد کردن ماژول csv نام فایلی را که روی آن کار می‌کنیم به متغیّر filename نسبت می‌دهیم. سپس فایل را باز کرده و شیء فایل حاصل را در f می‌گذاریم. سپس csv.reader() را فرامی‌خوانیم و شیء فایل را به عنوان نشان‌وند به آن می‌ده131 یم تا برای آن یک شیء خواننده بسازد. این شیء را به reader نسبت می‌دهیم.

ماژول csv تابعی به نام next() دارد که هنگام تعیین شدن شیء خواننده خط بعدی فایل را برمی‌گرداند. در برنامه‌ی قبلی next() را فقط یک بار فراخواندیم تا بتوانیم خط اوّل فایل را که شامل عنوان‌های فایل است استخراج کنیم. داده‌هایی را که برگردانده شده در header_row ذخیره می‌کنیم. چنانکه می‌بینید header_row شامل عنوان‌هایی معنادار و مربوط به هواشناسی است که به ما می‌گوید در هر خط داده چه اطّلاعاتی ذخیره شده است:

1
['STATION', 'NAME', 'DATE', 'PRCP', 'TAVG', 'TMAX', 'TMIN']

شیء reader خط اول مقدارهای جداشده با کاما را در فایل پردازش می‌کند و هر کدام را به عنوان عضو یک لیست ذخیره می‌کند. عنوان STATION بیانگر کد ایستگاه هواشناسی‌ای‌ست که این داده‌ها را ثبت کرده است. موقعیّت این عنوان به ما می‌گوید که اوّلین مقدار هر خط کد ایستگاه هواشناسی خواهد بود. عنوان بعدی NAME است که نام ایستگاه هواشناسی ثبت‌کننده‌ی داده را معلوم می‌کند. باقی عنوان‌ها انواع مختلف اطّلاعاتی که در هر قرائت ذخیره شده نشان می‌دهد. داده‌هایی که فعلاً مورد نظر ماست تاریخ، بیشینه‌ی دما (TMAX) و کمینه‌ی دما (TMIN) است. این مجموعه‌داده ساده‌ای است که تنها شامل داده‌های مربوط به دما و بارندگی است. اگر خودتان داده‌های هواشناسی را بارگیری کنید می‌توانید اندازه‌گیری‌های دیگری همچون سرعت و جهت باد و داده‌های جزئی‌تر بارندگی را نیز داشته باشید.

چاپ کردن عنوان‌ها و موقعیّت آن‌ها

برای درک ساده‌تر عنوان‌های فایل هر کدام را به همراه موقعیّت آن‌ها را در لیست چاپ می‌کنیم:

1
2
3
4
5
6
7
8
--snip--

with open(filename) as f:
    reader = csv.reader(f)
    header_row = next(reader)

    for index, column_header in enumerate(header_row):
        print(index, column_header)

تابع enumerate شماره و مقدار هر عضو لیستی را که در آن حلقه می‌زنید برمی‌گرداند. (توجّه کنید که خط print(header_row) را حذف کرده‌ایم چون جزئیات بیشتر آن را اینجا می‌بینیم.)

اینجا شاخص هر عنوان را در خروجی می‌بینیم:

1
2
3
4
5
6
7
0 STATION
1 NAME
2 DATE
3 PRCP
4 TAVG
5 TMAX
6 TMIN

چنان‌که می‌بینیم تاریخ‌ها و بیشینه‌ی دمای آن‌ها در ستون‌های ۲ تا ۵ ذخیره شده است. برای بررسی این داده‌ها هر سطر داده‌های sitka_weather_07-2018_simple.csv را پردازش و مقدار شاخص‌های ۲ تا ۵ را استخراج می‌کنیم.

استخراج و خواندن داده‌ها

اکنون که می‌دانیم به کدام ستون‌ها نیاز داریم، بیایید چند داده را بخوانیم. ابتدا بیشینه‌ی دمای هر روز را می‌خوانیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
--snip--

with open(filename) as f:
    reader = csv.reader(f)
    header_row = next(reader)

    # Get high temperatures from this file.
    highs = []
    for row in reader:
        high = int(row[5])
        highs.append(high)

print(highs)

لیستی خالی به نام highs می‌سازیم و سپس در باقیمانده‌ی سطرهای فایل حلقه می‌زنیم. شیء خواننده از جای قبلی در فایل CSV ادامه می‌دهد و به طور خودکار هر خط را به همراه جایگاه آن برمی‌گرداند. چون قبلاً سطر عنوان را خوانده‌ایم حلقه از دومین خط آغاز می‌شود یعنی جایی که اصل داده‌ها شروع می‌شوند. در هر اجرای حلقه پنجمین داده را از سطر بیرون می‌کشیم که متناظر با عنوان TMAX است و سپس آن را در متغیّر high می‌گذاریم. به کمک تابع int() داده را که به صورت رشته ذخیره شده به شکل عددی تبدیل می‌کنیم تا بتوانیم از آن استفاده کنیم. سپس آن را به لیست highs الحاق می‌کنیم.

در ادامه داده‌هایی را که اکنون در highs ذخیره شده‌اند می‌بینیم:

1
2
[62, 58, 70, 70, 67, 59, 58, 62, 66, 59, 56, 63, 65, 58, 56, 59, 64, 60, 60,
61, 65, 65, 63, 59, 64, 65, 68, 66, 64, 67, 65]

ما بیشینه‌ی دمای هر روز را استخراج و در لیستی ذخیره کرده‌ایم. اکنون بیایید این داده‌ها را بصری کنیم.

2.5.2 - رسم داده‌ها در یک نمودار دما

برای بصری کردن داده‌های دمایی که در اختیار داریم، ابتدا مانند زیر به کمک Matplotlib نمودار ساده‌ای از بیشینه‌های دما می‌سازیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import csv
import matplotlib.pyplot as plt

filename = 'data/sitka_weather_07-2018_simple.csv'

with open(filename) as f:
    --snip--

    # Plot the high temperatures.
    plt.style.use('seaborn')
    fig, ax = plt.subplots()
    ax.plot(highs, c='red')

    # Format plot.
    plt.title("Daily high temperatures, July 2018", fontsize=24)
    plt.xlabel('', fontsize=16)
    plt.ylabel("Temperature (F)", fontsize=16)
    plt.tick_params(axis='both', which='major', labelsize=16)

plt.show()

لیست بیشینه‌ی دما را به plot() می‌دهیم و c='red' را تعیین می‌کنیم تا نقطه‌ها را به رنگ قرمز رسم کند. (بیشینه‌ی دما را قرمز و کمینه‌ی آن را آبی رسم خواهیم کرد.) سپس تعدادی دیگر از جزئیات شکل‌دهی مانند عنوان، اندازه‌ی متن و برچسب‌ها را تعیین می‌کنیم که آن‌ها را از فصل ۱۵ بشناسید. چون هنوز تاریخ‌ها را اضافه نکرده‌ایم عنوانی برای محور x تعیین نمی‌کنیم امّا اندازه‌ی متن آن را برای افزایش خوانایی برچسب‌های پیش‌فرض تغییر می‌دهیم. شکل ۱۶-۱ نمودار حاصل را نشان می‌دهد: یک نمودار خطّی ساده مربوط به بیشینه‌های دما در ژوئیه‌ی ۲۰۱۸ در سیتکای آلاسکا.

شکل ۱۶-۱- نموداری خطّی که بیشینه‌ی دمای روزانه در ماه ژوئیه‌ی سال ۲۰۱۸ در سیتکای آلاسکا نشان می‌دهد.

2.5.3 - ماژول datetime

حال تاریخ‌ها را به نمودار خود اضافه می‌کنیم تا آن را کاربردی‌تر کنیم. اوّلین تاریخ داده‌های هواشناسی در دومین سطر فایل قرار دارد.

داده به صورت رشته خوانده خواهد شد پس باید رشته‌ی "2018-07-01" را به شی‌ای که بیانگر این تاریخ است تبدیل کنیم. می‌توانیم شیء بیانگر تاریخ اول ژوئیه‌ی ۲۰۱۸ را به کمک متد strptime() از ماژول datetime بسازیم. ابتدا در نشست ترمینال ببینیم که strptime() چگونه کار می‌کند:

1
2
3
4
from datetime import datetime

first_date = datetime.strptime('2018-07-01', '%Y-%m-%d')
print(first_date)
1
2018-07-01 00:00:00

ابتدا کلاس datetime را از ماژول datetime وارد می‌کنیم. سپس متد strptime() را فراخوانی می‌کنیم و رشته‌ی حاوی تاریخ را به عنوان یکمین نشان‌وند به آن می‌دهیم. دومین نشان‌وند به پایتون می‌گوید که تاریخ را به چه شکلی درآورد. در این مثال پایتون '%Y-' را اینجور تفسیر می‌کند که قسمتی از رشته که پشت اوّلین خطّ فاصله قرار دارد چهار رقمِ سال است؛ '%m-' به این معنی است که آن بخش از رشته که قبل از دومین خطّ فاصله قرار گرفته عددی بیانگر ماه است و '%d' می‌گوید که آخرین قسمت رشته چندمین روز ماه را نشان می‌دهد، از ۱ تا ۳۱.

متد strptime() می‌تواند گسترده‌ای از نشان‌وندها را برای تفسیر تاریخ بپذیرد. جدول ۱۶-۱ برخی از این نشان‌وندها را نشان می‌دهد.

جدول ۱۶-۱- نشان‌وندهای شکل‌دهی تاریخ و ساعت از ماژول datetime

نشان‌وند معنی
%A نام روز هفته مثل Monday
%B نام ماه مانند January
%m ماه، به صورت عددی از 01 تا 12
%d روز ماه به صورت عددی از 01 تا 31
%Y سال چهاررقمی مثل 2019
%y سال دورقمی مثل 19
%H ساعت در شکل ۲۴ ساعتی از 00 تا 23
%I ساعت در شکل ۱۲ ساعتی از 01 تا 12
%p AM یا PM
%M دقیقه‌ها (00 تا 59)
%S ثانیه‌ها (00 تا 61)

2.5.4 - رسم تاریخ‌ها

اکنون می‌توانیم نمودار داده‌های دمایی خود را با استخراج تاریخِ بیشینه‌های روزانه بهبود بدهیم و آن‌ها را مانند زیر به همراه بیشینه‌ی دما به plot() بدهیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import csv
from datetime import datetime
import matplotlib.pyplot as plt

filename = 'data/sitka_weather_07-2018_simple.csv'

with open(filename) as f:
    reader = csv.reader(f)
    header_row = next(reader)

    # Get dates and high temperatures from this file.
    dates, highs = [], []
    for row in reader:
        current_date = datetime.strptime(row[2], '%Y-%m-%d')
        high = int(row[5])
        dates.append(current_date)
        highs.append(high)

# Plot the high temperatures.
plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.plot(dates, highs, c='red')

# Format plot.
plt.title("Daily high temperatures, July 2018", fontsize=24)
plt.xlabel('', fontsize=16)
fig.autofmt_xdate()
plt.ylabel("Temperature (F)", fontsize=16)
plt.tick_params(axis='both', which='major', labelsize=16)

plt.show()

دو لیست خالی برای ذخیره‌سازی تاریخ و بیشینه‌ی دما می‌سازیم. سپس داده‌های شامل اطّلاعات تاریخی (row[2]) را به شیء datetime تبدیل می‌کنیم و آن را به dates الحاق می‌کنیم. تاریخ‌ها و بیشینه‌های دما را به plot() می‌دهیم. فراخوانی fig.autofmt_xdate() برچسب‌های تاریخ را به صورت مورّب رسم می‌کند تا از هم‌پوشانی آن‌ها را جلوگیری کند. شکل ۱۶-۲ نمودار بهبودیافته را نشان می‌دهد.

شکل ۱۶-۲- نمودار که اکنون تاریخ‌ها را بر محور x خود دارد معنادارتر است.

2.5.5 - رسم یک دوره‌ی زمانی طولانی‌تر

حال که نمودار ما راه افتاده اجازه دهید داده‌های بیشتر اضافه کنیم تا تصویر کامل‌تری از آب و هوای سیتکا داشته باشیم. فایل sitka_weather_2018_simple.csv را که شامل اطّلاعات کامل آب و هوایی یک‌ساله‌ی سیتکا است در پوشه‌ای کپی کنید که داده‌های برنامه‌های این فصل را نگه می‌دارید.

اکنون می‌توانیم نموداری برای آب و هوای یک سال کامل را رسم کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--snip--

filename = 'data/sitka_weather_2018_simple.csv'

with open(filename) as f:
    --snip--

    # Format plot.
    plt.title("Daily high temperatures - 2018", fontsize=24)
    plt.xlabel('', fontsize=16)
    --snip--

نام فایل را تغییر می‌دهیم تا از فایل داده‌ی جدید sitka_weather_2018_simple.csv استفاده کنیم و عنوان نمودار خود را عوض می‌کنیم تا بیانگر تغییر محتویّات آن باشد. شکل ۱۶-۳ نمودار حاصل را نشان می‌دهد.

شکل ۱۶-۳- داده‌های یک ساله

2.5.6 - رسم یک دنباله‌ی داده‌ی دیگر

می‌توانیم با اضافه کردن کمینه‌ی دما نمودار گویای خود را از این هم سودمندتر کنیم. باید داده‌های مربوط به کمینه‌ی دما را از فایل داده‌ها استخراج کنیم و آن‌ها را نمودار خود بیفراییم، مانند زیر:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
--snip--

filename = 'sitka_weather_2018_simple.csv'

with open(filename) as f:
    reader = csv.reader(f)
    header_row = next(reader)

    # Get dates, and high and low temperatures from this file.
    dates, highs, lows = [], [], []
    for row in reader:
        current_date = datetime.strptime(row[2], '%Y-%m-%d')
        high = int(row[5])
        low = int(row[6])
        dates.append(current_date)
        highs.append(high)
        lows.append(low)

# Plot the high and low temperatures.
plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.plot(dates, highs, c='red')
ax.plot(dates, lows, c='blue')

# Format plot.
plt.title("Daily high and low temperatures - 2018", fontsize=24)
--snip--

در ابتدا لیست lows را برای نگهداری دماهای کمینه اضافه می‌کنیم و سپس این داده را از موقعیّت هفتم هر سطر (row[6]) استخراج و ذخیره می‌کنیم. سپس یک فراخوانی plot() دیگر برای دماهای کمینه اضافه می‌کنیم و این مقادیر را آبی می‌کنیم. در نهایت عنوان را به‌روز می‌کنیم. شکل ۱۶-۴ نمودار حاصل را نشان می‌دهد.

شکل ۱۶-۴- دو دنباله‌ی داده در یک نمودار

2.5.7 - سایه زدن سطحی در نمودار

اکنون با اضافه کردن دو دنباله‌ی داده می‌توانیم گستره‌ی دمایی هر روز را بررسی کنیم. اجازه دهید آخرین تغییر را در نمودار ایجاد کنیم و با سایه زدن محدوده‌ی بین بالاترین و پایین‌ترین دمای هر روز را نشان دهیم. برای این کار از متد fill_between() استفاده خواهیم کرد که دنباله‌ای از مقدارهای x و دو دنباله‌ی مقدارهای y را دریافت کرده و فاصله‌ی بین آن‌ها را پر می‌کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--snip--

# Plot the high and low temperatures.
plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.plot(dates, highs, c='red', alpha=0.5)
ax.plot(dates, lows, c='blue', alpha=0.5)
plt.fill_between(dates, highs, lows, facecolor='blue', alpha=0.1)

--snip--

نشان‌وند alpha میزان محو بودن یک رنگ را کنترل می‌کند. مقدار alphaی ۰ کاملاً شفّاف و ۱ (مقدار پیش‌فرض) کاملاً مات است. با تنظیم مقدار ۰٫۵ برای alpha خط‌های آبی و قرمز نمودار را روشن‌تر می‌کنیم.

لیست dates را به عنوان مقدارهای x و سپس دو لیست highs و lows را به جای مقدارهای y به fill_between() بدهید. نشان‌وند facecolor رنگ ناحیه‌ی سایه‌خورده را مشخّص می‌کند؛ مقدار alphaی آن را ۰٫۱ می‌گذاریم تا ناحیه‌ی پرشده بدون منحرف کردن توجّه از اطّلاعات نمایش‌داده‌شده، دو منحنی را به هم وصل کند. شکل ۱۶-۵ نمودار را با ناحیه‌ی سایه‌خورده بین مقدارهای کمینه و بیشینه نشان می‌دهد.

شکل ۱۶-۵- ناحیه‌ی بین دو مجموعه داده سایه خورده است.

این سایه‌زنی کمک می‌کند که ناحیه‌ی بین دو مجموعه‌داده بلافاصله به چشم بیاید.

2.5.8 - بررسی خطاها

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

برای مثال ببینیم که در صورت تلاش برای تولید نمودار دما برای دره‌ی مرگ در کالیفرنیا چه رخ می‌دهد. فایل death_valley_2018_simple.csv را به پوشه‌ای که داده‌های برنامه‌های این فصل قرار داد منتقل کنید.

ابتدا برای دیدن عنوان داده‌هایی که در این فایل آمده کد زیر را اجرا کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import csv

filename = 'data/death_valley_2018_simple.csv'

with open(filename) as f:
    reader = csv.reader(f)
    header_row = next(reader)

    for index, column_header in enumerate(header_row):
        print(index, column_header)

خروجی چنین است:

1
2
3
4
5
6
7
0 STATION
1 NAME
2 DATE
3 PRCP
4 TMAX
5 TMIN
6 TOBS

جایگاه تاریخ همانند قبل شاخص ۲ است. امّا جایگاه دماهای کمینه و بیشینه ۴ و ۵ است پس باید شماره‌ی شاخص‌ها را در برنامه‌ی خود تغییر دهیم تا این جایگاه‌های جدید اعمال شوند. به جای قرائت دمای میانگین برای یک روز، این ایستگاه TOBS را ارسال می‌کند که بیانگر زمان خاص مشاهده و خواندن داده است.

من یکی از قرائت‌های دما را از این فایل حذف کرده‌ام تا نشان دهم که با فقدان داده‌ای در فایل چه رخ می‌دهد. با در نظر گرفتن تغییراتی که در شماره‌ی شاخص‌ها در بالا اشاره شد، برنامه‌ی sitka_highs_lows.py را برای تولید نموداری برای دره‌ی مرگ تغییر دهید و ببینید چه رخ می‌دهد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
--snip--

filename = 'data/death_valley_2018_simple.csv'

with open(filename) as f:
    --snip--

    # Get dates, and high and low temperatures from this file.
    dates, highs, lows = [], [], []
    for row in reader:
        current_date = datetime.strptime(row[2], '%Y-%m-%d')
        high = int(row[4])
        low = int(row[5])
        dates.append(current_date)
        --snip--

شاخص‌ها را برای تطابق با جایگاه TMAX و TMIN این فایل تغییر می‌دهیم.

زمانی که دوباره برنامه را اجرا می‌کنیم به خطا برمی‌خوریم؛ چنان‌که در آخرین خط خروجی زیر نمایان است:

1
2
3
4
Traceback (most recent call last):
  File "death_valley_highs_lows.py", line 15, in <module>
      high = int(row[4])
ValueError: invalid literal for int() with base 10: ''

این خروجی نشان می‌دهد که پایتون نمی‌تواند بیشینه‌ی دمای یکی از روزها را پردازش کند زیرا تبدیل رشته‌ی خالی ('') به عدد صحیح ممکن نیست. به جای گشتن به دنبال داده‌ی مفقود و فهمیدن این که کدام قرائت جا افتاده، موارد فقدان داده را مستقیماً مدیریّت می‌کنیم.

زمانی که مقدارها را از فایل CSV می‌خوانیم کد بررسی خطا را اجرا می‌کنیم تا استثناهای احتمالی را مدیریت کنیم. این کار به این صورت است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
--snip--

filename = 'data/death_valley_2018_simple.csv'

with open(filename) as f:
    --snip--

    for row in reader:
        current_date = datetime.strptime(row[2], '%Y-%m-%d')
        try:
            high = int(row[4])
            low = int(row[5])
        except ValueError:
            print(f"Missing data for {current_date}")
        else:
            dates.append(current_date)
            highs.append(high)
            lows.append(low)

    # Plot the high and low temperatures.
    --snip--

    # Format plot.
    title = "Daily high and low temperatures - 2018\nDeath Valley, CA"
    plt.title(title, fontsize=20)
    plt.xlabel('', fontsize=16)
    --snip--

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

اگر اکنون death_valley_highs_lows.py را اجرا کنید خواهید دید که تنها یک داده از دست رفته:

1
Missing data for 2018-02-18 00:00:00

چون خطا به درستی مدیریّت شده کد ما قادر است که نموداری تولید کند و از داده‌ی مفقود بگذرد. شکل ۱۶-۶ نمودار حاصل را نشان می‌دهد.

شکل ۱۶-۶- دماهای بیشینه و کمینه‌ی روزانه برای دره‌ی مرگ

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

بسیاری از مجموعه‌داده‌های مورد نظر شما شامل داده‌های مفقود، داده‌هایی که شکل درست ندارند یا داده‌های نادرست‌اند. شما می‌توانید به کمک ابزارهایی که در نیمه‌ی اوّل این کتاب آموختید این شرایط را مدیریّت کنیم. اینجا از بلوک try-except-else برای رسیدگی به داده‌ی مفقود استفاده کردیم. گاهی شما از continue برای پریدن از روی برخی داده‌ها یا از remove() یا del() برای حذف کردن برخی از داده‌های استخراج‌شده استفاده می‌کنید. هر رویکردی را که کار می‌کند پی بگیرید تا زمانی که نتیجه‌ی آن بصری‌سازی دقیق و معنادار باشد.

2.5.9 - بارگذاری داده‌هایی برای خود

اگر می‌خواهید داده‌های هواشناسی خود را بارگذاری کنید این گام‌ها را دنبال کنید:

۱- از سایت داده‌های اقلیمی آنلاین NOAA را به آدرس https://www.ncdc.noaa.gov/cdo-web/ بازدید کنید. در بخش Discover Data By روی Search Tool کلیک کنید. در جعبه‌ی Select a Dataset گزینه‌ی Daily Summaries را برگزینید.

۲- بازه‌ی زمانی دلخواه را انتخاب کنید و در بخش Search For، ZIP Codes را انتخاب کنید. کد ZIP مورد نظر خود را وارد کنید و روی Search کلیک کنید.

۳- در صفحه‌ی بعد یک نقشه و اطّلاعاتی در مورد محل مورد نظر شما خواهید دید. پایین نام محل روی View Full Details کلیک کنید یا روی نقشه کلیک کرده و سپس Full Details را انتخاب کنید.

۴- پایین‌تر بروید و روی Station List کلیک کنید تا ایستگاه‌های هواشناسی موجود در آن منطقه را ببینید. یکی از آن‌ها را انتخاب کنید و روی Add to Cart کلیک کنید. هرچند این سایت از نمایه‌ی سبد خرید استفاده کرده است، امّا این داده‌ها رایگان است. روی سبد خرید در گوشه‌ی بالا سمت راست کلیک کنید.

۵- در Select the Output، گزینه‌ی Custom GHCN-Daily CSV را انتخاب کنید. مطمئین شوید که بازه‌ی تاریخ درست است و روی Continue کلیک کنید.

۶- در صفحه‌ی بعد می‌توانید انواع داده‌های مورد نیاز خود را انتخاب کنید. می‌توان تنها یک نوع داده، مثلاً با تمرکز بر دمای هوا را بگیرید یا کل داده‌های موجود یک ایستگاه را بارگذاری کنید. انتخاب‌های خود را انجام دهید و روی Continue کلیک کنید.

۷- در آخرین صفحه خلاصه‌ای از سفارش خود خواهید دید. آدرس ایمیل خود را وارد کرده و Submit Order را بزنید. تاییدیه‌ای دریافت می‌کنید که نشان می‌دهد سفارش دریافت شده و ظرف چند دقیقه ایمیل دیگری شامل پیوندی برای بارگذاری داده‌های خود خواهید گرفت.

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

خودآزمایی

۱۶-۱- بارندگی در سیتکا: سیتکا جنگلی بارانی و معتدل است بنابراین بارندگی در آن نسبتاً زیاد است. در فایل داده‌ی sitka_weather_2018_simple.csv عنوانی به نام PRCP دارد که بیانگر میزان بارش روزانه است. بصری‌سازی‌ای با تمرکز بر داده‌های این ستون درست کنید. اگر درباره‌ی کم بودن میزان بارندگی در بیابان کنجکاوید، می‌توانید تمرین را برای دره‌ی مرگ هم تکرار کنید.

۱۶-۲- مقایسه‌ی سیتکا و دره‌ی مرگ: مقیاس‌های دما در نمودارهای سیتکا و دره‌ی مرگ بازتاب‌دهنده‌ی تفاوت گسترده‌های دمایی است. برای مقایسه‌ی دقیق گستره‌ی دمایی بین سیتکا و دره‌ی مرگ به مقیاس‌های مشابه بر روی محور y نیاز دارید. تنظیمات محور y را در یکی یا هر دوی نمودارهای ۱۶-۵ و ۱۶-۶ تغییر دهید. سپس مقایسه‌ای مستقیم بین گستره‌ی دمایی سیتکا و دره‌ی مرگ (یا هر دو نقطه‌ی دیگر که می‌خواهید) انجام دهید.

۱۶-۳- سان فرانسیسکو: دماها در سان‌فرانسیسکو بیشتر به سیتکا شبیه است یا به دره‌ی مرگ؟ داده‌هایی در مورد سان‌فرانسیسکو بارگذاری کنید و یک نمودار بیشینه‌-کمینه‌ی دما برای آن تولید کنید تا بتوان مقایسه‌ای انجام داد.

۱۶-۴- شاخص‌های خودکار: در این بخش شاخص‌های مربوط به ستون‌های TMIN و TMAX را دستی تعیین کردیم. از سطر عنوان‌ها برای تعیین این شاخص‌ها برای مقدارها استفاده کنید تا برنامه‌ی شما برای هر دو جا، سیتکا یا دره‌ی مرگ، کار کند. از نام ایستگاه هواشناسی هم برای تولید خودکار عنوان مناسب برای نمودار خود استفاده کنید.

۱۶-۵- کند و کاو: چند بصری‌سازی دیگر بسازید تا دیگر جنبه‌های آب و هوایی مورد علاقه‌ی خود را در هر موقعیّتی که درباره‌ی آن کنجکاو هستید، بررسی کنید.

2.5.10 - رسم نقشه‌ی مجموعه‌ی داده‌های جهانی: قالب JSON

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

بارگذاری داده‌های زمین‌لرزه

فایل eq_1_day_m1.json را به پوشه‌ای منتقل کنید که در آن داده‌های این فصل را ذخیر132 ه کرده‌اید. زمین‌لرزه‌ها بر اساس بزرگی آن‌ها در مقیاس ریشتر دسته‌بندی می‌شوند. این فایل شامل داده‌های تمام زمین‌لرزه‌ها با بزرگی بزرگتر از M1 است که در ۲۴ ساعت گذشته رخ داده‌اند. (در زمان نگارش این متن) این داده از یکی منابع داده‌ی زمین‌لرزه مربوط به مطالعات زمین‌شناسی ایالات متحده آمده است که می‌توانید آن را در آدرس https://earthquake.usgs.gov/earthquakes/feed/ پیدا کنید.

بررسی داده‌ی JSON

زمانی که فایل eq_1_day_m1.json را باز می‌کنید خواهید دیدید که محتویّات آن بسیار فشرده و خواندن آن‌ها مشکل است:

1
2
3
4
5
6
7
{"type":"FeatureCollection","metadata":{"generated":1550361461000,...
{"type":"Feature","properties":{"mag":1.2,"place":"11km NNE of Nor...
{"type":"Feature","properties":{"mag":4.3,"place":"69km NNW of Ayn...
{"type":"Feature","properties":{"mag":3.6,"place":"126km SSE of Co...
{"type":"Feature","properties":{"mag":2.1,"place":"21km NNW of Teh...
{"type":"Feature","properties":{"mag":4,"place":"57km SSW of Kakto...
--snip--

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import json

# Explore the structure of the data.
filename = 'data/eq_data_1_day_m1.json'

with open(filename) as f:
    all_eq_data = json.load(f)

readable_file = 'data/readable_eq_data.json'

with open(readable_file, 'w') as f:
    json.dump(all_eq_data, f, indent=4)

ابتدا ماژول json را وارد می‌کنیم تا داده‌ها را به درستی از فایل بخوانیم و کل این مجموعه‌داده را در all_eq_data ذخیره کنیم. تابع json.load() داده را به شکلی تبدیل می‌کند که پایتون بتواند با آن کار کند: در این مورد یک دیکشنری عظیم. سپس فایلی می‌سازیم که همان داده‌ها را به شکلی خواناتر در آن ذخیره کنیم. تابع json.dump() یک شیء داده‌ی JSON و یک شیء فایل دریافت می‌کند و داده را در فایل می‌نویسد. نشان‌وند indent=4 به تابع dump() می‌گوید که داده‌ها را با تورفتگی شکل دهد تا با ساختار داده جور دربیاید.

اکنون که در پوشه‌ی data فایل readable_eq_data.json را یافته و باز کنید، بخش اوّل آن را مانند زیر خواهید دید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
    "type": "FeatureCollection",
    "metadata": {
        "generated": 1550361461000,
        "url": "https://earthquake.usgs.gov/earthquakes/.../1.0_day.geojson",
        "title": "USGS Magnitude 1.0+ Earthquakes, Past Day",
        "status": 200,
        "api": "1.7.0",
        "count": 158
    },
    "features": [
--snip--

بخش اوّل فایل قسمتی را با کلید "metadata" دربرمی‌گیرد. این قسمت به ما می‌گوید که فایل داده چه زمانی تولید شده و کجا می‌توانیم آن را به صورت آنلاین پیدا کنیم. همچنین عنوانی قابل خواندن و تعداد زمین‌لرزه‌های موجود در فایل هم در این بخش مشخّص می‌شود. در یک دوره‌ی ۲۴ ساعته ۱۵۸ زمین‌لرزه ثبت شده است.

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

بیایید به یکی از دیکشنری‌های مربوط به یک زمین‌لرزه نگاهی بیندازیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
--snip--
{
    "type": "Feature",
    "properties": {
        "mag": 0.96,
        --snip--
        "title": "M 1.0 - 8km NE of Aguanga, CA"
    },
    "geometry": {
        "type": "Point",
        "coordinates": [
            -116.7941667,
            33.4863333,
            3.22
        ]
    },
    "id": "ci37532978"
},

کلید "properties" اطّلاعات بسیاری درباره‌ی هر زمین‌لرزه دارد. ما بیشتر به بزرگی هر زمین‌لرزه علاقه‌مندیم که در کلید مربوط به آن، "mag" قرار دارد. همچنین عنوان هر زمین‌لرزه هم برای ما جالب است که شامل خلاصه‌ای زیبا از بزرگی و موقعیّت آن است.

کلید "geometry" به ما کمک می‌کند که محل وقوع زمین‌لرزه را پیدا کنیم. این اطّلاعات برای رسم هر رخداد ضروری است. می‌توانیم طول و عرض جغرافیایی هر زمین‌لرزه را در لیستی با کلید "coordinates" پیدا کنیم.

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

نکته
زمانی که درباره‌ی موقعیت‌ها صحبت می‌کنیم غالباً عرض جغرافیایی را ابتدا و سپس طول جغرافیایی را می‌آوریم. این قرارداد احتمالاً به این دلیل به وجود آمده که انسان‌ها عرض جغرافیایی را بسیار پیش‌تر از مفهوم طول جغرافیایی توسعه داده‌اند. با این حال بسیاری از چارچوب‌های جغرافیایی ابتدا طول و سپس عرض جغرافیایی را می‌آورند تا با قرارداد (x,y) در بیان‌های ریاضیاتی متناظر باشد. قالب geoJSON از قرارداد (طول، عرض) پیروی می‌کند و اگر شما از چارچوب دیگری استفاده می‌کنید مهم است که قرارداد مورد استفاده‌ی آن را یاد بگیرید.

2.5.11 - ساختن لیست تمام زمین‌لرزه‌ها

ابتدا لیستی می‌سازیم که شامل تمام اطّلاعات هر زمین‌لرزه‌ی رخ‌داده باشد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import json

# Explore the structure of the data.
filename = 'data/eq_data_1_day_m1.json'

with open(filename) as f:
    all_eq_data = json.load(f)

all_eq_dicts = all_eq_data['features']

print(len(all_eq_dicts))

ما داده‌های متناظر با کلید 'features' را گرفته و همه را در all_eq_dicts ذخیره می‌کنیم. می‌دانیم که این فایل داده‌های ۱۵۸ زمین‌لرزه را در بر گرفته و خروجی تایید می‌کند که داده‌ی تمام زمین‌لرزه‌های داخل فایل را گرفته‌ایم:

1
158

توجّه کنید که این کد چه‌قدر کوتاه است. فایل readable_eq_data.json که آن را شکلی مرتّب درآوردیم بیشتر از ۶۰۰۰ خط است. امّا توانستیم در تنها چند خط کد تمام آن داده‌ها را خوانده و در یک لیست پایتون ذخیره کنیم. در ادامه بزرگی هر زمین‌لرزه را بیرون می‌کشیم.

استخراج بزرگی زمین‌لرزه‌ها

می‌توانیم در لیستی که شامل داده‌های هر زمین‌لرزه است، حلقه بزنیم و هر اطّلاعاتی را که بخواهیم استخراج کنیم. اکنون بزرگی هر زمین‌لرزه را بیرون می‌کشیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--snip--

all_eq_dicts = all_eq_data['features']

mags = []
for eq_dict in all_eq_dicts:
    mag = eq_dict['properties']['mag']
    mags.append(mag)

print(mags[:10])

لیستی خالی برای ذخیره کردن بزرگی‌ها می‌سازیم و سپس در دیکشنری all_eq_dicts حلقه می‌زنیم. داخل این حلقه هر زلزله با دیکشنری eq_dict نمایش داده می‌شود. بزرگی هر زلزله در بخش 'properties' دیکشنری ذیل کلید 'mag' ذخیره شده است. هر بزرگی را در متغیّر mag ذخیره و سپس آن را به لیست mags الحاق می‌کنیم.

۱۰ بزرگی اوّل را چاپ می‌کنیم تا ببینیم که داده‌های صحیح را گرفته‌ایم یا نه:

1
[0.96, 1.2, 4.3, 3.6, 2.1, 4, 1.06, 2.3, 4.9, 1.8]

در ادامه داده‌های موقعیّت هر زمین‌لرزه را به دست می‌آوریم تا پس از آن بتوانیم نقشه‌ی زمین‌لرزه‌ها را بسازیم.

استخراج داده‌ی موقعیّت

داده‌ی موقعیّت ذیل کلید "geometry" ذخیره شده است. داخل این دیکشنری کلید "coordinates" وجود دارد و دو مقدار اوّل این لیست طول و عرض جغرافیایی‌ست.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
--snip--

all_eq_dicts = all_eq_data['features']

mags, lons, lats = [], [], []
for eq_dict in all_eq_dicts:
    mag = eq_dict['properties']['mag']
    lon = eq_dict['geometry']['coordinates'][0]
    lat = eq_dict['geometry']['coordinates'][1]
    mags.append(mag)
    lons.append(lon)
    lats.append(lat)

print(mags[:10])
print(lons[:5])
print(lats[:5])

دو لیست خالی برای طول و عرض جغرافیایی می‌سازیم. کد eq_dict['geometry'] به دیکشنری‌ای دست می‌یابد که بیانگر عنصر هندسی زمین‌لرزه است. کلید دوم، 'coordinates' لیست مقدارهای مرتبط با 'coordinates' را بیرون می‌کشد. سرانجام شاخص صفرم اوّلین مقدار در لیست مختصّات را درخواست می‌کند که متناظر با طول جغرافیایی زمین‌لرزه است.

زمانی که پنج طول و عرض جغرافیایی اوّل را چاپ می‌کنیم خروجی نشان از استخراج صحیح داده‌ها دارد:

1
2
3
4
5
[0.96, 1.2, 4.3, 3.6, 2.1, 4, 1.06, 2.3, 4.9, 1.8]

[-116.7941667, -148.9865, -74.2343, -161.6801, -118.5316667]

[33.4863333, 64.6673, -12.1025, 54.2232, 35.3098333]

با این داده‌ها می‌توانیم به سمت نگاشت هر زمین‌لرزه حرکت کنیم.

2.5.12 - ساختن یک نقشه‌ی جهان

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

این نقشه‌ی اوّلیّه است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import json
from plotly.graph_objs import Scattergeo, Layout
from plotly import offline

--snip--

for eq_dict in all_eq_dicts:
    --snip--

# Map the earthquakes.
data = [Scattergeo(lon=lons, lat=lats)]

my_layout = Layout(title='Global Earthquakes')

fig = {'data': data, 'layout': my_layout}
offline.plot(fig, filename='global_earthquakes.html')

نمودار Scattergeo و کلاس Layout و سپس ماژول offline را برای ساختن نقشه وارد می‌کنیم. مشابه کاری که برای ساختن نمودار میله‌ای کردیم، لیستی به نام data تعریف می‌کنیم. داخل این لیست شیء Scattergeo را می‌سازیم زیرا شما می‌توانید در هر بصری‌سازی بیش از یک مجموعه‌داده را رسم کنید. نمودار از نوع Scattergeo به شما امکان می‌دهد که نمودار پراکندگی داده‌های جغرافیایی را روی یک نقشه بیندازید. این ساده‌ترین روش استفاده از این نوع نمودار است، کافی‌ست شما لیستی از طول‌ها و لیستی از عرض‌های جغرافیایی را ارائه کنید.

به نمودار عنوانی مناسب می‌دهیم و یک دیکشنری به نام fig شامل داده و چیدمان آن می‌سازیم. در نهایت fig را به همراه یک نام فایل گویا به plot() می‌دهیم. زمانی که این فایل را اجرا می‌کنید باید نقشه‌ای مشابه شکل ۱۶-۷ ببینید. زمین‌لرزه‌ها معمولاً در نزدیکی مرز صفحه‌های زمین رخ می‌دهند و این با آن‌چه در نمودار می‌بینیم مطابقت دارد.

شکل ۱۶-۷: نقشه‌ی ساده‌ای که محلّ وقوع تمام زمین‌لرزه‌های ۲۴ ساعت گذشته را نشان می‌دهد.

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

روش دیگر تعیین داده‌های نمودار

پیش از تنظیم نمودار بیایید نگاهی به روش کمی متفاوتی برای تعیین داده‌های نمودارهای پلات‌لی بیندازیم. در نمودار فعلی لیست data در یک خط تعریف می‌شود:

1
data = [Scattergeo(lon=lons, lat=lats)]

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

1
2
3
4
5
data = [{
    'type': 'scattergeo',
    'lon': lons,
    'lat': lats,
}]

در این رویکرد دانسته‌ها در مورد داده‌ها به صورت جفت‌های کلید-مقدار در یک دیکشنری درمی‌آیند. اگر این کد را در eq_plot.py قرار دهید خواهید دید که نمودار مشابهی حاصل خواهد شد. این شکل تعریف به ما امکان می‌دهد که شخصی‌سازی‌ها را نسبت به حالت قبلی آسان‌تر تعیین کنیم.

2.5.13 - شخصی‌سازی اندازه‌ی نشان‌گرها

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

برای انجام این کار اندازه‌ی نشان‌گرها را بسته به بزرگی هر زمین‌لرزه تغییر می‌دهیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import json
--snip--

# Map the earthquakes.
data = [{
    'type': 'scattergeo',
    'lon': lons,
    'lat': lats,
    'marker': {
        'size': [5*mag for mag in mags],
    },
}]

my_layout = Layout(title='Global Earthquakes')

--snip--

پلات‌لی طیف گسترده‌ای از شخصی‌سازی‌ها را برای یک دنباله‌ی داده فراهم می‌کند که هر کدام را می‌توان به صورت یک جفت کلید-مقدار بیان کرد. اینجا از کلید 'marker' استفاده می‌کنیم تا میزان بزرگی هر نشان‌گر را روی نقشه مشخّص کنیم. ما از دیکشنری تودرتو به عنوان مقدار مرتبط با 'marker' استفاده می‌کنیم چون می‌توان تعدادی تنظیمات را همزمان برای تمام نشان‌گرهای نقشه تعیین کرد.

هدف این است که اندازه‌ی نشان‌گر متناسب با بزرگی از زمین‌لرزه باشد. امّا اگر تنها لیست mag را ملاک قرار دهیم نشان‌گرها بسیار کوچک خواهند شد و تشخیص تفاوت آن‌ها مشکل است. باید بزرگی را در ضریب مقیاسی ضرب کنیم تا به اندازه‌ی مطلوب نشان‌گر برسیم. در صفحه نمایش من مقدار ۵ به خوبی کار می‌کند؛ مقداری کمی کوچک‌تر یا بزرگتر ممکن است برای نقشه‌ی شما نتیجه‌ی مطلوب‌تر بدهد. ما از دربرداری لیست استفاده می‌کنیم که اندازه‌ی مناسب نشان‌گر را برای هر مقدار در لیست mags تولید می‌کند.

زمانی که این کد را اجرا می‌کنید باید نقشه‌ای شبیه شکل ۱۶-۸ ببینید. این نقشه به مراتب بهتر است امّا هنوز کارهای بیشتری برای انجام مانده است.

شکل ۱۶-۸- اکنون نقشه بزرگی هر زمین‌لرزه را نشان می‌دهد.

شخصی‌سازی رنگ نشان‌گرها

ما همچنین می‌توانیم رنگ هر نشان‌گر را برای ارائه‌ی برخی دسته‌بندی‌ها در مورد شدّت هر زمین‌لرزه تغییر دهیم. برای این کار از مقیاس‌های رنگی پلات‌لی استفاده می‌کنیم. پیش از هر تغییری فایل eq_data_30_day_m1.json را به پوشه‌ی داده‌ی خود کپی کنید. این فایل شامل داده‌های زمین‌لرزه‌ای یک دوره‌ی ۳۰ روزه است و نقشه با وجود این مجموعه‌داده‌ی بزرگ‌تر جالب‌تر به نظر خواهد رسید.

این روش استفاده از مقیاس رنگی برای نمایش بزرگی هر زمین‌لرزه است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
--snip--

filename = 'data/eq_data_30_day_m1.json'

--snip--

# Map the earthquakes.
data = [{
    --snip--

    'marker': {
        'size': [5*mag for mag in mags],
        'color': mags,
        'colorscale': 'Viridis',
        'reversescale': True,
        'colorbar': {'title': 'Magnitude'},
    },
}]

--snip--

مطمئن شوید که نام فایل را به‌روز می‌کنید تا از مجموعه‌داده‌ی ۳۰روزه استفاده کنید. اینجا تمام تغییرات مهم در دیکشنری 'marker' رخ می‌دهد چون تنها می‌خواهیم ظاهر نشان‌گرها را تغییر دهیم. تنظیمات 'color' به پلات‌لی می‌گوید که باید از چه مقدارهایی استفاده کند تا محل قرارگیری هر نشان‌گر در مقیاس رنگی تعیین شود. از لیست mags برای تعیین رنگی که استفاده شده است استفاده می‌کنیم. تنظیمات 'colorscale' به پلات‌لی می‌گوید که از کدام گستره‌ی رنگی استفاده کند: 'Viridis' مقیاسی رنگی است که آبی تیره تا زرد روشن تغییر می‌کند و برای چنین مجموعه‌داده‌ای کارآمد است. 'reversescale' را برابر True می‌گذاریم زیرا می‌خواهیم زرد روشن را برای کمترین مقدارها و آبی تیره را برای شدیدترین زمین‌لرزه‌ها استفاده کنیم. تنظیمات 'colorbar' به ما امکان کنترل ظاهر مقیاس رنگی نمایش‌داده‌شده در کنار نقشه را می‌دهد. این عنوان مقیاس رنگی را 'Magnitude' قرار می‌دهید تا آن‌چه را رنگ‌ها بیان می‌کنند شفّاف کنیم.

اگر اکنون برنامه را اجرا کنید نقشه‌ای بسیار زیباتر133 خواهید دید. در شکل ۱۶-۹ مقیاس رنگی شدت یک‌یک زمین‌لرزه‌ها را نشان می‌دهد. رسم این تعداد زمین‌لرزه به طور واضح نشان می‌دهد که مرزهای صفحه‌های تکتونیکی کجاست!

شکل ۱۶-۹: در زمین‌لرزه‌های ۳۰ روزه رنگ و اندازه برای نمایش بزرگی هر زمین‌لرزه به کار می‌رود.

2.5.14 - دیگر مقیاس‌های رنگی

شما می‌توانید از تعدادی مقیاس رنگی دیگر نیز استفاده کنید. برای دیدن مقیاس‌های موجود برنامه‌ی کوتاه زیر را به عنوان show_color_scales.py ذخیره کنید:

1
2
3
4
from plotly import colors

for key in colors.PLOTLY_SCALES.keys():
    print(key)

پلات‌لی مقیاس‌های رنگی را در ماژول colors ذخیره می‌کند. این مقیاس‌ها در دیکشنری PLOTLY_SCALES تعریف می‌شوند و نام آن‌ها به عنوان کلید این دیکشنری‌ها می‌آیند. این خروجی شامل تمام مقیاس‌های رنگی موجود است:

1
2
3
4
5
Greys
YlGnBu
Greens
--snip--
Viridis

این مقیاس‌ها را امتحان کنید و به یاد داشته باشید که می‌توانید آن‌ها را با تنظیم reversescale وارونه کنید.

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

تعیین متن شناور

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

برای ایجاد چنین تغییری اطّلاعات بیشتری از فایل بیرون می‌کشیم و آن‌ها را به دیکشنری data اضافه می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
--snip--

mags, lons, lats, hover_texts = [], [], [], []

for eq_dict in all_eq_dicts:
    --snip--

    lat = eq_dict['geometry']['coordinates'][1]
    title = eq_dict['properties']['title']
    mags.append(mag)
    lons.append(lon)
    lats.append(lat)
    hover_texts.append(title)

--snip--

# Map the earthquakes.
data = [{
    'type': 'scattergeo',
    'lon': lons,
    'lat': lats,
    'text': hover_texts,
    'marker': {
        --snip--
    },
}]

--snip--

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

سپس کلید 'text' را به شیء data اضافه می‌کنیم، پلات‌لی از این مقدار به عنوان برچسب هر نشان‌گر استفاده می‌کند که هنگام ایستادن روی آن نمایش داده می‌شود. زمانی که لیستی که طول آن به تعداد نشان‌گر می‌خورد، ارائه کنیم پلات‌لی هر برچسب را به یک نشان‌وند نسبت می‌دهد. زمانی که این برنامه را اجرا کنید باید بتوانید روی هر نشان‌گر بایستید و شرحی از محل وقوع زمین‌لرزه و بزرگی دقیق آن ببینید.

این تاثیرگذار است! در تقریباً ۴۰ خط کد نقشه‌ای زیبا و پرمعنی از فعالیّت‌های زمین‌لرزه‌ای جهانی ساختیم که ساختار زمین‌شناختی سیّاره را هم به تصویر می‌کشد. پلات‌لی چندین راه برای شخصی‌سازی ظاهر و رفتار بصری‌سازی‌ها به شما ارائه می‌کند. با استفاده از این گزینه‌های پلات‌لی می‌توانید جدول‌ها و نقشه‌هایی بسازید که دقیقاً مطابق با خواسته‌های شما هستند.

2.5.15 - خودآزمایی

۱۶-۶- بازآرایی: حلقه‌ای که داده‌ها را از all_eq_dicts بیرون می‌کشد پیش از الحاق موارد به لیست‌های متناظر، از متغیّرهایی برای بزرگی، طول و عرض جغرافیایی و عنوان هر زمین‌لرزه استفاده می‌کند. این رویکرد به این دلیل انتخاب شده که نحوه‌ی استخراج داده از فایل JSON شفّاف باشد امّا این در کد شما الزامی نیست. به جای استفاده از این متغیّرهای موقّت، هر مقدار را از eq_dict بیرون بکشید و آن را در یک خط به لیست متناظر اضافه کنید. این کار باعث می‌شود کل بدنه‌ی حلقه به چهار خط کاهش یابد.

۱۶-۷- عنوان خودکار: در این بخش هنگام تعریف my_layout عنوان را به صورت دستی تعیین کردیم که نشان می‌دهد باید با134 هر بار تغییر فایل منبع عنوان را نیز عوض کنیم. به جای این می‌توانید از عنوان مجموعه‌داده استفاده کنید که در بخش فراداده‌ی فایل JSON آمده است. این مقدار را بیرون بکشید، به متغیّری نسبت دهید و آن را به عنوان عنوان نقشه هنگام تعریف my_layout به کار ببرید.

۱۶-۸- زمین‌لرزه‌های اخیر: شما می‌توانید فایل‌های داده‌ی حاوی اطّلاعات را در مورد جدیدترین زمین‌لرزه‌ها در بازه‌های ۱ ساعته، ۱ روزه، ۷ روزه و ۳۰ روزه به صورت آنلاین پیدا کنید. به https://earthquake.usgs.gov/earthquakes/feed/v1.0/geojson.php بروید. در اینجا لیستی از پیوندها برای مجموعه‌داده‌ها در بازه‌های زمانی مختلف خواهید دید که برای زمین‌لرزه‌ها با بزرگی‌های متفاوت تمرکز دارد. یکی از این مجموعه‌داده‌ها را بگیرید و بصری‌سازی‌ای در مورد جدیدترین فعّالیّت‌های زمین‌لرزه‌ای بسازید.

۱۶-۹- آتش‌سوزی‌های جهانی: در منابع این فصل فایلی به نام world_fires_1_day.csv خواهید یافت که شامل اطّلاعاتی در مورد آتش‌سوزی‌های نقاط مختلف کره‌ی زمین است و طول و عرض جغرافیایی و شدّت آن را نیز شامل می‌شود. به کمک کارهای مربوط به پردازش داده در بخش اوّل این فصل و نقشه‌هایی که در این بخش ساختیم، نقشه‌ای درست کنید که نشان دهد کدام بخش جهان تحت تاثیر آتش‌سوزی قرار گرفته است.

می‌توانید نسخه‌های جدیدتری از این داده را از https://earthdata.nasa.gov/earth-observation-data/near-real-time/firms/active-fire-data/ دریافت کنید. می‌توانید پیوندهایی به داده‌های به شکل CSV در بخش TXT پیدا کنید.

2.5.16 - خلاصه

در این فصل کار کردن با مجموعه‌داده‌های دنیای واقعی را آموختید. فایل‌های CSV و JSON را پردازش و داده‌های مورد نظر خود را از آن‌ها استخراج کردید. با استفاده از داده‌های تاریخی آب و هوا درباره‌ی کار کردن با Matplotlib بیشتر آموختید، مانند چگونگی کار با ماژول datetime و نحوه‌ی رسم چند دنباله‌ی داده بر روی یک نمودار. شما در پلات‌لی داده‌های جغرافیایی را روی نقشه‌ی جهان رسم کردید و نقشه‌ها و نمودارهای پلات‌لی را نیز شکل دادیم.

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

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


2.6 - فصل هفدهم

2.6.1 - کار کردن با APIها

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

2.6.2 - استفاده از یک API تحت وب

API تحت وب بخشی از یک وب‌سایت است که برای ارتباط ب136 ا برنامه‌ها طرّاحی شده است. برنامه‌ها از URLهای خاصّی برای درخواست اطّلاعات استفاده می‌کنند. این درخواست‌ها فراخوانی API خوانده می‌شود. داده‌ی درخواستی در قالبی دریافت می‌شود که پردازش آن آسان باشد، مانند JSON یا CSV. بسیاری از برنامه‌هایی که بر منابع داده‌های خارجی متّکی هستند، مانند برنامه‌هایی که با سایت‌های شبکه‌های اجتماعی ادغام می‌شوند، به این فراخوانی‌ها وابسته‌اند.

2.6.3 - Git و GitHub

ما بصری‌سازی‌های خود را بر اساس اطّلاعات دریافتی از GitHub درست خواهیم کرد. گیت‌هاب سایتی است که به برنامه‌نویسان امکان مشارکت در پروژه‌های برنامه‌نویسی می‌دهد. از API گیت‌هاب برای دریافت اطّلاعاتی درباره‌ی پروژه‌های پایتون موجود در سایت استفاده می‌کنیم و سپس به کمک پلات‌لی یک بصری‌سازی تعاملی از محبوبیّت نسبی این پروژه‌ها تولید می‌کنیم.

گیت‌هاب (https://github.com/) نام خود را از Git گرفته که یک سیستم کنترل نسخه‌ی توزیع‌شد138 137 ه است. گیت به افراد کمک می‌کند که کار خود را در یک پروژه مدیریّت کنند تا تغییراتی که یک نفر ایجاد می‌کند در کار دیگران اختلالی ایجاد نکند. زمانی که ویژگی جدیدی به یک پروژه اضافه می‌کنید، گیت تغییرات شما را در هر فایل دنبال می‌کند. اگر کد شما کار کند، شما تغییرات اعمالی خود را اجرایی می‌کنید و گیت وضعیّت جدید پروژه را ثبت می‌کند. اگر اشتباهی کرده باشید و بخواهید تغییرات خود را برگردانید، به سادگی می‌توانید به یک وضعّیت درست قبلی برگردید. (برای آشنایی بیشتر با کنترل نسخه با گیت پیوست ت را ببینید) پروژه‌های گیت‌هاب در مخزن‌ها نگهداری می‌شوند که دربردارنده‌ی تمام چیزهای مربوط به یک پروژه است: کد آن، اطّلاعات مشارکت‌کنندگان، مشکلات یا گزارش ایرادات و …

زمانی که کاربران گیت‌هاب پروژه‌ای را می‌پسندند می‌توانند به آن ستاره بدهند تا حمایت خود را نشان دهند و پروژه‌هایی را که احتمالاً می‌خواهند مورد استفاده قرار دهند پی بگیرند. در این فصل برنامه‌ای خواهیم نوشت که به طور خودکار اطّلاعاتی درباره‌ی پُرستاره‌ترین پروژه‌های پایتون در گیت‌هاب دریافت کند و سپس بصری‌سازی‌های پرمعنایی درباره‌ی این پروژه‌ها خواهیم ساخت.

2.6.4 - درخواست داده با یک فراخوانی API

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

https://api.github.com/search/repositories?q=language:python&sort=stars

این فراخوانی تعداد پروژه‌هایی را که در حال حاضر گیت‌هاب میزبان آن‌هاست به همراه اطّلاعاتی درباره‌ی محبوب‌ترین مخزن‌های پایتون برمی‌گرداند. بیایید فراخوانی را بررسی کنیم. اوّلین بخش، https://api.github.com درخواست را به قسمتی از گیت‌هاب هدایت می‌کند که به فراخوانی‌های API پاسخ می‌دهد. بخش بعدی، search/repositories به API می‌گوید که در تمام مخزن‌های گیت‌هاب جست‌و‌جو انجام بده.

علامت سوال بعد از repositories نشان می‌دهد که می‌خواهیم نشان‌وندی را رد کنیم. q مخفّف query (= پرس و جو) است و علامت تساوی (=) به ما امکان می‌دهد که تعیین مورد پرسش را شروع کنیم (q=). با استفاده از language:python می‌گوییم که تنها به دنبال اطّلاعاتی درباره‌ی مخزن‌هایی هستیم که پایتون زبان اصلی آن‌هاست. بخش پایانی، &sort=stars، پروژه‌ها را بر اساس تعداد ستاره‌هایی که دریافت کرده‌اند مرتّب می‌کند.

تکه‌کد زیر چند خطّ اوّل پاسخ را نشان می‌دهد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "total_count": 3494012,
  "incomplete_results": false,
  "items": [
    {
      "id": 21289110,
      "node_id": "MDEwOlJlcG9zaXRvcnkyMTI4OTExMA==",
      "name": "awesome-python",
      "full_name": "vinta/awesome-python",
      --snip--

چنان‌که از پاسخ درمی‌یابید این URL برای این طرّاحی نشده که انسان‌ها آن را وارد کنند زیرا خروجی آن به شکلی‌ست که برنامه‌ها آن را پردازش می‌کنند. گیت‌هاب در زمان نگارش این متن ۳،۴۹۴،۰۱۲ پروژه‌ی پایتون یافته است (۱). چون مقدار incomplete_results برابر false است، متوجّه می‌شویم که درخواست موفقیّت‌آمیز بوده و ناقص نیست. (۲) اگر گیت‌هاب قادر به پردازش کامل درخواست API نباشد این مقدار را true برمی‌گرداند. پس از این دو items می‌آید که لیستی شامل جزئیات محبوب‌ترین پروژه‌های پایتون روی گیت‌هاب است. (۳)

2.6.5 - نصب کردن Requests

بسته‌ی Requests به برنامه‌های پایتون امکان می‌دهد که به سادگی از یک وب‌سایت درخواست اطّلاعات کرده و پاسخ آن‌ها را بررسی کنند. از pip برای نصب آن استفاده می‌کنیم:

1
$ python -m pip install --user requests

این خط به پایتون می‌گوید که ماژول pip را اجرا و بسته‌ی Requests را به نصب پایتون کاربر فعلی اضافه کند. اگر از python3 یا دستور متفاوتی برای اجرای برنامه‌ها و نصب بسته‌ها استفاده می‌کنید، مطمئن شوید که اینجا هم از آن‌ها استفاده می‌کنید.

نکته
اگر این دستور در macOS کار نکرد آن را بدون پرچم --user امتحان کنید.

2.6.6 - پردازش پاسخ API

اکنون نوشتن برنامه‌ای را آغاز می‌کنیم که به طور خودکار API را فراخوانی می‌کند و نتایج آن را با تشخیص پرستاره‌ترین پروژه‌های پایتون در گیت‌هاب پردازش می‌کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import requests

# Make an API call and store the response.
url = 'https://api.github.com/search/repositories?q=language:python&sort=stars'
headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(url, headers=headers)  # ③
print(f"Status code: {r.status_code}")

# Store API response in a variable.
response_dict = r.json()  # ④

# Process results.
print(response_dict.keys())

ماژول requests را وارد کرده‌ایم. URL مربوط به API را در متغیّر url ذخیره می‌کنیم. گیت‌هاب در حال حاضر از سومین نسخه‌ی API خود استف139 اده می‌کند پس این سَرپیام‌ها را برای فراخوانی آن تعریف می‌کنیم که صریحاً این نسخه‌ی API را درخواست می‌کند. سپس به کمک requests این API را فرامی‌خوانیم.

تابع get() را فراخوانی کرده و URL و سرپیام‌های تعریف‌شده را به آن می‌دهیم و شیء پاسخ را به متغیّر r نسبت می‌دهیم. شیء پاسخ مشخّصه‌ای به نام status_code دارد که وضعیّت درخواست را نشان می‌دهد. (کد وضعیّت ۲۰۰ بیانگر درخواست موفّق است.) مقدار status_code را چاپ می‌کنیم تا مطمئن باشیم که فراخوانی درست پیش رفته است.

این API اطّلاعات را در قالب JSON برمی‌گرداند پس از متد json() برای تبدیل آن‌ها به دیکشنری پایتون استفاده می‌کنیم. دیکشنری حاصل را در response_dict ذخیره می‌کنیم.

در نهایت keysهای response_dict را چاپ کرده و خروجی را می‌بینیم:

1
2
Status code: 200
dict_keys(['total_count', 'incomplete_results', 'items'])

چون کد وضعیّت ۲۰۰ است می‌دانیم که درخواست موفقّیت‌آمیز بوده است. دیکشنری پاسخ تنها سه کلید دارد: total_count، incomplete_results و items. اجازه دهید نگاهی به محتویّات دیکشنری پاسخ بیندازیم.

نکته
فراخوانی‌های ساده مانند این باید مجموعه‌ی کامل نتایج را برگرداند بنابراین صرف نظر کردن از مقدارهای مرتبط با incomplete_results اشکالی ندارد. امّا اگر فراخوانی‌های پیچیده‌تری انجام می‌دهید برنامه‌ی شما باید این مقدار را بررسی کند.

2.6.7 - کار کردن با دیکشنری پاسخ

اکنون که اطّلاعات دریافتی از API به صورت دیکشنری ذخیره شده، می‌توانیم با داده‌های داخل آن کار کنیم. ابتدا خروجی‌ای تولید می‌کنیم که اطّلاعات را خلاصه می‌کند. این راه خوبی است که بفهمیم داده‌های دریافتی مطابق انتظار ماست و بررسی داده‌های مورد نظر خود را شروع کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests

# Make an API call and store the response.
--snip--

# Store API response in a variable.
response_dict = r.json()

print(f"Total repositories: {response_dict['total_count']}")  # ①

# Explore information about the repositories.
repo_dicts = response_dict['items']  # ②

print(f"Repositories returned: {len(repo_dicts)}")

# Examine the first repository.
repo_dict = repo_dicts[0]  # ③

print(f"\nKeys: {len(repo_dict)}")  # ④

for key in sorted(repo_dict.keys()):  # ⑤
    print(key)

مقدار مربوط به total_count را چاپ می‌کنیم که بیانگر تعداد کل مخزن‌های پایتون در گیت‌هاب است.

مقدار مرتبط با items لیستی شامل تعدادی دیکشنری است که هر کدام حاوی داده‌های هر مخزن پایتون است. این لیست را در repo_dicts ذخیره می‌کنیم. سپس طول repo_dicts را چاپ می‌کنیم تا بدانیم اطّلاعات چند مخزن را در اختیار داریم.

برای نگاهی دقیق‌تر به داده‌های دریافتی درباره‌ی هر مخزن، اوّلین مورد در repo_dicts را در repo_dict قرار می‌دهیم. سپس تعداد کلیدهای این دیکشنری را چاپ می‌کنیم تا بفهمیم چه‌قدر اطّلاعات در اختیار داریم. در ادامه تمام کلیدهای دیکشنری را چاپ می‌کنیم تا ببینیم چه نوع اطّلاعاتی در پاسخ گنجانده شده است.

این نتایج تصویر واضح‌تری از داده‌های واقعی می‌دهد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Status code: 200
Total repositories: 3494030
Repositories returned: 30
Keys: 73
archive_url
archived
assignees_url
--snip--
url
watchers
watchers_count

API گیت‌هاب اطّلاعات بسیاری درباره‌ی هر مخزن برمی‌گرداند: ۷۳ کلید در repo_dict وجود دارد. اگر به این کلیدها نگاهی بیندازید درکی کلّی از نوع اطّلاعاتی که می‌توانید درباره‌ی هر پروژه استخراج کنید کسب می‌کنید. (تنها راه دانستن این که چه اطّلاعاتی از طریق API در دسترس است خواندن مستندات آن یا بررسی اطّلاعات از طریق کد است، همان راهی که ما اینجا پی گرفته‌ایم.)

بیایید مقدار برخی از کلیدهای repo_dict را بیرون بکشیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
--snip--

# Explore information about the repositories.
repo_dicts = response_dict['items']

print(f"Repositories returned: {len(repo_dicts)}")

# Examine the first repository.
repo_dict = repo_dicts[0]

print("\nSelected information about first repository:")
print(f"Name: {repo_dict['name']}")
print(f"Owner: {repo_dict['owner']['login']}")
print(f"Stars: {repo_dict['stargazers_count']}")
print(f"Repository: {repo_dict['html_url']}")
print(f"Created: {repo_dict['created_at']}")
print(f"Updated: {repo_dict['updated_at']}")
print(f"Description: {repo_dict['description']}")

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Status code: 200
Total repositories: 3494032
Repositories returned: 30

Selected information about first repository:
Name: awesome-python
Owner: vinta
Stars: 61549
Repository: https://github.com/vinta/awesome-python
Created: 2014-06-27T21:00:06Z
Updated: 2019-02-17T04:30:00Z
Description: A curated list of awesome Python frameworks, libraries, software
and resources

چنان‌که می‌بینیم پرستاره‌ترین پروژه‌ی پایتون در گیت‌هاب در زمان نوشتن این کتاب، awesome-python است، مالک آن vinta است و بیش از ۶۰ هزار ستاره از کاربران گیت‌هاب دریافت کرده است. می‌توانیم URL مخزن پروژه، تاریخ ایجاد آن در ژوئن ۲۰۱۴ و به‌روزرسانی شدن آن را به تازگی ببینیم. علاوه بر این شرح مخزن به ما می‌گوید که این پروژه شامل فهرستی از محبوب‌ترین منابع پایتون است.

2.6.8 - خلاصه کردن مخزن‌های برتر

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
--snip--

# Explore information about the repositories.
repo_dicts = response_dict['items']

print(f"Repositories returned: {len(repo_dicts)}")

print("\nSelected information about each repository:")
for repo_dict in repo_dicts:  # ②
    print(f"\nName: {repo_dict['name']}")
    print(f"Owner: {repo_dict['owner']['login']}")
    print(f"Stars: {repo_dict['stargazers_count']}")
    print(f"Repository: {repo_dict['html_url']}")
    print(f"Description: {repo_dict['description']}")

پیام معرّفی را چاپ می‌کنیم. داخل دیکشنری‌های repo_dicts حلقه می‌زنیم. داخل حلقه نام هر پروژه، مالک آن، تعداد ستاره‌ها، URL و شرح پروژه را چاپ می‌کنیم، مانند زیر:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Status code: 200
Total repositories: 3494040
Repositories returned: 30

Selected information about each repository:

Name: awesome-python
Owner: vinta
Stars: 61549
Repository: https://github.com/vinta/awesome-python
Description: A curated list of awesome Python frameworks, libraries, software
and resources

Name: system-design-primer
Owner: donnemartin
Stars: 57256
Repository: https://github.com/donnemartin/system-design-primer
Description: Learn how to design large-scale systems. Prep for the system
design interview. Includes Anki flashcards.

--snip--

Name: python-patterns
Owner: faif
Stars: 19058
Repository: https://github.com/faif/python-patterns
Description: A collection of design patterns/idioms in Python

2.6.9 - پایش محدودیت نرخ API

نرخ استفاده از بیشتر APIها محدود است، به این معنی که دفعات فراخوانی آن در مدت زمان مشخّص محدود شده است. برای دیدن این که به محدودیّت‌های گیت‌هاب نزدیک شده‌اید یا نه آدرس https://api.github.com/rate_limit را در مرورگر خود وارد کنید. باید پاسخی شبیه به این ببینید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "resources": {
    "core": {
      "limit": 60,
      "remaining": 58,
      "reset": 1550385312
    },
    "search": {
      "limit": 10,
      "remaining": 8,
      "reset": 1550381772
    },
    --snip--

اطّلاعاتی که در اینجا مورد توجّه ماست محدودیّت نرخ جستجوی API است. در می‌بینیم که حد استفاده ۱۰ درخواست بر دقیقه است و ۸ درخواست این دقیقه را انجام داده‌ایم. مقدار بازنشانی بیانگر زمان به شکل Unix یا epoch time است (تعداد ثانیه‌ها از نیمه‌شب اول ژانویه‌ی ۱۹۷۰) و نشان می‌دهد سهمیّه‌ی ما کی تجدید خواهد شد. اگر به پایان سهمیّه‌ی خود برسید پاسخی کوتاه دریافت می‌کنید که بیان می‌کند شما از محدوده‌ی API گذر کرده‌اید. اگر چنین شود باید تا تجدید سهمیّه‌ی خود صبر کنید.

نکته
بسیاری از APIها از شما می‌خواهند که ثبت نام کنید و هنگام فراخوانی یک کلید API بفرستید. تا زمان نگارش این متن گیت‌هاب چنین درخواستی ندارد امّا اگر یک کلید API دریافت کنید محدودیّت استفاده‌ی شما خیلی کم‌تر خواهد بود.

2.6.10 - بصری کردن مخزن‌ها به کمک پلات‌لی

اکنون داده‌هایی را که در اختیار داریم بصری می‌کنیم تا محبوبیّت نسبی پروژه‌های پایتون را در گیت‌هاب نمایش دهیم. یک نمودار میله‌ای تعاملی خواهیم ساخت: ارتفاع هر میله بیانگر تعداد ستاره‌های دریافتیِ پروژه است و با کلیک کردن روی هر میله می‌توانید به صفحه‌ی پروژه در گیت‌هاب بروید. نسخه‌ای از برنامه‌ای را که مشغول کار بر روی آن بودیم به نام python_repos_visual.py ذخیره کنید و آن را تغییر دهید تا مشابه کد زیر شود:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import requests
from plotly.graph_objs import Bar  # ①
from plotly import offline

# Make an API call and store the response.
url = 'https://api.github.com/search/repositories?q=language:python&sort=stars'
headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(url, headers=headers)
print(f"Status code: {r.status_code}")  # ②

# Process results.
response_dict = r.json()
repo_dicts = response_dict['items']

repo_names, stars = [], []  # ③
for repo_dict in repo_dicts:
    repo_names.append(repo_dict['name'])
    stars.append(repo_dict['stargazers_count'])

# Make visualization.
data = [{  # ④
    'type': 'bar',
    'x': repo_names,
    'y': stars,
}]

my_layout = {  # ⑤
    'title': 'Most-Starred Python Projects on GitHub',
    'xaxis': {'title': 'Repository'},
    'yaxis': {'title': 'Stars'},
}

fig = {'data': data, 'layout': my_layout}
offline.plot(fig, filename='python_repos.html')

کلاس Bar و ماژول offline را از plotly وارد می‌کنیم. نیازی به وارد کردن کلاس Layout نیست چون از رویکرد دیکشنری برای تعیین چیدمان استفاده می‌کنیم، مشابه همان کاری که برای لیست data در پروژه‌ی نقشه‌ی زمین‌لرزه در فصل ۱۶ کردیم. وضعیّت پاسخ فراخوانی API را همچنان چاپ می‌کنیم تا در صورت بروز مشکل از آن مطّلع شویم. بخشی از کد پردازش پاسخ API را حذف کرده‌ایم زیرا مرحله‌ی اکتشافی دیگر به پایان رسیده و می‌دانیم چه داده‌ای را نیاز داریم.

سپس دو لیست خالی می‌سازیم تا داده‌های نمودار اوّلیّه را در آن ذخیره کنیم. نام هر پروژه را به عنوان برچسب میله‌ها و تعداد ستاره‌ها را برای تعیین ارتفاع میله نیاز داریم. داخل حلقه نام و تعداد ستاره‌ی هر پروژه را به این دو لیست الحاق می‌کنیم.

در ادامه لیست data را تعریف می‌کنیم. این لیست شامل دیکشنری‌ای مشابه آن‌چه در فصل ۱۶ استفاده کردیم خواهد بود که نوع نمودار و داده‌های مربوط به مقدارهای x و y را شامل می‌شود. مقدارهای x نام پروژه‌ها و مقدارهای y تعداد ستاره‌های دریافتی هر پروژه خواهد بود.

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

شکل ۱۷-۱ نمودار حاصل را نشان می‌دهد. می‌بینیم که چند پروژه‌ی اوّل به طور چشم‌گیری از باقی پروژه‌ها محبوب‌ترند ولی همه‌ی آن‌ها پروژه‌های مهمّی در زیست‌بوم پایتون به شمار می‌آیند.

شکل ۱۷-۱: محبوب‌ترین پروژه‌های پایتون در گیت‌هاب

2.6.11 - بهبود نمودارهای پلات‌لی

بیایید شکل و قیافه‌ی نمودارها را بهبود بخشیم. همان‌طور که در فصل ۱۶ دیدید شما می‌توانید تمام راهنماهای شکل‌دهی را به صورت جفت‌های کلید-مقدار در دیکشنری‌های data و my_layout تعریف کنید. تغییراتی اعمالی در شیء data میله‌ها را تحت تاثیر قرار می‌دهد. این نسخه‌ی تغییریافته‌ی شیء data برای نمودار ماست که رنگی خاص و حاشیه‌ای واضح برای هر میله فراهم می‌کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
--snip--
data = [{
    'type': 'bar',
    'x': repo_names,
    'y': stars,
    'marker': {
        'color': 'rgb(60, 100, 150)',
        'line': {'width': 1.5, 'color': 'rgb(25, 25, 25)'}
    },
    'opacity': 0.6,
}]
--snip--

تنظیمات marker نمایش‌داده شده طرح میله‌ها را تغییر می‌دهد. یک رنگ آبی شخصی و حاشیه‌ای به رنگ خاکستری تیره و عرض ۱٫۵ پیکسل برای هر میله تعیین کرده‌ایم. همچنین شفافیّت میله‌ها را برابر ۰٫۶ قرار دادیم تا ظاهر نمودار را کمی ملایم‌تر کنیم.

در ادامه my_layout را تغییر می‌دهیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
--snip--
my_layout = {
    'title': 'Most-Starred Python Projects on GitHub',
    'titlefont': {'size': 28},  # ①
    'xaxis': {
        'title': 'Repository',
        'titlefont': {'size': 24},  # ②
        'tickfont': {'size': 14},
    },
    'yaxis': {
        'title': 'Stars',
        'titlefont': {'size': 24},  # ③
        'tickfont': {'size': 14},
    },
}
--snip--

از کلید titlefont برای تعریف اندازه‌ی قلم در عنوان کلی نمودار استفاده می‌کنیم. در دیکشنری xaxis تنظیماتی برای کنترل اندازه‌ی قلم عنوان محور x (titlefont) و علامت‌گذاری‌های آن (tickfont) اضافه می‌کنیم. چون این دیکشنری‌های تودرتو مستقل‌اند می‌توانید کلیدهای مربوط به رنگ و خانواده‌ی قلم عنوان و علامت‌گذاری‌های هر محور را وارد کنیم. همین تنظیمات را برای محور y انجام می‌دهیم.

شکل ۱۷-۲ نمودار دوباره‌طرّاحی‌شده را نشان می‌دهد:

شکل ۱۷-۲: شکل و سیاق نمودار بازنگری شده است.

2.6.12 - افزودن تول‌تیپ‌های شخصی

در پلات‌لی می‌توانید با قرار دادن نشان‌گر ماوس روی هر میله اطّلاعاتی را که آن میله نمایندگی می‌کند نشان دهید. این راهنما معمولاً تول‌تیپ نامیده می‌شود و در این مورد تعداد ستاره‌های هر پروژه را نشان می‌دهد. اجازه دهید یک تول‌تیپ شخصی برای نمایش شرح پروژه و نام مالک آن بسازیم.

باید داده‌های دیگری برای تولید تول‌تیپ‌ها استخراج کنیم و شیء data را تغییر دهیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
--snip--

# Process results.
response_dict = r.json()
repo_dicts = response_dict['items']

repo_names, stars, labels = [], [], []  # ①

for repo_dict in repo_dicts:
    repo_names.append(repo_dict['name'])
    stars.append(repo_dict['stargazers_count'])

    owner = repo_dict['owner']['login']  # ②
    description = repo_dict['description']
    label = f"{owner}<br />{description}"  # ③
    labels.append(label)

# Make visualization.
data = [{
    'type': 'bar',
    'x': repo_names,
    'y': stars,
    'hovertext': labels,  # ④
    'marker': {
        'color': 'rgb(60, 100, 150)',
        'line': {'width': 1.5, 'color': 'rgb(25, 25, 25)'}
    },
    'opacity': 0.6,
}]
--snip--

ابتدا لیستی خالی برای نگهداری متن نمایشی هر پروژه140 درست می‌کنیم. در حلقه‌ی پردازش داده‌ها شرح و مالک هر پروژه را بیرون می‌کشیم. پلات‌لی به‌کارگیری کد HTML داخل عناصر متنی را مجاز می‌داند پس رشته‌ای برای برچسب‌ها درست می‌کنیم و از شکست خط (<br />) برای ایجاد فاصله بین نام کاربری مالک و شرح پروژه استفاده می‌کنیم. سپس این برچسب را در لیست labels ذخیره می‌کنیم.

در دیکشنری data مدخلی به کلید hovertext اضافه می‌کنیم و لیستی را که هم‌اکنون ساخته‌ایم به آن نسبت می‌دهیم. پلات‌لی همچنان‌که هر میله را می‌سازد برچسب متناظرش را از این لیست بیرون می‌کشد و تنها زمانی آن را نمایش می‌دهد که بیننده روی آن میله توقّف کند.

شکل ۱۷-۳ نمودار حاصل را نشان می‌دهد.

شکل ۱۷-۳: قرار دادن نشان‌گر روی یک میله نام مالک و شرح پروژه را نشان می‌دهد.

2.6.13 - افزودن پیوندهای قابل کلیک به نمودار

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
--snip--

# Process results.
response_dict = r.json()
repo_dicts = response_dict['items']

repo_links, stars, labels = [], [], []  # ①

for repo_dict in repo_dicts:
    repo_name = repo_dict['name']  # ②
    repo_url = repo_dict['html_url']
    repo_link = f"<a href='{repo_url}'>{repo_name}</a>"  # ③
    repo_links.append(repo_link)

    stars.append(repo_dict['stargazers_count'])

--snip--

# Make visualization.
data = [{
    'type': 'bar',
    'x': repo_links,  # ④
    'y': stars,
    --snip--
}]
--snip--

نام لیست را از repo_names به repo_links تغییر می‌دهیم تا نوع اطّلاعاتی را که در نمودار کنار هم قرار می‌دهیم دقیق‌تر بیان کنیم. سپس URL پروژه را از repo_dict بیرون کشیده و در متغیّر موقّت repo_url قرار می‌دهیم. پیوندی به پروژه درست می‌کنیم. از تگ HTML anchor که به شکل <a href='URL'>link text</a> است برای تولید پیوند استفاده می‌کنیم. سپس این پیوند را به لیست repo_links الحاق می‌کنیم.

در ادامه از این لیست به عنوان مقدارهای x استفاده می‌کنیم. نتیجه باید شبیه به قبل باشد با این تفاوت که الان بیننده می‌تواند روی نام پروژه‌ها در پایین نمودار کلیک کند تا صفحه‌ی خانه‌ی پروژه در گیت‌هاب باز شود. اکنون یک بصری‌سازی پرمفهوم و تعاملی از داده‌هایی داریم که از طریق API دریافت شده‌اند.

2.6.14 - اطّلاعات بیشتر درباره‌ی پلات‌لی و API گیت‌هاب

برای مطالعه‌ی بیشتر در مورد کار با نمودارهای پلات‌لی دو محل خوب برای شروع وجود دارد. یکی Plotly User Guide in Python به آدرس https://plot.ly/python/user-guide است. این منبع به شما درک بهتری از چگونگی به‌کارگیری داده‌های شما در پلات‌لی برای ساختن بصری‌سازی و دلیل استفاده از این رویکرد می‌دهد.

Python Figure Reference به آدرس https://plot.ly/python/reference/ تمام تنظیمات موجود را برای پیکربندی بصری‌سازی‌های پلات‌لی فهرست می‌کند. تمام انواع نمودارهای قابل رسم به همراه تمام مشخّصه‌هایی که برای هر پیکربندی قابل انتخاب است هم فهرست شده است.

برای اطّلاعات بیشتر در مورد API گیت‌هاب به مستندات آن در https://developer.github.com/v3/ مراجعه کنید. اینجا خواهید آموخت که چگونه گستره‌ی بزرگی از اطّلاعات تخصّصی را از گیت‌هاب بیرون بکشید. اگر حساب کاربریِ گیت‌هاب دارید می‌توانید با داده‌های مربوط به خود یا داده‌های مخزن‌های دیگر کاربران که در دسترس عموم‌اند کار کنید.

2.6.15 - API اخبار هکرها

برای بررسی نحوه‌ی استفاده از فراخوانی APIها در سایت‌های دیگر بیایید نگاهی سریع به سایت Hacker News (http://news.ycombinator.com/) بندازیم. در این سایت افراد مقاله‌هایی درباره‌ی برنامه‌نویسی و فنّآوری به اشتراک می‌گذارند و در بحث‌های زنده پیرامون این مقاله‌ها شرکت می‌کنند. API این سایت دسترسی به داده‌های تمام مطالب ثبت‌شده و نظرات سایت را فراهم می‌آورد و می‌توانید از آن بدون نیاز به ثبت‌نام برای دریافت کلید استفاده کنید.

فراخوانی زیر اطّلاعاتی درباره‌ی بالاترین مقاله‌ی فعلی در زمان نگارش این کتاب برمی‌گرداند:

https://hacker-news.firebaseio.com/v0/item/19155826.json

اگر این URL را در مرورگری وارد کنید متنی داخل آکلاد خواهید دید که نشان می‌دهد این یک دیکشنری است. امّا بررسی این پاسخ بدون شکل‌دهی مناسب به آن کاری مشکل است. بیایید این URL را از طریق متد json.dump() فرابخوانیم همانند آنچه در فصل ۱۶ کردیم، تا ببینیم چه اطّلاعاتی درباره‌ی یک مقاله برگردانده می‌شود:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import requests
import json

# Make an API call, and store the response.
url = 'https://hacker-news.firebaseio.com/v0/item/19155826.json'
r = requests.get(url)
print(f"Status code: {r.status_code}")

# Explore the structure of the data.
response_dict = r.json()
readable_file = 'data/readable_hn_data.json'
with open(readable_file, 'w') as f:
    json.dump(response_dict, f, indent=4)

همه چیز در این برنامه آشنا به نظر می‌آید زیرا آن را در سراسر دو فصل قبل استفاده کرده‌ایم. خروجی یک دیکشنری درباره‌ی اطّلاعات مقاله به شماره‌ی ۱۹۱۵۵۸۲۶ است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "by": "jimktrains2",
  "descendants": 220,
  "id": 19155826,
  "kids": [
    19156572,
    19158857,
    --snip--
  ],
  "score": 722,
  "time": 1550085414,
  "title": "Nasa's Mars Rover Opportunity Concludes a 15-Year Mission",
  "type": "story",
  "url": "https://www.nytimes.com/.../mars-opportunity-rover-dead.html"
}

این دیکشنری شامل تعدادی کلید است که می‌توان با آن کار کرد. کلید descendants (فرزندان) تعداد نظراتی را نشان می‌دهد که مقاله دریافت کرده است. کلید kids (کودکان) شماره‌ی تمام نظراتی را در برمی‌گیرد که به طور مستقیم برای این مقاله ارسال شده است. این نظرات ممکن است خود نظرات دیگری را هم شامل شوند در نتیجه تعداد فرزندان معمولاً بیشتر از تعداد کودکان است. عنوان مقاله‌ی مورد بحث را می‌توان دید و URL‌ای برای مقاله هم در دسترس است.

URL زیر لیست ساده‌ای از شماره‌ی تمام مقاله‌های برتر فعلی وب‌سایت را برمی‌گرداند:

https://hacker-news.firebaseio.com/v0/topstories.json

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from operator import itemgetter
import requests

# Make an API call and store the response.
url = 'https://hacker-news.firebaseio.com/v0/topstories.json'
r = requests.get(url)  # ①
print(f"Status code: {r.status_code}")

# Process information about each submission.
submission_ids = r.json()  # ②

submission_dicts = []  # ③
for submission_id in submission_ids[:30]:
    # Make a separate API call for each submission.
    url = f"https://hacker-news.firebaseio.com/v0/item/{submission_id}.json"  # ④
    r = requests.get(url)
    print(f"id: {submission_id}\tstatus: {r.status_code}")

    response_dict = r.json()

    # Build a dictionary for each article.
    submission_dict = {  # ⑤
        'title': response_dict['title'],
        'hn_link': f"http://news.ycombinator.com/item?id={submission_id}",
        'comments': response_dict['descendants'],
    }
    submission_dicts.append(submission_dict)  # ⑥

submission_dicts = sorted(submission_dicts, key=itemgetter('comments'),  # ⑦
                          reverse=True)

for submission_dict in submission_dicts:  # ⑧
    print(f"\nTitle: {submission_dict['title']}")
    print(f"Discussion link: {submission_dict['hn_link']}")
    print(f"Comments: {submission_dict['comments']}")

ابتدا API را فراخوانی و وضعیّت پاسخ را چاپ می‌کنیم. فراخوانی API لیستی شامل شماره‌ی ۵۰۰ مقاله‌ی محبوب این سایت در زمان فراخوانی را برمی‌گرداند. سپس شیء پاسخ را به لیست پایتون تبدیل می‌کنیم و آن را در submission_ids ذخیره می‌کنیم. از این شماره‌ها برای ساختن مجموعه‌ای از دیکشنری‌ها استفاده خواهیم کرد که اطّلاعات هر کدام از مقاله‌های فعلی را ذخیره می‌کنند.

لیستی خالی به نام submission_dicts می‌سازیم تا دیکشنری‌ها را نگه داریم. سپس روی شماره‌ی ۳۰ مقاله‌ی برتر حلقه می‌زنیم. با تولید یک URL که شامل مقدار فعلی submission_id است، برای هر مقاله یک فراخوانی API انجام می‌دهیم. وضعیّت هر درخواست را به همراه شماره‌ی آن چاپ می‌کنیم تا مطمئن شویم که درخواست موفقیّت‌آمیز بوده یا نه.

برای هر مقاله‌ی در حال پردازش یک دیکشنری می‌سازیم که در آن عنوان، پیوندی به صفحه‌ی نظرات آن و تعداد نظراتی را که تا الان دریافت کرده ذخیره می‌کنیم. سپس هر submission_dict را به لیست submission_dicts الحاق می‌کنیم.

هر مقاله در این سایت بر اساس امتیازی کلّی رده‌بندی می‌شود که به عوامل مختلفی مثل تعداد رای‌های مثبت، تعداد نظرات دریافتی و میزان جدید بودن آن بستگی دارد. لیست دیکشنری‌ها را بر اساس تعداد نظرات آن‌ها مرتّب می‌کنیم. بر این کار از تابعی به نام itemgetter() استفاده می‌کنیم که در ماژول operator قرار دارد. کلید comments را به این تابع می‌دهیم تا مقدارهای مرتبط با آن را از هر دیکشنری داخل لیست بیرون بکشد. سپس تابع sorted() لیست را بر اساس این مقدار مرتّب می‌کند. لیست را به ترتیب معکوس مرتّب می‌کنیم تا مقالات با بیشترین نظر را اوّل بیاوریم.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Status code: 200
id: 19155826   status: 200
id: 19180181   status: 200
id: 19181473   status: 200
--snip--

Title: Nasa's Mars Rover Opportunity Concludes a 15-Year Mission
Discussion link: http://news.ycombinator.com/item?id=19155826
Comments: 220

Title: Ask HN: Is it practical to create a software-controlled model rocket?
Discussion link: http://news.ycombinator.com/item?id=19180181
Comments: 72

Title: Making My Own USB Keyboard from Scratch
Discussion link: http://news.ycombinator.com/item?id=19181473
Comments: 62

--snip--

شما می‌توانید فرآیندی مشابه را برای دسترسی و تحلیل اطّلاعات هر API پی بگیرید. با این داده‌ها می‌توانید نموداری تولید کنید که نشان دهد اخیراً کدام مقاله‌ها بیشترین بحث‌های فعّال را برانگیخته‌اند. این همچنین پایه‌ی برنامه‌هایی است که تجربه‌ی مطالعه‌ی شخصی را برای سایت‌های اینچنینی فراهم می‌آورند. برای اطّلاع یافتن از انواع اطّلاعاتی که از طریق این API می‌توانید دریافت کنید به صفحه‌ی مستندات آن در https://github.com/HackerNews/API/ مراجعه کنید.

2.6.16 - خودآزمایی

۱۷-۱- زبان‌های دیگر: فراخوانی API را در python_repos.py تغییر دهید تا جدولی برای نمایش پروژه‌های محبوب به دیگر143 142 141 زبان‌های برنامه‌نویسی تولید کند. زبان‌هایی مانند جاوااسکریپت، روبی، سی، جاوا، پرل، هسکل و گو را امتحان کنید.

۱۷-۲- گفتگوهای فعّال: با استفاده از داده‌های به‌دست‌آمده از hn_submissions.py نموداری میله‌ای از فعّال‌ترین گفت‌و‌گوهای فعلی در Hacker News تولید کنید. ارتفاع هر میله متناسب با تعداد نظراتی است که مقاله دریافت کرده است. برچسب هر میله باید شامل عنوان مقاله باشد و مانند پیوندی به صفحه‌ی نظرات آن عمل کند.

۱۷-۳- آزمودن python_repos.py: در python_repos.py مقدار status_code را چاپ کردیم تا مطمئن شویم فراخوانی API به درستی انجام می‌پذیرد. برنامه‌ای به نام test_python_repos.py بنویسید که از unittest برای اطمینان یافتن از ۲۰۰ بودن مقدار status_code استفاده کند. به بررسی‌های دیگری که می‌توانید انجام دهید فکر کنید. مثلاً این که ذکر تعداد مواردی که برگردانده می‌شود مورد انتظار است یا تعداد کلّ مخزن‌ها باید از مقدار مشخّصی بیشتر باشد.

۱۷-۴- کاوش‌های بیشتر: مستندات مربوط به پلات‌لی و یکی از دو API این فصل را ببینید. از اطّلاعاتی که پیدا می‌کنید استفاده کنید تا شکل نمودارها را شخصی کنید یا اطّلاعاتی دیگری در آن‌ها قرار دهید و بصری‌سازی‌های خود را بسازید.

2.6.17 - خلاصه

در این فصل آموختید که چگونه از APIها برای نوشتن برنامه‌های خودمتّکی استفاده کنید که داده‌های مورد نیاز خود را جمع‌آوری و آن‌ها را بصری می‌کنند. از API گیت‌هاب برای بررسی پرستاره‌ترین پروژه‌های پایتون در گیت‌هاب استفاده کردید و نگاهی گذرا به API سایت Hacker News انداختید. یاد گرفتید که چگونه از بسته‌ی Requests برای ارسال خودکار فراخوانی API به گیت‌هاب و روش پردازش نتایج آن استفاده کنید. برخی از تنظیمات پلات‌لی نیز برای شخصی‌سازی بیشتر نمودارهایی که تولید می‌کنند معرّفی شدند.

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


2.7 - فصل هجدهم

2.7.1 - شروع کار با جانگو

وب‌سایت‌های امروز، در پشتِ صحنه، نرم‌افزارهای کارب144 ردی غنی و کاملی هستند که مانند برنامه‌های رومیزی کاملاً توسعه‌یافته رفتار می‌کنند. پایتون مجموعه‌ی بزرگ از ابزارها به نام جانگو برای ساختن برنامه‌های کاربردی تحت وب دارد. جانگو یک چهارچوب یا فریم‌ورک برای وب است— مجموعه‌ای از ابزارها که برای کمک به شما در ساختن وب‌سایت‌های تعاملی طرّاحی شده‌اند. در این فصل خواهید آموخت که چگونه از جانگو (https://djangoproject.com/) برای ساختن پروژه‌ای به نام Learning Log استفاده کنید— یک سیستم گزارش کار آنلاین که به شما امکان می‌دهد که دانسته‌هایی که درباره‌ی موضوعات خاصّی کسب کرده‌اید ثبت کنید.

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

جانگو می‌تواند به درخواست‌های صفحه پاسخ دهد و خواندن و نوشتن در پایگاه داده، مدیرّیت کاربران و … را ساده می‌کند. در فصل‌های ۱۹ و۲۰ این پروژه را بهبود می‌بخشید و سپس آن‌ را روی یک سرور مستقر می‌کنید تا خود و دوستانتان بتوانند از آن استفاده کنند.

2.7.2 - برپا کردن یک پروژه

هنگام شروع یک پروژه اوّلین کار شرح دادن پروژه در ی146 145 ک مجموعه مشخّصات است. سپس یک محیط مجازی برپا می‌کنید که در آن پروژه را خواهید ساخت.

نوشتن مشخّصات

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

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

وقتی در مورد موضوع جدیدی چیزی می‌آموزید، ثبت کردن آنچه آموخته‌اید می‌تواند در پیگیری و مرور اطّلاعات کمک کند. یک برنامه‌ی خود می‌تواند این فرآیند را کارآمد کند.

ساختن یک محیط مجازی

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

پوشه‌ای جدید به نام learning_log برای پروژه‌ی خود بسازید و در یک نشست ترمینال وارد آن پوشه شوید، ‌سپس برای ساختن محیط مجازی کد زیر را وارد کنید:

1
2
learning_log$ python -m venv ll_env
learning_log$

اینجا ماژول محیط مجازی venv را اجرا کرده و از آن برای ساختن یک محیط مجازی جدید به نام ll_env استفاده می‌کنیم. (توجّه کنید که این نام با دو حرف l شروع می‌شود نه دو عدد 1). اگر برای اجرای برنامه‌ها یا نصب بسته‌ها از دستوری مانند python3 استفاده می‌کرده‌اید، اکنون نیز آن را به کار ببرید.

فعّال‌سازی محیط مجازی

اکنون باید با فرمان زیر محیط مجازی را فعّال کنیم:

1
2
learning_log$ source ll_env/bin/activate
(ll_env)learning_log$

این دستور اسکریپت activate در ll_env/bin را اجرا می‌کند. زمانی که محیط فعّال باشد، نام آن را داخل پرانتز خواهید دید، مانند آن‌چه در بالا می‌بینید؛ سپس می‌توانید بسته‌ها را در آن نصب کنید و از بسته‌هایی که قبلاً نصب کرده‌اید استفاده کنید. بسته‌هایی که در ll_env نصب می‌کنید تنها زمانی در دسترس‌اند که محیط فعّال باشد.

نکته
اگر از ویندوز استفاده می‌کنید برای فعّال‌سازی محیط مجازی از دستور ll_env\Script\activate استفاده کنید (بدون کلمه‌ی source). اگر از PowerShell استفاده می‌کنید شاید نیاز باشد که Activate را با حرف اوّل بزرگ بنویسید.

برای توقّف کار با یک محیط مجازی deactivate را وارد کنید:

1
2
(ll_env)learning_log$ deactivate
learning_log$

همچنین اگر ترمینالی را که محیط مجازی در آن اجرا می‌شود ببندید، محیط غیرفعّال می‌شود.

نصب جانگو

پس از فعّال شدن محیط مجازی موارد زیر را برای نصب جانگو وارد کنید:

1
2
3
4
5
6
(ll_env)learning_log$ pip install django
Collecting django
--snip--
Installing collected packages: pytz, django
Successfully installed django-2.2.0 pytz-2018.9 sqlparse-0.2.4
(ll_env)learning_log$

چون اکنون در یک محیط مجازی کار می‌کنیم، که محیطی خودمتّکی است، این دستور در تمام سیستم‌ها یکسان خواهد بود. نیازی به استفاده از پرچم –user نیست و لازم نیست فرمان‌های طولانی‌تری مانند python -m pip install package_name استفاده کنید.

در خاطر داشته باشید که جانگو تنها در صورت فعّال بودن محیط ll_env در دسترس خواهد بود.

نکته
جانگو هر هشت ماه یک بار نسخه‌ی جدیدی منتشر می‌کند پس ممکن است هنگام نصب آن نسخه‌ی جدیدتری ببینید. این پروژه همان‌گونه که اینجا نوشته شده به احتمال زیاد حتّی در نسخه‌های جدید جانگو هم کار خواهد کرد. اگر می‌خواهید مطمئن شوید که از نسخه‌ی مشابه اینجا استفاده می‌کنید از دستور pip install django==2.2.* استفاده کنید. این دستور آخرین نسخه‌ی جانگوی 2.2 را نصب می‌کند. اگر با نسخه‌ای که استفاده می‌کنید مشکلی دارید منابع آنلاین کتاب را در https://nostarch.com/pythoncrashcourse2e ببینید.

ساختن یک پروژه در جانگو

بدون ترک کردن محیط مجازی فعّال (حواستان به عبارت ll_env داخل پرانتز در اعلان ترمینال باشد) دستورهای زیر را برای ساختن یک پروژه‌ی جدید وارد کنید:

1
2
3
4
5
(ll_env)learning_log$ django-admin startproject learning_log .
(ll_env)learning_log$ ls
learning_log  ll_env  manage.py
(ll_env)learning_log$ ls learning_log
__init__.py  settings.py  urls.py  wsgi.py

دستور اوّل از جانگو می‌خواهد که پروژه‌ی جدیدی به نام learning_log بسازد. نقطه در انتهای دستور پوشه‌های پروژه‌ی جدید را با ساختاری می‌سازد که استقرار برنامه را در سرور پس از پایان توسعه‌ی آن آسان می‌کند.

نکته
گذاشتن این نقطه را فراموش نکنید وگرنه هنگام استقرار برنامه به برخی مشکلات در تنظیمات برخواهید خورد. اگر نقطه را فراموش کردید فایل‌ها و پوشه‌های ایجادشده را پاک کنید (به جز ll_env) و سپس دستور را دوباره اجرا کنید.

اجرای دستور ls در خروجی نشان می‌دهد که جانگو پوشه‌ای جدید به نام learning_log ساخته است. یک فایل manage.py هم ساخته شده که برنامه‌ای کوتاه است که دستورها را می‌گیرد و آن‌ها را بخش‌های مرتبط جانگو می‌دهد تا آن‌ها را اجرا کند. از این دستورها برای مدیریّت وظیفه‌ها مانند کار کردن با پایگاه‌های داده و اجرای سرورها استفاده خواهیم کرد.

پوشه‌ی learning_log چهار فایل را شامل می‌شود؛ که مهم‌ترین آن‌ها settings.py، urls.py و wsgi.py است. فایل settings.py نحوه‌ی تعامل جانگو با سیستم شما را کنترل و پروژه‌ی شما را مدیریّت می‌کند. همگام با تحوّل پروژه برخی از این تنظیمات را تغییر داده و برخی تنظیمات خود را نیز اضافه خواهیم کرد. فایل urls.py به جانگو می‌گوید که در پاسخ به درخواست‌های مرورگر چه صفحاتی بسازید. فایل wsgi.py به جانگو کمک می‌کند که فایل‌هایی را که ایجاد می‌کند ارائه کند. نام این فایل سرواژه‌ی web server gateway interface است.

ساختن پایگاه داده

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(ll_env)learning_log$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  --snip--
  Applying sessions.0001_initial... OK
(ll_env)learning_log$ ls
db.sqlite3  learning_log  ll_env  manage.py

هر گاه یک پایگاه داده را تغییر می‌دهیم از اصطلاح 147 انتقال پایگاه داده استفاده می‌کنیم. اوّلین اجرای دستور migrate از جانگو می‌خواهد که از تطابق پایگاه داده با وضعیّت فعلی پروژه اطمینان حاصل کند. اوّلین باری که این دستور را در یک پروژه‌ی جدید با SQLite (به زودی به آن خواهیم پرداخت) اجرا می‌کنید جانگو یک پایگاه داده‌ی جدید برای ما می‌سازد. جانگو اطّلاع می‌دهد که پایگاه داده را برای اطّلاعات مورد نیاز جهت مدیریّت وظایفه‌های مدیریّت و احراز هویّت آماده خواهد کرد.

اجرای دستور ls نشان می‌دهد که جانگو فایل دیگری به نام db.sqlite3 ساخته است. SQLite یک پایگاه داده است که تنها در یک فایل خلاصه می‌شود و برای نوشتن برنامه‌های ساده ایده‌ال است چون نیاز نیست بیش از حد به جزئیات مدیریّت پایگاه داده بپردازید.

نکته
برای اجرای دستورهای manage.py در یک محیط مجازی فعّال از فرمان python استفاده کنید حتّی اگر برای اجرای دیگر برنامه‌ها از python3 بهره می‌گیرید. در یک محیط مجازی فرمان python به نسخه‌ی پایتونی اشاره می‌کند که محیط مجازی را ساخته است.

مشاهده‌ی پروژه

اجازه دهید از برپایی درست پروژه مطمئن شویم. دستور runserver را مانند زیر اجرا کنید تا وضعیّت فعلی پروژه را ببینید:

1
2
3
4
5
6
7
8
9
(ll_env)learning_log$ python manage.py runserver
Watchman unavailable: pywatchman not installed.
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
February 18, 2019 - 16:26:07
Django version 2.2.0, using settings 'learning_log.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

جانگو باید سروری به نام سرور توسعه را راه بیاندا148 زد تا شما بتوانید پروژه را در سیستم خود ببینید و از صحّت عملکرد آن مطمئن شوید. زمانی که با وارد کردن یک URL در یک مرورگر صفحه‌ای را درخواست می‌کنید سرور جانگو با ساختن صفحه‌ی متناسب به آن پاسخ می‌دهد و آن را به مرورگر می‌فرستد.

جانگو بررسی می‌کند تا مطمئن شود پروژه به درستی برپا شده؛ نسخه‌ی جانگو و نام فایل تنظیمات مورد استفاده را گزارش می‌150 149 کند؛ URLای را که پروژه در آن ارائه می‌شود گزارش می‌کند. آدرس http://127.0.0.1:8000/ نشان می‌دهد که به درخواست‌های روی درگاه ۸۰۰۰ در رایانه‌ی شما، که میزبان‌محلّی نامیده می‌شود، پاسخ می‌دهد. عبارت میزبان محلّی به سروری اشاره می‌کند که تنها به درخواست‌های روی سیستم شما پاسخ می‌دهد؛ این سرور به هیچ کس دیگری اجازه نمی‌دهد که صفحه‌های در حال توسعه‌ی شما را ببیند.

یک مرورگر وب باز کنید و آدرس http://localhost:8000/ یا اگر کار نکرد، http://127.0.0.1:8000/ را وارد کنید. باید صفحه‌ای ببینید که جانگو می‌سازد تا نشان دهد تا اینجا همه چیز به خوبی پیش رفته است. فعلاً سرور را در حال اجرا نگه دارید امّا هرگاه خواستید آن را متوقّف کنید CTRL+C را در ترمینالی که فرمان runserver صادر شده اجرا کنید.

نکته
اگر پیام خطایی مبنی بر اشغال بودن درگاه دریافت کردید با وارد کردن دستور python manage.py runserver 8001 از جانگو بخواهید که از درگاه دیگری استفاده کنید و شماره‌ی درگاه را افزایش دهید تا یک درگاه خالی پیدا کنید.

خودآزمایی

۱۸-۱- پروژه‌های جدید: برای این که درک بهتری از جانگو پیدا کنید چند پروژه‌های خالی بسازید و به فایل‌هایی که می‌سازد توجّه کنید. پوشه‌ی جدید با نامی ساده مانند snap_gram یا insta_chat (خارج از پوشه‌ی learning_log) بسازید، با یک ترمینال وارد آن شوید و یک محیط مجازی بسازید. جانگو را نصب و فرمان django-admin.py startproject snap_gram . را اجرا کنید. (مطمئن شوید که نقطه را در پایان دستور می‌آورید.)

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

2.7.3 - آغاز کردن یک برنامه

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

شما باید اجازه دهید که سرور توسعه‌ای که پیش‌تر در پنجره‌ی ترمینال باز کرده‌اید به کار خود ادامه دهد. یک برگه یا پنجره‌ی ترمینال جدید باز کنید و به پوشه‌ای بروید که manage.py در آن است. محیط مجازی را فعّال کنید و فرمان startapp را اجرا کنید:

1
2
3
4
5
6
learning_log$ source ll_env/bin/activate
(ll_env)learning_log$ python manage.py startapp learning_logs
(ll_env)learning_log$ ls
db.sqlite3  learning_log  learning_logs  ll_env  manage.py
(ll_env)learning_log$ ls learning_logs/
__init__.py  admin.py  apps.py  migrations  models.py  tests.py  views.py

دستور startapp از جانگو می‌خواهد که زیرساخت‌های لازم را برای ساختن یک برنامه فراهم آورد. اگر اکنون به پوشه‌ی پروژه نگاه کنید پوشه‌ای جدید به نام learning_logs خواهید دید. آن پوشه را باز کنید تا ببینید جانگو چه چیزهایی در آن ساخته است. مهم‌ترین فایل‌ها عبارت‌اند از models.py، admin.py و views.py. از models.py برای تعریف کردن داده‌هایی که می‌خواهیم در برنامه مدیریّت کنیم استفاده می‌کنیم. به دو فایل admin.py و views.py کمی بعد نگاهی می‌اندازیم.

تعریف کردن مدل‌ها

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

فایل models.py را باز کنید و به محتویّات فعلی آن نگاهی بیندازید:

1
2
3
from django.db import models

# Create your models here.

ماژولی به نام models برای ما وارد شده است و از ما دعوت شده که مدل‌های خود را بسازیم. یک مدل به جانگو می‌گوید که چگونه با داده‌های ذخیره‌شده در برنامه کار کند. از منظر کدنویسی مدل تنها یک کلاس است که درست مانند همه‌ی کلاس‌هایی که مورد بحث قرار دادیم، مشخّصه‌ها و متدهایی دارد. برای موضوعاتی که کاربران ذخیره خواهند کرد چنین مدلی داریم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from django.db import models

class Topic(models.Model):
    """A topic the user is learning about."""
    text = models.CharField(max_length=200)
    date_added = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        """Return a string representation of the model."""
        return self.text

کلاسی به نام Topic ساخته‌ای که از Model ارث می‌برد—کلاس والدی که در جانگو آورده شده و قابلیّت‌های ابتدایی یک مدل را تعریف می‌کند. به کلاس Topic دو مشخّصه‌ی text و date_added را اضافه می‌کنیم.

مشخّصه‌ی text یک نوع CharField است— بخشی داده که از حرف‌ها یا متن تشکیل شده است. از CharField زمانی استفاده می‌کنید که می‌خواهید متن‌های کوچکی مانند نام، عنوان یا شهر را ذخیره کنید. زمانی که یک مشخّصه‌ی CharField تعریف می‌کنیم باید به جانگو بگوییم که چه‌قدر فضا در پایگاه داده اشغال خواهد کرد. اینجا ما max_length را ۲۰۰ حرف تعریف کرده‌ایم که برای نگه‌داری نام بیشتر موضوعات کافی است.

مشخّصه‌ی date_added از نوع DateTimeField است— داده‌ای که تاریخ و ساعتی را ضبط خواهد کرد. نشان‌وند auto_now_add=True را به آن می‌دهیم تا هر گاه کاربر موضوع جدیدی ایجاد کرد جانگو به طور خودکار این مشخّصه را برابر با تاریخ و زمان فعلی قرار دهد.

نکته
برای دیدن انواع مختلف فیلدهایی که می‌توانید در یک مدل استفاده کنید، مرجع فیلدِ مدل جانگو را در آدرس https://docs.djangoproject.com/en/2.2/ref/models/fields ببینید. همین الان به این اطّلاعات نیازی ندارید امّا این مرجع هنگامی که برنامه‌های خود را توسعه می‌دهید بسیار مفید خواهد بود.

به جانگو می‌گوییم که به طور پیش‌فرض کدام مشخّصه را درباره‌ی یک موضوع نشان دهد. جانگو یک متد __str__() را برای نمایش ساده‌ی یک مدل فرامی‌خواند. اینجا یک متد __str__() نوشته‌ایم که رشته‌ی ذخیره‌شده در مشخّصه‌ی text را برمی‌گرداند.

فعّال‌سازی مدل‌ها

برای استفاده از مدل‌های خود باید به جانگو بگوییم که برنامه‌ی ما را در پروژه‌ی کلّی بگنجاند. فایل settings.py را (در پوشه‌ی learning_log/learning_log) باز کنید؛ بخشی را خواهید دید که به جانگو می‌گوید چه برنامه‌های نصب شده‌اند و با هم در پروژه کار می‌کنند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--snip--
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
--snip--

برنامه‌ی خود را با تغییر دادن INSTALLED_APPS به فهرست اضافه می‌کنیم تا به این شکل درآید:

1
2
3
4
5
6
7
8
9
--snip--
INSTALLED_APPS = [
    # My apps
    'learning_logs',
    # Default django apps.
    'django.contrib.admin',
    --snip--
]
--snip--

گروه‌بندی برنامه‌ها با هم در یک پروژه کمک می‌کند که همزمان با بزرگ شدن پروژه و در برگرفتن برنامه‌های بیشتر، آن‌ها را پیگیری کنید. اینجا بخشی به نام My Apps می‌سازیم که فعلاً تنها شامل learning_logs است. قرار دادن برنامه‌ی خود قبل از برنامه‌های پیش‌فرض زمانی مهم است که بخواهید هر کدام از رفتارهای این برنامه‌های پیش‌فرض را با رفتار دلخواه خود جایگزین کنید.

در گام بعدی باید به جانگو بگوییم که پایگاه داده را تغییر دهد تا بتواند اطّلاعات مرتبط با مدل Topic را ذخیره کند. در ترمینال دستور زیر را اجرا کنید:

1
2
3
4
5
(ll_env)learning_log$ python manage.py makemigrations learning_logs
Migrations for 'learning_logs':
  learning_logs/migrations/0001_initial.py
    - Create model Topic
(ll_env)learning_log$

دستور makemigrations به جانگو می‌فهماند که چگونه پایگاه‌داده را برای ذخیره‌سازی داده‌های مرتبط با مدل‌های تعریف‌شده تغییر دهد. خروجی اینجا نشان می‌دهد که جانگو یک فایل جابه‌جایی به نام 0001_initial.py ساخته است که جدولی برای مدل Topic در پایگاه داده خواهد ساخت.

اکنون این انتقال را اعمال می‌کنیم و جانگو پایگاه داده را برای ما تغییر می‌دهد:

1
2
3
4
5
(ll_env)learning_log$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, learning_logs, sessions
Running migrations:
  Applying learning_logs.0001_initial... OK

بیشتر خروجی این دستور عیناً مشابه اوّلین اجرای آن است. خطّی که باید آن را بررسی کنیم جایی است که جانگو تایید می‌کند انتقال learning_logs به درستی انجام شده است.

هرگاه بخواهیم داده‌هایی را تغییر دهیم که Learning Log مدیریّت می‌کند، باید این سه گام را پی بگیریم: تغییر دادن models.py، فراخوانی makemigrations برای learning_logs و درخواست از جانگو برای migrate کردن پروژه.

2.7.4 - سایت مدیریّت جانگو

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

برپایی یک ابرکاربر

جانگو به شما امکان می‌دهد که یک ابرکاربر بسازید؛152 151 کاربری که از تمام امتیازات موجود در سایت برخوردار است. امتیازات یک کاربر اعمالی را که کاربر می‌تواند انجام دهد کنترل می‌کند.

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

برای ساختن یک ابرکاربر در جانگو دستور زیر را وارد کنید و به اعلان‌های آن پاسخ دهید:

1
2
3
4
5
6
7
(ll_env)learning_log$ python manage.py createsuperuser
Username (leave blank to use 'eric'): ll_admin
Email address:
Password:
Password (again):
Superuser created successfully.
(ll_env)learning_log$

زمانی که دستور createsuperuser را صادر می‌کنید جانگو از شما نام کاربری مورد نظرتان را برای ابرکاربر می‌پرسد. اینجا من از ll_admin استفاده می‌کنم امّا شما می‌توانید هر نامی که خواستید وارد کنید. می‌توانید در صورت تمایل یک آدرس ایمیل هم وارد کنید یا این اعلان را خالی بگذارید. باید گذرواژه‌ی خود را دو بار وارد کنید.

نکته
برخی اطّلاعات حسّاس از مدیران سایت مخفی‌اند. برای مثال جانگو گذرواژه را153 به صورتی که شما وارد می‌کنید ذخیره نمی‌کند؛ بلکه آن را به صورت رشته‌ای که از این گذرواژه به دست می‌آید و درهم‌ساخته یا هَش نامیده می‌شود، ضبط می‌کند. هر بار که شما گذرواژه‌ای وارد می‌کنید جانگو آن را در هم می‌ریزد و سپس با هَش ذخیره‌شده مقایسه می‌کند. اگر این دو منطبق باشند هویّت شما احراز می‌شود. با الزام انطباق این دو هَش، اگر مهاجمی به پایگاه داده‌ی سایت شما دسترسی پیدا کند، می‌تواند هش‌های ذخیره‌شده را بخواند نه گذرواژه‌ها را. اگر سایتی درست بر پا شود بازیابی گذرواژه‌های اصلی از هش‌ها تقریباً غیر ممکن است.

ثبت کردن یک مدل با سایت مدیر

جانگو چند مدل را به طور خودکار دربرمی‌گیرد، مانند User و Group، امّا مدل‌هایی که خودمان ساخته‌ایم باید به صورت دستی اضافه شوند.

زمانی که برنامه‌ی learning_logs را شروع کردیم جانگو یک فایل admin.py در همان پوشه‌ی models.py ایجاد کرد. آن را باز کنید:

1
2
3
from django.contrib import admin

# Register your models here.

برای ثبت Topic از طریق سایت مدیرّیت کد زیر را وارد کنید:

1
2
3
4
from django.contrib import admin
from .models import Topic

admin.site.register(Topic)

در این کد ابتدا مدلی، Topic، را وارد می‌کنیم که می‌خواهیم آن را ثبت کنیم. نقطه‌ی جلوی models به جانگو می‌گوید که در همان پوشه‌ی حاوی admin.py به دنبال models.py بگردد. کد admin.site.register() از جانگو می‌خواهد که مدل ما را از طریق سایت مدیر مدیریّت کند.

اکنون از حساب کاربری ابرکاربر برای دسترسی به سایت مدیر استفاده کنید. به آدرس https://localhost:8000/admin/ بروید و نام کاربری و گذرواژه‌ی ابرکاربری را که ساخته‌اید وارد کنید. باید صفحه‌ای ببینید که به شما اجازه می‌دهد که کاربران و گروه‌های جدید را ایجاد کرده یا قبلی‌ها را تغییر دهید. همچنین می‌توانید با مدل Topic که کمی پیش‌تر تعریف کرده‌اید کار کنید.

نکته
اگر در مرورگر خود پیامی مبنی بر در دسترسی نبودن صفحه‌ی وب دیدید مطمئن شوید که سرور جانگو همچنان در پنجره‌ی ترمینال اجرا می‌شود. اگر چنین نیست یک محیط مجازی فعّال کنید و فرمان python manage.py runserver را مجدداً صادر کنید. اگر هر جایی در فرآیند توسعه در مشاهده‌ی پروژه‌ی خود دچار مشکل شدید، اوّلین گام مناسب برای رفع اشکال بستن تمام ترمینال‌ها و صدور مجدّد فرمان runserver است.

افزودن موضوعات

اکنون که Topic از طریق سایت مدیر ثبت شده بیایید اوّلین موضوع خود را اضافه کنیم. روی Topics کلیک کنید تا به صفحه‌ی موضوعات بروید که تقریباً خالی است زیرا هنوز هیچ موضوعی برای مدیریّت نداریم. روی Add Topic کلیک کنید تا فرمی برای افزودن موضوع جدید ظاهر شود. Chess را در اوّلین خانه وارد کنید و Save را بزنید. به صفحه‌ی مدیریّت موضوعات برگردانده می‌شوید و موضوعی را که هم‌اکنون اضافه کردید می‌بینید.

موضوع دومی را هم اضافه می‌کنیم تا داده‌ی بیشتری برای کار کردن داشته باشیم. دوباره روی Add Topic کلیک کنید و این بار Rock Climbing را وارد کنید. Save را فشار دهید تا دوباره به صفحه‌ی اصلی موضوعات برگردید. حال شما شطرنج و صخره‌نوردی را در فهرست خود دارید.

تعریف کردن مدلی برای مطلب‌ها

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

این کد مدل Entry است. آن را در فایل models.py قرار دهید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from django.db import models

class Topic(models.Model):
    --snip--

class Entry(models.Model):
    """Something specific learned about a topic."""
    topic = models.ForeignKey(Topic, on_delete=models.CASCADE)
    text = models.TextField()
    date_added = models.DateTimeField(auto_now_add=True)

    class Meta:
        verbose_name_plural = 'entries'

    def __str__(self):
        """Return a string representation of the model."""
        return f"{self.text[:50]}..."

کلاس Entry مانند Topic از کلاس Model جانگو ارث می‌156 155 برد. اوّلین مشخّصه، topic، نمونه‌ای از ForeignKey است. یک کلید خارجی اصطلاحی در پایگاه‌های داده است و ارجاعی به یک رکورد دیگر در پایگاه داده به شمار می‌آید. به هر موضوع هنگام ساخته شدن، یک کلید یا ID نسبت داده می‌شود. زمانی که جانگو می‌خواهد بین دو داده ارتباط برقرار کند از کلید‌هایی استفاده می‌کند که به هر کدام نسبت داده شده است. به زودی از این ارتباط‌ها برای دریافت تمام مطالب مرتبط با یک موضوع خاص استفاده خواهیم کرد. نشان‌وند on_delete=models.CASCADE به جانگو می‌گوید که اگر موضوعی حذف شد، تمام موضوعات مرتبط با آن را هم حذف کن. این روش به نام حذف آبشاری شناخته می‌شود.

مشخّصه‌ی بعدی text نامیده می‌شود که نمونه‌ای از TextField است. این فیلد نیازی به محدودیّت طول ندارد زیرا قصد نداریم اندازه‌ی مطالب را محدود کنیم. مشخّصه‌ی date_added به ما امکان می‌دهد که مطالب را به ترتیبی که ایجاد شده‌اند نشان دهیم و کنار هر کدام یک برچسب زمانی قرار دهید.

کلاس Meta را داخل کلاس Entry قرار می‌دهیم. این کلاس اطّلاعات اضافی را در مورد مدیریّت یک مدل در برمی‌گیرد؛ اینجا به ما امکان می‌دهد که مشخّصه‌ای ویژه تنظیم کنیم تا به جانگو بگوییم که هرگاه به بیش از یک موضوع اشاره کرد، از Entries استفاده کند. بدون این جانگو به چند موضوع Entrys اطلاق می‌کند.

متد __str__() برای جانگو مشخّص می‌کند که چه هنگام نمایش هر موضوع چه چیزی را نشان دهد. از آن‌جایی که طول متن می‌تواند خیلی زیاد باشد از جانگو می‌خواهیم که تنها ۵۰ حرف آن را نمایش دهد. یک سه‌نقطه نیز اضافه می‌کنیم که شفّاف کنیم همیشه تمام مطلب را نمایش نمی‌دهیم.

2.7.5 - انتقال مدل Entry

چون مدل جدیدی اضافه کرده‌ایم باید پایگاه داده را دوباره تغییر دهیم. به تدریج با این فرآیند آشنا خواهید شد: ابتدا models.py را تغییر می‌دهید، سپس فرمان python manage.py makemigrations app_name را صادر و دستور python manage.py migrate را اجرا می‌کنید.

پایگاه داده را انتقال دهید و خروجی را با وارد کردن فرمان‌های زیر بررسی کنید:

1
2
3
4
5
6
7
8
(ll_env)learning_log$ python manage.py makemigrations learning_logs
Migrations for 'learning_logs':
  learning_logs/migrations/0002_entry.py
    - Create model Entry
(ll_env)learning_log$ python manage.py migrate
Operations to perform:
  --snip--
  Applying learning_logs.0002_entry... OK

فایل انتقال جدیدی به نام 0002_entry.py تولید می‌شود که به جانگو می‌گوید چگونه پایگاه داده را برای ذخیره‌سازی اطّلاعات مربوط به مدل Entry تغییر دهد. هنگام صدور فرمان migrate می‌بینیم که جانگو انتقال را انجام داده و همه چیز درست است.

ثبت Entry با سایت مدیر

باید مدل Entry را نیز ثبت کنیم. اکنون باید فایل admin.py اینگونه به نظر بیاید:

1
2
3
4
5
from django.contrib import admin
from .models import Topic, Entry

admin.site.register(Topic)
admin.site.register(Entry)

به http://localhost/admin/ بروید موضوعات فهرست شده ذیل Learning_Log را ببینید. روی پیوند Add برای موضوعات یا روی Entries کلیک کنید و سپس Add entry را بزنید. اینجا باید یک لیست پایین‌افتادنی برای انتخاب موضوع مطلب خود و یک خانه برای وارد کردن متن خود ببینید. Chess را از لیست پایین‌افتادنی انتخاب کنید و یک موضوع اضافه کنید. این اوّلین مطلبی است که من ساخته‌ام:

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

البته این‌ها تنها چند راهنماست. مهم است که یاد بگیرید کی از آن‌ها پیروی کنید و کی به این توصیه‌ها عمل نکنید.

زمانی که روی Save کلیک می‌کنید به صفحه‌ی اصلی مدیریّت مطالب برمی‌گردید. این فایده‌ی استفاده از text[:50] را برای نمایش رشته‌ای هر مطلب می‌بینید: اگر به جای کلّ مطلب تنها بخشی از آن را ببینید کار کردن با چند مطلب در رابط مدیریّتی بسیار ساده‌تر خواهد بود.

مطلب دومی در مورد شطرنج و یک مطلب در رابطه با صخره‌نوردی اضافه کنید تا کمی داده‌ی ابتدایی داشته باشیم. این دومین مطلب در مورد شطرنج است:

در مرحله‌ی آغازین بازی مهم است که اسب‌ها و فیل‌های خود را بیرون بیاورید. این مهره‌ها قدرتمندند و به اندازه‌ی کافی تحرّک‌پذیرند تا در حرکت‌های ابتدایی بازی نقش مهمّی بازی کنند.

و این اوّلین مطلب درباره‌ی صخره‌نوردی است:

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

همچنان که به توسعه‌ی Learning Log ادامه می‌دهیم این سه مطلب حداقل‌هایی برای کار کردن فراهم می‌آورند.

پوسته‌ی جانگو

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

1
2
3
4
(ll_env)learning_log$ python manage.py shell
>>> from learning_logs.models import Topic
>>> Topic.objects.all()
<QuerySet [<Topic: Chess>, <Topic: Rock Climbing>]>

فرمان python manage.py shell که در یک محیط مجازی فعّال اجرا می‌شود، یک مفسّر پایتون فراهم می‌آورد که می‌توانید از آن برای بررسی داده‌های ذخیره‌شده در پایگاه داده‌ی پروژه‌ی خود استفاده کنید. اینجا مدل Topic را از ماژول learning_logs.models وارد کرده‌ایم. سپس از متد Topic.objects.all() برای گرفتن تمام نمونه‌های مدل Topic استفاده می‌کنیم؛ لیستی که برگردانده می‌شود یک queryset نامیده می‌شود.

درست مثل یک لیست می‌توانید در یک queryset هم حلقه بزنید. اینجا روش فهمیدن شماره‌ای را که به هر عنوان نسبت داده شده می‌بینیم:

1
2
3
4
5
6
>>> topics = Topic.objects.all()
>>> for topic in topics:
...     print(topic.id, topic)
...
1 Chess
2 Rock Climbing

این queryset را در topics ذخیره می‌کنیم و سپس مشخّصه‌ی id هر موضوع و بیان رشته‌ای آن را چاپ می‌کنیم. می‌بینیم که موضوع شطرنج شماره‌ی ۱ و موضوع صخره‌نوردی شماره‌ی ۲ را گرفته است.

اگر شماره‌ی یک شیء خاص را بدانید می‌توانید از متد Topic.objects.get() برای دریافت آن و بررسی هرکدام از مشخّصه‌های آن استفاده کرد. بیایید نگاهی به مقدارهای text و date_added مربوط به Chess بیندازیم:

1
2
3
4
5
>>> t = Topic.objects.get(id=1)
>>> t.text
'Chess'
>>> t.date_added
datetime.datetime(2019, 2, 19, 1, 55, 31, 98500, tzinfo=<UTC>)

علاوه بر این‌ها می‌توانیم مطالب مرتبط با یک موضوع خاص را هم ببینیم. پیش‌تر مشخّصه‌ی topic برای مدل Entry تعریف کردیم. این یک ForeignKey است، رابطه‌ای بین یک موضوع و یک مطلب. جانگو از این ارتباط برای گرفتن هر مطلب مرتبط با یک موضوع استفاده می‌کند؛ مانند زیر:

1
2
3
>>> t.entry_set.all()
<QuerySet [<Entry: The opening is the first part of the game, roughly...>,
<Entry: In the opening phase of the game, it's important t...>]>

برای گرفتن داده‌ها از طریق رابطه‌ی کلید خارجی می‌توانید از نام مدل مربوطه با حروف کوچک و به دنبال آن یک زیرخط و کلمه‌ی set استفاده کنید. برای مثال فرض کنید که دو مدل Pizza و Topping را دارید و Topping از طریق یک کلید خارجی با Pizza مرتبط است. اگر نام شیء شما my_pizza باشد، که نماینده‌ی یک پیتزاست، می‌توانید تمام مخلّفات این پیتزا را با استفاده از my_pizza.topping_set.all() دریافت کنید.

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

نکته
هر بار که مدل‌ها را تغییر می‌دهید برای دیدن اثر تغییراتی که اعمال کرده‌اید باید پوسته را از نو اجرا کنید. برای خروج از نشست پوسته CTRL-D را بزنید؛ در ویندوز Ctrl-Z و سپس Enter را بزنید.

خودآزمایی

۱۸-۲: مطالب کوتاه: در حال حاضر هنگامی که جانگو مطالب را در سایت مدیر یا در پوسته نشان می‌دهد، متد __str__() در مدل Entry سه‌‌نقطه را به تمام مطلب‌ها اضافه می‌کند. یک عبارت if به این متد اضافه کنید تا تنها در صورتی سه‌نقطه را اضافه کند که طول متن بیشتر از ۵۰ حرف باشد. از سایت مدیر برای اضافه کردن یک مطالب کوتاه‌تر از ۵۰ حرف استفاده کنید و بررسی کنید که سه‌نقطه هنگام نمایش وجود نداشته باشد.

۱۸-۳: API جانگو: زمانی که برای دسترسی به داده‌های158 پروژه‌ی خود کدی می‌نویسید، شما در واقع در حال نوشتن یک عبارت پرس و جو هستید. به مستندات مربوط به پرس و جو در آدرس https://docs.djangoproject.com/en/2.2/topics/db/queries/ نگاهی بیندازید. بیشتر چیزی که می‌بینید برای شما تازگی دارد امّا هنگام شروع کار بر پروژه‌های خود بسیار مفید خواهند بود.

۱۸-۴: پیتزافروشی: پروژه‌ای جدید به نام pizzeria با برنامه‌ای به نام pizzas آغاز کنید. یک مدل Pizza با فیلدی به نام name تعریف کنید که نام پیتزاها را نگه می‌دارد، مانند Hawaiian و Meat Lovers. مدلی به نام Topping با فیلدهای pizza و name تعریف کنید. فیلد pizza باید یک کلید خارجی به Pizza باشد و نام باید مقدارهای مربوط به مخلّفات مانند pineapple، Canadian bacon و sausage را ذخیره کند.

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

2.7.6 - ساختن صفحه‌ها: صفحه‌ی خانه‌ی Learning Log

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

هر URL با یک نمای خاص مرتبط می‌شود—تابع نما داده161 160 ‌های مورد نیاز برای تولید آن صفحه را دریافت و پردازش می‌کند. تابع نما اغلب صفحه را به کمک یک قالب شکل می‌دهد که ساختار کلّی صفحه را دربرمی‌گیرد. برای دیدن این روش کار بیایید برای Learning Log یک صفحه‌ی خانه بسازیم. برای این صفحه URLای تعیین می‌کنیم، تابع نمای آن را می‌نویسیم و یک قالب ساده می‌سازیم.

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

نگاشتن یک URL

کاربران با وارد کردن URLها در مرورگر یا کلیک کردن روی پیوندها صفحه‌ها را درخواست می‌کنند پس باید تصمیم بگیریم که به چه URLهایی نیاز داریم. URL صفحه‌ی خانه اوّلین مورد است: URL پایه است که افراد برای دسترسی به پروژه استفاده می‌کنند. در حال حاضر URL پایه http://localhost:8000/ است که سایت پیش‌فرض جانگو را برمی‌گرداند و به ما می‌گوید که پروژه به درستی برپا شده است. این صفحه را با نگاشت URL پایه به صفحه‌ی خانه‌ی Learning Log عوض می‌کنیم.

در پوشه‌ی اصلی پروژه‌ی learning_log فایل urls.py را باز کنید. چیزی که می‌بینید این است:

1
2
3
4
5
6
from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('admin/', admin.site.urls),
]

دو خطّ اوّل یک ماژول و یک تابع برای مدیریّت URLهای سایت مدیریّت وارد می‌کند. در بدنه‌ی فایل متغیّر urlpatterns تعریف می‌شود. در این فایل urls.py که کل پروژه را نمایندگی می‌کند، متغیّر urlpatterns مجموعه‌ای از URLهای برنامه‌های داخل پروژه را در برمی‌گیرد. کد در ماژول admin.site.urls را شامل می‌شود که تمام URLهایی را تعریف می‌کند که می‌توان از سایت مدیر درخواست کرد.

ما باید URLهای مربوط به learning_log را هم اضافه کنیم پس:

1
2
3
4
5
6
7
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('learning_logs.urls')),
]

خطّی اضافه کرده‌ایم تا ماژول learning_logs.urls را اضافه کنیم. فایل urls.py پیش‌فرض در پوشه‌ی learning_log قرار دارد؛ اکنون باید یک فایل urls.py دیگر در پوشه‌ی learning_logs بسازیم. یک فایل پایتون جدید ساخته و آن را در learning_logs به نام urls.py ذخیره کنید و کد زیر را در آن وارد کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
"""Defines URL patterns for learning_logs."""
from django.urls import path
from . import views

app_name = 'learning_logs'

urlpatterns = [
    # Home page
    path('', views.index, name='index'),
]

یک docstring به ابتدای فایل اضافه می‌کنیم تا مشخّص شود که روی کد urls.py در حال کاریم. سپس تابع path را وارد می‌کنیم که برای نگاشت URLها به نماها مورد نیاز است. ماژول views را هم وارد می‌کنیم؛ نقطه به پایتون می‌گوید که ماژول views.py را از همان پوشه‌ی ماژول urls.py فعلی وارد کند. متغیّر app_name به جانگو کمک می‌کند که این فایل urls.py را از فایل‌های هم‌نام در دیگر برنامه‌های پروژه تشخیص دهد. متغیّر urlpatterns در این ماژول لیستی از صفحه‌های جداگانه است که می‌تواند از طریق برنامه‌ی learning_logs درخواست شود.

الگوی URL در حقیقت فراخوانی تابع path() است که سه نشان‌وند می‌پذیرد. اوّلین نشان‌وند رشته‌ای است که به جانگو کمک می‌کند درخواست فعلی را به درستی مسیریابی کند. جانگو URL درخواستی را گرفته و سعی می‌کند که آن را به یک نما متّصل کند. این کار با جست‌و‌جوی تمام الگوهای تعریف‌شده تا یافتن موردی که به درخواست فعلی می‌خورد انجام می‌شود. جانگو URL پایه‌ی پروژه (https://localhost:8000/) را در نظر نمی‌گیرد پس رشته‌ی خالی (’’) متناظر با URL پایه است. هیچ URL دیگری با این الگو تطبیق نخواهد یافت و اگر جانگو هیچ الگویی مطابق با URL درخواستی پیدا نکند یک صفحه‌ی خطا برمی‌گرداند.

دومین نشان‌وند در path() تابعی را تعیین می‌کند که باید در view.py فراخوانی شود. زمانی که URL درخواستی با الگویی که تعریف کردیم منطبق شود جانگو تابع index() را از فایل view.py صدا می‌کند (این تابع را در بخش بعدی می‌نویسیم.) سومین نشان‌وند نام index را به این الگوی URL نسبت می‌دهد تا بتوانیم در بخش‌های دیگر کد به آن ارجاع دهیم. هرگاه بخواهیم پیوندی به صفحه‌ی خانه ارائه کنیم به جای نوشتن یک URL از این نام استفاده می‌کنیم.

نوشتن یک نما

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

فایل views.py در learning_logs هنگام اجرای فرمان python manage.py startapp به طور خودکار ایجاد می‌شود. این چیزی است که هم‌اکنون در views.py می‌بینیم:

1
2
3
from django.shortcuts import render

# Create your views here.

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

1
2
3
4
5
from django.shortcuts import render

def index(request):
    """The home page for Learning Log."""
    return render(request, 'learning_logs/index.html')

زمانی که درخواست یک URL با الگویی که ما تعریف کرده‌ایم منطبق می‌شود جانگو در فایل views.py به دنبال تابعی به نام index() می‌گردد. جانگو سپس شیء request را به تابع نما می‌دهد. در این مورد به پردازش هیچ داده‌ای احتیاج نداریم پس در این تابع تنها فراخوانی تابع render() قرار دارد. تابع render() اینجا دو نشان‌وند می‌گیرد—شیء request اصلی و یک قالب که برای ساختن صفحه مورد استفاده قرار می‌گیرد. بیایید این قالب را بنویسیم.

نوشتن یک قالب

قالب مشخّص می‌کند که صفحه باید چه شکلی باشد و هر بار که صفحه درخواست شود جانگو داده‌های مربوطه را جاگذاری می‌کند. یک قالب به شما امکان می‌دهد که به هر داده‌ای که نما ارائه می‌کند دسترسی داشته باشید. چون نمای صفحه‌ی خانه هیچ داده‌ای در اختیار نمی‌گذارد این قالب به نسبت ساده است.

داخل پوشه‌ی learning_logs پوشه‌ای جدید به نام templates بسازید. داخل آن پوشه‌ی دیگری به نام learning_logs بسازید. این ممکن است اضافه‌کاری به نظر بیاید (داخل پوشه‌ای به نام learning_logs پوشه‌ی templates قرار دارد و دوباره در آن پوشه‌ای به نام learning_logs) امّا این کار ساختاری ایجاد می‌کند که جانگو آن را حتّی در بستر پروژه‌های بزرگ یا چندین برنامه‌ی مجزّا، بدون هیچ ابهامی تفسیر می‌کند. داخل پوشه‌ی learning_logs داخلی فایلی به نام index.html بسازید. مسیر این فایل learning_logs/templates/learning_logs/index.html خواهد بود. این کد را داخل آن فایل بنویسید:

1
2
3
<p>Learning Log</p>
<p>Learning Log helps you keep track of your learning, for any topic you're
learning about.</p>

این فایلی بسیار ساده است. اگر با HTML آشنا نیستید، بدانید که تگ‌های <p></p> پاراگراف‌ها را مشخّص می‌کنند. تگ <p> پاراگراف را باز و </p> آن را می‌بندد. دو پاراگراف داریم: اوّلی مثل یک عنوان است و دومی توضیح می‌دهد که کاربران در Learning Logs چه می‌توانند بکنند.

حال اگر URL پایه‌ی پروژه، http://localhost:8000/، را باز کنید، باید به جای صفحه‌ی پیش‌فرض جانگو، صفحه‌ای را که هم‌اکنون ساخته‌اید ببینید. جانگو URL درخواستی را دریافت می‌کند و چون بر الگوی ’’ منطبق است؛ جانگو تابع views.index() را فرامی‌خواند که صفحه را به کمک قالبی که در index.html قرار گرفته تولید می‌کند.

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

نکته

ممکن است پیام خطای زیر را ببینید:

ModuleNotFoundError: No module named 'learning_logs.urls'

اگر چنین شد سرور توسعه را با فشردن CTRL+C در پنجره‌ی ترمینالی که فرمان runserver را از آن صادر کردید، متوقّف کنید. سپس دستور python manage.py runserver را مجدّداً وارد کنید. باید بتوانید صفحه‌ی خانه را ببینید. هر زمانی که به مشکلی مثل این برخوردید سرور را متوقّف کنید و دوباره راه بیندازید.

خودآزمایی

۱۸-۵: برنامه‌ریز غذایی: برنامه‌ای را در نظر بگیرید که به مردم در تعیین برنامه‌ی غذایی خود در طول هفته کمک می‌کند. پوشه‌ای جدید به نام meal_planner بسازید و داخل آن یک پروژه‌ی جدید جانگو ایجاد کنید. سپس برنامه‌ی جدیدی به نام meal_plans و یک صفحه‌ی خانه‌ی ساده برای این پروژه بسازید.

۱۸-۶: صفحه‌ی خانه‌ی پیتزافروشی: به پروژه‌ی پیتزافروشی که در تمرین ۱۸-۴ شروع کردید، به صفحه‌ی خانه اضافه کنید.

2.7.7 - ساختن صفحه‌های اضافی

حال که راه و روش مشخّصی برای ساختن یک صفحه ایجاد کرده‌ایم می‌توانیم ساختن پروژه‌ی Learning Log را شروع کنیم. دو صفحه برای نمایش داده‌ها می‌سازیم: یکی تمام عنوان‌ها را فهرست می‌کند و دیگری تمام موضوعات مرتبط با یک موضوع خاص را نشان می‌دهد. برای هر صفحه یک الگوی URL تعیین می‌کنیم، تابع نمایی می‌نویسیم و یک قالب می‌سازیم. امّا پیش از این یک قالب پایه ایجاد می‌کنیم که تمام قالب‌های این پروژه می‌توانند از آن ارث ببرند.

ارث‌بری قالب‌ها

برخی از عنصرها هنگام ساختن یک وب‌سایت در هر صفحه تکرار می‌شوند. به جای این که این عناصر را مستقیماً در هر صفحه بنویسید می‌توانید یک قالب پایه شامل تمام این موارد تکراری ایجاد کنید و سپس کاری کنید که هر صفحه از این قالب پایه ارث ببرد. این رویکرد به شما امکان تمرکز برای جنبه‌های یکتای هر صفحه می‌دهد و تغییر دادن شکل کلّی پروژه را بسیار ساده‌تر می‌کند.

قالب والد

قالبی به نام base.html در همان پوشه‌ی index.html می‌سازیم. این فایل تمام عناصر مشترک صفحه‌ها را دربرمی‌گیرد؛ هر قالب دیگری از base.html ارث خواهد برد. تنها عنصری که فعلاً می‌خواهیم در هر صفحه تکرار کنیم عنوان در بالای هر کدام است. چون این قالب را در هر صفحه‌ای خواهیم آورد بیایید عنوان را به صورتی پیوندی به صفحه‌ی خانه دربیاوریم:

1
2
3
4
5
<p>
  <a href="{% url 'learning_logs:index' %}">Learning Log</a>
</p>

{% block content %}{% endblock content %}

اوّلین بخش این فایل پاراگرافی شامل نام پروژه می‌سا162 زد که به صورت پیوندی به صفحه‌ی خانه هم عمل می‌کند. برای تولید یک پیوند از تگ قالب استفاده می‌کنیم که با آکُلاد و علامت درصد {% %} مشخّص می‌شود. هر تگ قالب اطّلاعاتی را برای نمایش در صفحه تولید می‌کند. تگ قالب ما {% url 'learning_logs:index' %} URLای تولید می‌کند که با الگوی URL تعریف‌شده در learning_logs/urls.py به نام ‘index’ مطابقت داشته باشد. در این مثل learning_logs یک فضای نام و index نام منحصربه‌فرد الگوی URL در آن فضای نام است. فضای نام از مقداری می‌آید که به app_name در فایل learning_logs/urls.py نسبت داده‌ایم.

در یک صفحه‌ی HTML ساده پیوندها داخل تگ لنگر <a> قرار می‌گیرد:

1
<a href="link_url">link text</a>

تولید URL به کمک تگ قالب کار را برای به‌روز نگه داشتن پ163 یوندها بسیار ساده‌تر می‌کند. کافی است تنها الگوی URLای که در urls.py تعریف کرده‌ایم تغییر دهیم و جانگو دفعه‌ی بعد که صفحه درخواست شد، به طور خودکار URL جدید را در آن قرار می‌دهد. هر صفحه‌ای در پروژه از base.html ارث می‌برد پس از الان به بعد هر کدام از صفحه‌ها پیوندی برای برگشتن به صفحه‌ی خانه خواهند داشت.

یک جفت تگ block قرار دادیم. این بلوک، با نام content، یک جانگه‌دار است؛ قالب فرزند تعیین خواهد کرد که چه نوع اطّلاعاتی در اینجا قرار می‌گیرد.

الزامی نیست که قالب فرزند تمام بلوک‌های والد خود را تعریف کند بنابراین می‌توانید در قالب والد برای هر تعداد بلوک که می‌خواهید جا نگه دارید؛ قالب فرزند تنها از آن‌هایی که می‌خواهد استفاده خواهد کرد.

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

قالب فرزند

اکنون باید index.html را بازنویسی کنیم تا از base.html ارث ببرد. کد زیر را به index.html اضافه کنید:

1
2
3
4
5
6
{% extends "learning_logs/base.html" %}

{% block content %}
  <p>Learning Log helps you keep track of your learning, for any topic you're
  learning about.</p>
{% endblock content %}

اگر این را با index.html اوّلیّه مقایسه کنید می‌بینید که عنوان Learning Log را به کدی برای ارث‌بری از قالب والد جایگزین کرده‌ایم. یک قالب فرزند باید یک تگ {% extends %} در اوّلین خط بیاورد تا به جانگو بگوید از کدام قالب والد باید ارث ببرد. فایل base.html بخشی از learning_logs است بنابراین learning_logs را در مسیر قالب والد قرار می‌دهیم. این خط همه‌ی آن‌چه که در قالب base.html وجود دارد بیرون می‌کشد و به index.html امکان می‌دهد که فضای نگه‌داشته‌شده در بلوک content را تعریف کند.

بلوک محتوا را و با داخل کردن یک تگ {% block %} با نام content تعریف می‌کنیم. هر چیزی که از قالب والد به ارث نرسد داخل بلوک content قرار می‌گیرد که اینجا یک پاراگراف است که در مورد پروژه‌ی Learning Log توضیح می‌دهد. پایان محتوا را با یک تگ {% endblock content %} معلوم می‌کنیم. تگ {% endblock %} به نام نیاز ندارد امّا اگر قالبی بزرگ شود و چند بلوک را دربربگیرد این که بدانیم کدام بلوک در حال خاتمه است برای ما سودمند است.

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

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

صفحه‌ی موضوعات

حال که رویکردی موثّر برای ساختن صفحه‌ها داریم می‌توانیم روی دو صفحه‌ی بعدی تمرکز کنیم: صفحه‌ی عمومی موضوعات و صفحه‌ای برای نمایش مطالب مربوط به هر موضوع. صفحه‌ی موضوعات تمام موضوع‌هایی را که کاربر ساخته نشان می‌دهد و اوّلین صفحه‌ای است که به کار با داده‌ها می‌پردازد.

الگوی URL موضوع‌ها

ابتدا باید URLای برای صفحه‌ی موضوعات تعیین کنیم. انتخاب انشعابی از URL که بیانگر نوع اطّلاعات نشان‌داده شده در صفحه است معمول است.

ما از واژه‌ی topics استفاده می‌کنیم پس URL http://localhost:8000/topics/ این صفحه را باز خواهد گرداند. تغییرات learning_logs/urls.py به شکل زیر است:

1
2
3
4
5
6
7
8
9
"""Defines URL patterns for learning_logs."""
--snip--

urlpatterns = [
    # Home page.
    path('', views.index, name='index'),
    # Page that shows all topics.
    path('topics/', views.topics, name='topics'),
]

topics/ را به سادگی به نشان‌وند رشته‌ای استفاده‌شده در URL صفحه‌ی خانه افزودیم. هنگامی که جانگو URL درخواستی را بررسی می‌کند هر آدرسی که از URL پایه به همراه topics تشکیل شده باشد با این الگو تطبیق خواهد داشت. گذاشتن اسلش در انتها اختیاری است امّا اگر هر چیزی در ادامه topics اضافه کنیم الگو را به هم می‌ریزد. هر درخواستی که URL آن با این الگو مطابق باشد به تابع topics() در view.py فرستاده می‌شود.

نمای موضوعات

تابع topics() باید تعدادی داده را از پایگاه داده بگیرد و آن را به قالب بفرستد. این چیزی است که باید به views.py اضافه کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from django.shortcuts import render
from .models import Topic

def index(request):
    --snip--

def topics(request):
    """Show all topics."""
    topics = Topic.objects.order_by('date_added')
    context = {'topics': topics}
    return render(request, 'learning_logs/topics.html', context)

ابتدا مدل مربوط به داده‌های مورد نیاز خود را وارد می‌کنیم. تابع topics() یک پارامتر می‌خواهد: شیء requestای که جانگو از سرور دریافت کرده است. در پایگاه داده را برای یافتن شیءهای Topic می‌گردیم و نتایج را بر اساس مشخّصه‌ی date_added مرتّب می‌کنیم. مجموعه‌ی نتایج را در topics ذخیره می‌کنیم.

بستری برای ارسال اطّلاعات به قالب تعریف می‌کنیم. بستر یک د164 یکشنری است که کلیدهای آن نام‌هایی است که در قالب برای دسترسی به داده‌ها استفاده می‌کنیم و مقدارهای آن داده‌هایی است که قرار است به قالب بفرستیم. در این مورد یک جفت کلید-مقدار وجود دارد که شامل مجموعه‌ای موضوعاتی است که در صفحه نمایش می‌دهیم. هنگام ساختن صفحه‌ای که از داده‌ها استفاده می‌کند، متغیّر context، شیء request و مسیر قالب را به render() می‌دهیم.

قالب موضوعات

قالب صفحه‌ی موضوعات دیکشنری context را دریافت می‌کند بنابراین این قالب قادر است که از داده‌های ارائه‌شده‌ی topics() بهره بگیرد. فایلی به نام topics.html در همان پوشه‌ی index.html بسازید. نمایش موضوعات در قالب به این شکل است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{% extends "learning_logs/base.html" %}

{% block content %}
  <p>Topics</p>
  <ul>
    {% for topic in topics %}
       <li>{{ topic }}</li>
    {% empty %}
      <li>No topics have been added yet.</li>
    {% endfor %}
  </ul>
{% endblock content %}

درست مانند قالب index، از تگ {% extends %} برای ارث‌بری از base.html استفاده می‌کنیم و سپس یک ب166 165 لوک content باز می‌کنیم. بدنه‌ی این صفحه شامل یک فهرست نشانه‌دار از موضوعاتی است که وارد شده است. در HTML استاندارد فهرست نشانه‌دار فهرست بدون ترتیب نامیده شده و با تگ‌های <ul></ul> نشان داده می‌شود.

از یک تگ قالب دیگر که معادل حلقه‌ی for است استفاده می‌کنیم که در لیست topics موجود در دیکشنری context حلقه می‌زند. کد به‌کاررفته در قالب‌ها از چند منظر مهم با پایتون متفاوت است. پایتون برای مشخّص کردن خط‌های یک عبارت for به عنوان جزئی از حلقه از تورفتگی استفاده می‌کند. در یک قالب هر حلقه‌ی for صریحاً به یک تگ {% endfor %} برای نشان‌دادن نقطه‌ی پایان حلقه نیاز دارد. پس در یک قالب حلقه‌ها را به این شکل خواهید دید:

{% for item in list %}
do something with each item
{% endfor %}

می‌خواهیم که داخل حلقه هر موضوع را به یک مورد در فهرست نشانه‌دار تبدیل کنیم. برای چاپ کردن یک متغیّر در یک قالب نام متغیّر را داخل دو آکُلاد قرار می‌دهیم. پس کد {{ topic }} در هر اجرای حلقه با مقدار topic جایگزین می‌شود. تگ HTML <li></li> یک مورد فهرست را نشان می‌دهد. هر چیزی که در این تگ‌ها، و در کل داخل یک جفت <ul></ul>، به صورت موردی نشانه‌دار نمایش داده می‌شود.

از تگ قالب {% empty %} استفاده می‌کنیم تا به جانگو بگوییم در صورت خالی بودن فهرست چه کار کند. در این مورد پیامی چاپ می‌کنیم که به کاربر می‌گوید هنوز هیچ موضوعی اضافه نشده است.

اکنون باید قالب پایه را تغییر دهیم و پیوندی به صفحه‌ی موضوعات به آن اضافه کنیم. کد زیر را به base.html بیافزایید:

1
2
3
4
5
6
<p>
  <a href="{% url 'learning_logs:index' %}">Learning Log</a> -
  <a href="{% url 'learning_logs:topics' %}">Topics</a>
</p>

{% block content %}{% endblock content %}

پس از پیوند صفحه‌ی خانه یک خطّ فاصله اضافه می‌کنیم و سپس به کمک تگ قالب {% url %} پیوندی به صفحه‌ی موضوعات اضافه می‌کنیم. این خط به جانگو می‌گوید که پیوندی مطابق با الگوی URL به نام ’topics’ در learning_logs/urls.py ایجاد کن.

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

2.7.8 - صفحه‌های جداگانه‌ی هر موضوع

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

الگوی URL موضوعات

الگوی URL برای صفحه‌ی موضوع با الگوهای قبلی کمی متفاوت است چون از مشخّصه‌ی id موضوع برای تعیین موضوع درخواستی استفاده می‌کند. برای مثال اگر کاربر بخواهد صفحه‌ی جزئیات موضوع شطرنج را ببیند، که id آن ۱ است، URL آن به صورت http://localhost:8000/topics/1/ خواهد بود.

این یک الگو برای تطبیق دادن این URL است که باید آن را در learning_logs/urls.py قرار دهید:

1
2
3
4
5
6
7
--snip--

urlpatterns = [
  --snip--
  # Detail page for a single topic.
  path('topics/<int:topic_id>/', views.topic, name='topic'),
]

اجازه دهید رشته‌ی 'topics/<int:topic_id>/' را در این الگوی URL بررسی کنیم. اوّلین بخش آن به جانگو می‌گوید به دنبال URLهایی بگرد که پس از URL پایه کلمه‌ی topics را داشته باشد. بخش دوم رشته، / <int:topic_id>/ عددی را که بین دو اسلش قرار می‌گیرد دریافت کرده و مقدار عددی آن را در نشان‌وندی به نام topic_id ذخیره می‌کند.

زمانی که جانگو URLای مطابق با این الگو پیدا می‌کند تابع نمای topic() را فرامی‌خواند و مقدار ذخیره‌شده‌ی topic_id را به صورت یک نشان‌وند به آن می‌دهد. داخل تابع از مقدار topic_id برای دریافت صحیح موضوع استفاده می‌کنیم.

نمای موضوع

تابع topic() باید موضوع و تمام مطالب مرتبط با آن را از پایگاه داده بگیرد، مانند زیر:

1
2
3
4
5
6
7
8
--snip--

def topic(request, topic_id):
    """Show a single topic and all its entries."""
    topic = Topic.objects.get(id=topic_id)
    entries = topic.entry_set.order_by('-date_added')
    context = {'topic': topic, 'entries': entries}
    return render(request, 'learning_logs/topic.html', context)

این اوّلین تابع نمایی است که به جز شیء request یک پارامتر هم می‌خواهد. این تابع مقداری را که از عبارت <int:topic_id> به دست می‌آید گرفته و در topic_id ذخیره می‌کند. درست مانند کاری که در پوسته‌ی جانگو کردیم، از get() برای دریافت موضوع استفاده می‌کنیم. موضوعات مرتبط با این موضوع را می‌گیریم و بر اساس date_added مرتّب می‌کنیم. علامت منفی جلوی date_added باعث مرتّب‌سازی معکوس می‌شود که موضوعات جدیدتر را ابتدا نشان می‌دهد. موضوع و مطالب را در دیکشنری context ذخیره کرده و آن را به قالب topic.html ارسال می‌کنیم.

نکته
عبارت‌های کد در get() و order_by() پرس و جو نامیده می‌شوند زیرا از پایگاه داده در مورد اطّلاعات خاصّی پرس و جو می167 ‌کنند. زمانی که چنین عبارت‌هایی را در پروژه‌های خود می‌نویسید بهتر است ابتدا آن‌ها را در پوسته‌ی جانگو امتحان کنید. بازخوردی که در پوسته می‌گیرید بسیار سریع‌تر از آن است که یک نما و قالب بنویسید و سپس نتایج را در مرورگر بررسی کنید.

قالب موضوع

قالب باید نام موضوع و مطالب مرتبط را نشان دهد. همچنین اگر هیچ موضوعی در این رابطه نوشته نشده باید به کاربر اطّلاع دهیم.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{% extends 'learning_logs/base.html' %}

{% block content %}
  <p>Topic: {{ topic }}</p>
  <p>Entries:</p>
  <ul>
  {% for entry in entries %}
    <li>
       <p>{{ entry.date_added|date:'M d, Y H:i' }}</p>
       <p>{{ entry.text|linebreaks }}</p>
    </li>
  {% empty %}
    <li>There are no entries for this topic yet.</li>
  {% endfor %}
  </ul>
{% endblock content %}

اینجا هم مانند تمام صفحه‌های این پروژه base.html را گسترش داده‌ایم. سپس موضوعی را نشان می‌دهیم که اکنون در حال نمایش است و در متغیّر قالب {{ topic }} ذخیره می‌شود. متغیّر topic در دسترس است چون در دیکشنری context گنجانده شده است. سپس فهرستی بدون ترتیب برای نمایش هر مطلب ایجاد می‌کنیم و مانند آن‌چه برای موضوعات کردیم، داخل آن حلقه می‌زنیم.

هر مورد نشانه‌دار دو جزء اطّلاعات را در برمی‌گیرد: برچسب زمان و متن کامل هر مطلب. برای برچسب زمانی مقدار مشخّصه‌ی date_added را نشان می‌دهیم. در قالب‌های جانگو خط عمودی (|) بیانگر فیلتر قالب است—تابعی که مقدار یک متغیّر قالب را تغییر می‌دهد. فیلتر date:'M d, Y H:i' برچسب زمان به شکل January 1, 2018 23:00 نشان می‌دهد. خط بعدی مقدار کامل text را به جای تنها ۵۰ حرف اوّل entry نمایش می‌دهد. فیلتر linebreaks اطمینان می‌دهد که در متن‌های طولانی شکست خط‌ها به شکلی که برای مرورگرها قابل درک باشند شامل می‌شوند و کل متن به صورت یک نوشته‌ی بدون وقفه نمایش داده نمی‌شود. اگر هیچ مطلبی نوشته نشده باشد، از تگ قالب {% empty %} برای چاپ پیامی خطاب به کاربر استفاده می‌کنیم.

پیوندهای ایجاد شده از صفحه‌ی موضوعات

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

1
2
3
4
5
6
7
8
--snip--

{% for topic in topics %}
  <li>
    <a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a>
  </li>
{% empty %}
--snip--

از تگ قالب URL و بر اساس الگوی URL با نام ’topic’ در learning_logs برای ساختن پیوند مناسب استفاده می‌کنیم. این الگوی URL یک نشان‌وند topic_id می‌خواهد بنابراین مشخّصه‌ی topic.id را به تگ قالب URL اضافه می‌کنیم. اکنون هر موضوع در فهرست موضوعات پیوندی به صفحه‌ی آن موضوع است، مانند http://localhost:8000/topics/1/.

اگر صفحه‌ی موضوعات را به‌روزآوری کنید و روی موضوعی کلیک کنید باید صفحه‌ای مانند شکل ۱۸-۵ ببینید.

نکته
یک تفاوت ظریف امّا مهم بین topic.id و topic_id وجود دارد. عبارت topic.id یک موضوع را بررسی و مقدار ID متناظر با آن را می‌گیرد. متغیّر topic_id ارجاعی به آن ID در کد است. اگر هنگام کار کردن با IDها به خطا برخوردید مطمئن شوید که اینگونه عبارت‌ها را به درستی استفاده می‌کنید.

خودآزمایی

۱۸-۷: مستندات قالب‌سازی: مستندات مربوط به ساختن قالب‌ها را در https://docs.djangoproject.com/en/2.2/ref/templates مرور کنید. می‌توانید هنگامی که بر روی پروژه‌های خود کار می‌کنید به آن دوباره مراجعه کنید.

۱۸-۸: صفحه‌های پیتزافروشی: صفحه‌ای به پروژه‌ی پیتزافروشی در تمرین ۱۸-۶ اضافه کنید که نام پیتزاهای موجود را نمایش دهد. سپس نام هر پیتزا را به صفحه‌ای که مخلّفات آن را نشان می‌دهد پیوند دهید. مطمئن شوید که برای ساختن موثّر صفحه‌های خود از ارث‌بری قالب استفاده می‌کنید.

2.7.9 - خلاصه

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

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

در فصل ۱۹ صفحه‌های کاربرپسندی می‌سازید که به کاربران اجازه‌ی افزودن موضوعات و مطالب و ویرایش مطالب موجود را بدون استفاده از سایت مدیر می‌دهد. همچنین سیستم ثبت نام را اضافه خواهید کرد که به کاربران اجازه می‌دهد یک حساب کاربری برای خود بسازند و دفترچه‌ی آموخته‌های خود را ایجاد کنند. این قلب یک برنامه‌ی کاربری تحت وب است—امکان ساختن چیزی که هر تعداد کاربر بتواند با آن تعامل کند.


2.8 - فصل نوزدهم

2.8.1 - فراهم آوردن امکان ورود داده برای کاربران

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

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

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

در حال حاضر تنها ابرکاربر می‌تواند داده‌ها را از طریق سایت مدیر وارد کند. هدف ما این است که کاربران با سایت مدیریّت تعاملی نداشته باشند پس از ابزارهای فرم‌سازی جانگو کمک می‌گیریم تا به کاربران اجازه‌ی وارد کردن داده‌ها را بدهیم.

2.8.2 - افزودن موضوع‌های جدید

بیایید کار را برای فراهم آوردن امکان افزودن یک موضوع جدید شروع کنیم. افزودن صفحه‌های مبتنی بر فرم از بسیاری جهات شبیه به صفحه‌هایی کار می‌کنند که پیش از این ساخته‌ایم: یک URL تعیین می‌کنیم، یک تابع نما و یک قالب می‌نویسیم. تنها تفاوت اساسی اضافه کردن ماژول جدید forms.py است که فرم‌ها را در بر می‌گیرد.

مدل فرم موضوع

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

در جانگو ساده‌ترین راه برای ساختن یک فرم استفاده ا169 ز فرمِ مدل است که به کمک داده‌ی مدل‌هایی که در فصل ۱۸ام تعریف کرده‌ایم به طور خودکار فرمی را تولید می‌کند. اوّلین فرم خود را در فایل forms.py بسازید که در همان پوشه‌ی حاوی فایل models.py قرار می‌گیرد:

1
2
3
4
5
6
7
8
9
from django import forms

from .models import Topic

class TopicForm(forms.ModelForm):
    class Meta:
        model = Topic
        fields = ['text']
        labels = {'text': ''}

ابتدا ماژول forms و مدل مورد نظر خود، به نام Topic، را وارد می‌کنیم. در ۱ کلاسی به نام TopicForm تعریف می‌کنیم که از forms.ModelForm ارث می‌برد.

ساده‌ترین نسخه‌ی ModelForm از یک کلاس داخلی به نام Meta تشکیل شده است که به جانگو می‌گوید فرم را بر اساس کدام مدل بنا کند و کدام فیلدها را در آن بیاورد. در ۲ فرمی از مدل Topic می‌سازیم و تنها فیلد text را در آن می‌آوریم (۳). کد در ۴ به جانگو می‌گوید که برای فیلد text برچسبی تولید نکند.

آدرس new_topic

URLای که برای یک صفحه‌ی جدید باید کوتاه و گویا باشد. هرگاه کاربر بخواهد موضوع جدیدی اضافه کند باید او را به http://localhost:8000/new_topic/ بفرستیم. الگوی URL برای صفحه‌ی new_topic به صورت زیر است که آن را به learning_logs/urls.py اضافه می‌کنیم:

1
2
3
4
5
6
7
--snip--

urlpatterns = [
    --snip--
    # Page for adding a new topic
    path('new_topic/', views.new_topic, name='new_topic'),
]

این الگو درخواست‌ها را به تابع نمای new_topic() می‌نویسیم که در ادامه آن را می‌نویسیم.

تابع نمای new_topic()

تابع new_topic() باید دو وضعیّت مختلف را مدیریّت کند: یکی درخواست‌های اوّلیّه‌ی صفحه‌ی new_topic (که در آن‌ها باید یک فرم خالی نمایش دهد) و دیگری پردازش هر داده‌ای که در فرم ثبت می‌شود. پس از این که داده‌های ثبت‌شده پردازش شد، باید کاربر به صفحه‌ی topics برگردانده شود:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django.shortcuts import render, redirect

from .models import Topic
from .forms import TopicForm

--snip--

def new_topic(request):
    """Add a new topic."""
    if request.method != 'POST':
        # No data submitted; create a blank form.
        form = TopicForm()
    else:
        # POST data submitted; process data.
        form = TopicForm(data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topics')

    # Display a blank or invalid form.
    context = {'form': form}
    return render(request, 'learning_logs/new_topic.html', context)

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

درخواست‌های GET و POST

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

تابع new_topic() شیء درخواست را به عنوان پارامتر می‌پذیرد. وقتی کاربران در ابتدا این صفحه را درخواست می‌کنند مرورگر آن‌ها یک درخواست GET خواهد فرستاد. زمانی که کاربران فرم را پر کرده و ثبت می‌کنند مرورگر آن‌ها یک درخواست POST ثبت می‌کند. بسته به نوع درخواست ما خواهیم فهمید که کاربران درخواست فرم خالی دارند (یک درخواست GET) یا از ما می‌خواهند که فرم تکمیل‌شده را پردازش کنیم (یک درخواست POST).

آزمونی که در ۱ نوشتیم مشخّص می‌کند که درخواست از نوع GET است یا POST. اگر متد درخواست POST نباشد پس به احتمال زیاد GET خواهد بود پس باید یک فرم خالی برگردانیم (اگر هم نوع درخواست چیز دیگری جز این دو باشد، برگرداندن فرم خالی به لحاظ امنیّتی مشکلی ندارد.) یک نمونه از TopicForm می‌سازیم (۲) و آن را به متغیّر form نسبت می‌دهیم و فرم را در قالب دیکشنری context به قالب می‌فرستیم (۷). چون هنگام نمونه‌سازی TopicForm هیچ نشان‌وندی تعیین نکرده‌ایم جانگو یک فرم خالی می‌سازد که کاربر می‌تواند آن را پر کند.

اگر متد درخواست POST باشد بخش else اجرا می‌شود و داده‌های ثبت‌شده در فرم را پردازش می‌کند. نمونه‌ای از TopicForm می‌سازیم (۳) و داده‌هایی را که کاربر وارد کرده و در request.POST ذخیره شده، به آن می‌دهیم. شیء form که برگردانده می‌شود حاوی اطّلاعاتی است که کاربر ثبت کرده است.

تا زمانی که اطّلاعات ثبت‌شده اعتبارسنجی نشده باشد آن‌ها را در پایگاه داده ذخیره نخواهیم کرد (۴). متد is_valid() بررسی می‌کند که آیا تمام فیلدهای اجباری پر شده‌اند یا نه (تمام فیلدها به صورت پیش‌فرض اجباری‌اند) و آیا داده‌های وارد شده با نوع داده‌ی مورد انتظار انطباق دارند یا نه—برای مثال آیا چنان‌که در models.py در فصل ۱۸ام تعیین کردیم، طول text کمتر از ۲۰۰ حرف است یا نه. اگر همه چیز معتبر باشد save() را فرامی‌خوانیم (۵) که داده‌های دریافتی از فرم را در پایگاه داده می‌نویسد.

به محض ذخیره‌سازی داده‌ها در پایگاه داده می‌توانیم صفحه را ترک کنیم. از redirect() برای بازگرداندن مرورگر کاربر به صفحه‌ی topics استفاده می‌کنیم که کاربر باید بتواند در آن فهرستی موضوعاتی را که همان لحظه وارد کرده ببیند.

متغیّر context در انتهای تابع نما تعریف می‌شود و صفحه به کمک قالب new_topic.html ساخته می‌شود که در ادامه آن را ایجاد می‌کنیم. این کد خارج از تمام عبارت‌های if قرار می‌گیرد؛ زمانی اجرا می‌شود که یک فرم خالی ایجاد شده یا اطّلاعات ثبت‌شده در فرم معتبر نیستند. فرم نامعتبر چند پیام خطای پیش‌فرض نیز دربرمی‌گیرد تا به کاربر در ثبت داده‌های قابل قبول کمک کند.

قالب new_topic

بیایید قالبی جدید به نام new_topic.html بسازیم تا فرم ساخته‌شده را نمایش دهیم.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{% extends "learning_logs/base.html" %}

{% block content %}
  <p>Add a new topic:</p>

  <form action="{% url 'learning_logs:new_topic' %}" method='post'>
    {% csrf_token %}
    {{ form.as_p }}
    <button name="submit">Add topic</button>
  </form>

{% endblock content %}

این قالب base.html را گسترش می‌دهد پس ساختار آن مشابه دیگر صفحه‌های Learning Log خواهد بود. در ۱ یک فرم HTML تعریف می‌کنیم. نشان‌وند action به مرورگر نشانی ارسال اطّلاعات ثبت‌شده در فرم را به مرورگر می‌دهد؛ در این مورد آن را به تابع نمای new_topic() می‌فرستیم. نشان‌وند method به مرورگر می‌گوید که داده‌ها را به صورت یک درخواست POST ثبت کند.

جانگو به کمک تگ قالب {% csrf_token %} مانع دسترسی غیرمجاز نفوذگران به سرور از طریق فرم می‌شود.170 (این نوع حمله را سرقت درخواست بین سایتی می‌نامند.) در ۳ فرم را نمایش می‌دهیم؛ اینجا می‌توانید ببینید که جانگو انجام برخی از کارها مانند نمایش یک فرم را چه‌قدر ساده می‌کند. کافی است متغیّر قالب {{ form.as_p }} را اضافه کنیم تا جانگو تمام فیلدهای مورد نیاز برای نمایش خودکار فرم بسازد. عبارت as_p به جانگو می‌گوید که تمام عنصرهای فرم را به عنوان یک روش ساده و منظّم برای نمایش فرم، به شکل پاراگراف دربیاورد.

جانگو دکمه‌ی ثبت را در فرم‌ها ایجاد نمی‌کند پس در ۴ یکی تعریف می‌کنیم.

ایجاد پیوند به صفحه‌ی new_topic

در ادامه پیوندی را به صفحه‌ی new_topic در صفحه‌ی topics ایجاد می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{% extends "learning_logs/base.html" %}

{% block content %}
  <p>Topics</p>
  <ul>
    --snip--
  </ul>

  <a href="{% url 'learning_logs:new_topic' %}">Add a new topic</a>

{% endblock content %}

پیوند را پس از فهرست موضوعات موجود قرار دهید. شکل ۱۹-۱ فرم حاصل را نمایش می‌دهد. از این فرم برای افزودن چند موضوع استفاده کنید.

![][image47]

شکل ۱۹-۱: صفحه‌ی اضافه کردن موضوع جدید

2.8.3 - افزودن مطالب جدید

همانگونه که کاربران می‌توانند موضوع جدیدی اضافه کنند، باید امکان افزودن مطالب جدید هم وجود داشته باشد. باز هم یک URL تعریف می‌کنیم، یک تابع نما و قالب می‌نویسیم و پیوندی به صفحه ایجاد می‌کنیم. امّا اوّل اجازه دهید کلاسی به forms.py اضافه کنیم.

فرم مدل موضوع

ما باید فرمی در ارتباط با مدل Entry بسازیم امّا این بار نسبت به TopicForm کمی بیشتر آن را شخصی می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from django import forms

from .models import Topic, Entry

class TopicForm(forms.ModelForm):
    --snip--

class EntryForm(forms.ModelForm):
    class Meta:
        model = Entry
        fields = ['text']
        labels = {'text': 'Entry:'}
        widgets = {'text': forms.Textarea(attrs={'cols': 80})}

عبارت import را به‌روز می‌کنیم تا Entry را هم مانند Topic وارد کنیم. کلاس جدیدی به نام EntryForm می‌سازیم که از forms.ModelForm ارث می‌برد. کلاس EntryForm کلاسی داخلی به نام Meta دارد که مدلی را که بر آن بنا شده و فیلدهایی را که باید در فرم بیاورد فهرست می‌کند. دوباره به فیلد text یک برچسب خالی می‌دهیم (۱).

در ۲ مشخّصه‌ی widgets را اضافه می‌کنیم. ویجت یا ابزاره نوعی عنصر فرم HTML اس171 ت، مانند یک ورودی متن یک‌خطّی، ورودی متنی چندخطّی یا یک فهرست پایین‌افتادنی. با اضافه کردن مشخّصه‌ی widgets می‌توانید ویجت‌های انتخابی پیش‌فرض جانگو را تغییر دهید. وقتی به جانگو می‌گوییم که از یک forms.Textarea استفاده کند ویجت ورودی فیلد text را شخصی می‌کنیم تا عرض آن به جای ۴۰ ستونِ پیش‌فرض، به اندازه‌ی ۸۰ ستون باشد. این کار فضایی مناسب برای نوشتن یک مطلب معنادار ایجاد می‌کند.

آدرس new_entry

موضوعات جدید باید در رابطه با موضوع خاصّی باشند پس نیاز داریم که نشان‌وند topic_id را در URL مربوط به اضافه کردن مطلب جدید بیاوریم. این URLایست که آن را به learning_logs/urls.py اضافه می‌کنیم:

1
2
3
4
5
6
7
--snip--

urlpatterns = [
    --snip--
    # Page for adding a new entry
    path('new_entry/<int:topic_id>/', views.new_entry, name='new_entry'),
]

هر آدرسی به شکل http://localhost:8000/new_entry/id/ با این الگوی URL مطابق خواهد بود که در آن id شماره‌ای برابر با ID موضوع خواهد بود. کد <int:topic_id> مقداری عددی را استخراج و آن را به متغیّر topic_id نسبت می‌دهد. زمانی که آدرسی با الگوی فوق درخواست شد جانگو آن را به همراه شماره‌ی موضوع به تابع نمای new_entry() می‌فرستد.

تابع نمای new_entry()

تابع نمای new_entry بسیار شبیه به تابع اضافه کردن موضوع جدید است. این کد را به فایل views.py اضافه کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from django.shortcuts import render, redirect

from .models import Topic
from .forms import TopicForm, EntryForm

--snip--

def new_entry(request, topic_id):
    """Add a new entry for a particular topic."""
    topic = Topic.objects.get(id=topic_id)

    if request.method != 'POST':
        # No data submitted; create a blank form.
        form = EntryForm()
    else:
        # POST data submitted; process data.
        form = EntryForm(data=request.POST)
        if form.is_valid():
            new_entry = form.save(commit=False)
            new_entry.topic = topic
            new_entry.save()
            return redirect('learning_logs:topic', topic_id=topic_id)

    # Display a blank or invalid form.
    context = {'topic': topic, 'form': form}
    return render(request, 'learning_logs/new_entry.html', context)

عبارت import را برای اضافه کردن EntryForm تغییر می‌دهیم. در تعریف new_entry() یک پارامتر topic_id وجود دارد که مقدار دریافت شده از URL در آن ذخیره می‌شود. موضوع مربوطه را برای ساختن صفحه و پردازش داده‌های فرم نیاز دارید پس از در ۱ از topic_id استفاده می‌کنیم تا شیء موضوع درست را دریافت کنیم.

در ۲ بررسی می‌کنیم که آیا متد درخواست GET است یا POST. اگر GET باشد کد داخل if اجرا می‌شود و نمونه‌ای خالی از EntryForm می‌سازد (۳).

اگر نوع درخواست POST باشد با ساختن یک نمونه از EntryForm و انتقال داده‌های POST داخل شیء request به آن، داده‌ها را پردازش می‌کنیم (۴). سپس اعتبار فرم را بررسی می‌کنیم. اگر معتبر بود باید پیش از ذخیره کردن شیء در پایگاه داده مشخّصه‌ی topic آن را تعیین کنیم. زمانی که save() را صدا می‌کنیم نشان‌وند commit=False را به آن می‌دهیم (۵) تا جانگو بداند باید برای موضوع جدید شیء جدیدی ایجاد کند و آن را به new_entry نسبت دهد امّا فعلاً آن را در پایگاه داده ذخیره نکند. مقدار مشخّصه‌ی new_entry را برابر با موضوعی قرار می‌دهیم که در ابتدای تابع از پایگاه داده خواندیم (۶). سپس save() را بدون هیچ نشان‌وندی فرامی‌خوانیم تا موضوع را به همراه موضوع مرتبط با آن در بانک اطّلاعاتی ذخیره کنیم.

فراخوانی redirect() در ۷ به دو نشان‌وند نیاز دارد—نام نمایی که می‌خواهیم به آن بازگردیم و نشان‌وندی که آن تابع نما نیاز دارد. اینجا به topic() برمی‌گردیم پس به نشان‌وند topic_id نیاز داریم. این نما صفحه‌ی موضوعی را می‌سازد که کاربران در رابطه با آن موضوعی ایجاد کرده‌اند و آن‌ها باید بتوانند موضوع جدید خود را در فهرست موضوعات ببینند.

در پایان تابع یک دیکشنری context ایجاد می‌کنیم و صفحه را به کمک قالب new_entry.html ایجاد می‌کنیم. این کد زمانی اجرا خواهد شد که فرم خالی درخواست شده باشد یا فرم ثبت‌شده معتبر نباشد.

قالب new_entry

چنانکه در کد زیر می‌بینید قالب new_entry شبیه به قالب new_topic است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{% extends "learning_logs/base.html" %}

{% block content %}
  <p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></p>
  <p>Add a new entry:</p>
  <form action="{% url 'learning_logs:new_entry' topic.id %}" method='post'>
    {% csrf_token %}
    {{ form.as_p }}
    <button name='submit'>Add entry</button>
  </form>

{% endblock content %}

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

نشان‌وند action فرم مقدار topic_id داخل URL را هم دربرمی‌گیرد تا تابع نما بتواند بین مطلب جدید و موضوع مرتبط با آن ارتباط برقرار کند (۱). به جز این قالب کاملاً شبیه به new_topic.html است.

ایجاد پیوند به صفحه‌ی new_entry

در گام بعدی باید از هر صفحه‌ی موضوع در قالب مربوط به موضوعات پیوندی به صفحه‌ی new_entry ایجاد کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{% extends "learning_logs/base.html" %}

{% block content %}
  <p>Topic: {{ topic }}</p>
  <p>Entries:</p>
  <p>
    <a href="{% url 'learning_logs:new_entry' topic.id %}">Add new entry</a>
  </p>
  <ul>
  --snip--
  </ul>

{% endblock content %}

پیوندِ اضافه کردن موضوعات جدید را درست پیش از نمایش موضوعات قرار می‌دهیم زیرا در این صفحه افزودن مطلب جدید متداول‌ترین عمل است. شکل ۱۹-۲ صفحه‌ی new_entry را نشان می‌دهد. اکنون کاربران می‌توانند هر تعداد موضوع که خواستند اضافه کنند و برای هر موضوع نیز هر اندازه مطلب که دوست دارند بنویسند. صفحه‌ی new_entry را با افزودن چند مطلب به برخی از موضوعاتی که ایجاد کرده‌اند امتحان کنید.

![][image48]

شکل ۱۹-۲: صفحه‌ی new_entry

2.8.4 - ویرایش مطلب‌ها

اکنون صفحه‌ای می‌سازیم تا کاربران بتوانند از طریق آن موضوعات خود را ویرایش کنند.

آدرس edit_entry

URL صفحه باید شماره‌ی مطلب مورد ویرایش را انتقال دهد. این learning_logs/urls.py است:

1
2
3
4
5
6
7
--snip--

urlpatterns = [
    --snip--
    # Page for editing an entry.
    path('edit_entry/<int:entry_id>/', views.edit_entry, name='edit_entry'),
]

شماره‌ی انتقال‌یافته از طریق URL (برای مثال http://localhost:8000/edit_entry/1/) در پارامتر entry_id ذخیره می‌شود. این الگوی URL درخواست‌های منطبق با خود را به تابع نمای edit_entry() می‌فرستد.

تابع نمای edit_entry()

زمانی که صفحه‌ی edit_entry یک درخواست GET دریافت می‌کند تابع edit_entry() فرمی برای ویرایش مطلب برمی‌گرداند. وقتی یک درخواست POST به همراه متن ویرایش‌شده بگیرد آن را در پایگاه داده ذخیره می‌کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from django.shortcuts import render, redirect

from .models import Topic, Entry
from .forms import TopicForm, EntryForm

--snip--

def edit_entry(request, entry_id):
    """Edit an existing entry."""
    entry = Entry.objects.get(id=entry_id)
    topic = entry.topic

    if request.method != 'POST':
        # Initial request; pre-fill form with the current entry.
        form = EntryForm(instance=entry)
    else:
        # POST data submitted; process data.
        form = EntryForm(instance=entry, data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('learning_logs:topic', topic_id=topic.id)

    context = {'entry': entry, 'topic': topic, 'form': form}
    return render(request, 'learning_logs/edit_entry.html', context)

ابتدا مدل Entry را وارد می‌کنیم. در ۱ شیء موضوعی که کاربر می‌خواهد ویرایش کند و موضوع مرتبط با آن را دریافت می‌کنیم. در داخل if که برای درخواست‌های GET اجرا می‌شود نمونه‌ای از EntryForm با نشان‌وند instance=entry می‌سازیم (۲). این نشان‌وند به جانگو می‌گوید که فرمی بسازد که با اطّلاعات شیء فعلی موضوع از پیش پر شده باشد. کاربران قادر هستند که داده‌های موجود را ببینند و آن را اصلاح کنند.

برای پردازش درخواست POST نشان‌وندهای instance=entry و data=request.POST را انتقال می‌دهیم (۳). این نشان‌وندها به جانگو می‌گویند که یک نمونه‌ی فرم بر اساس اطّلاعات مربوط به شیء مطلب فعلی بسازد و آن را با هر داده‌ای مرتبطی از request.POST به‌روز کند. سپس اعتبار فرم را بررسی می‌کنیم؛ اگر چنین بود save() را بدون هیچ نشان‌وندی فرامی‌خوانیم چون موضوع از قبل با موضوع درست مرتبط شده است (۴). سپس کاربر را به صفحه‌ی topic برمی‌گردانیم که کاربران در آن می‌توانند نسخه‌ی به‌روزشده‌ی مطلبی را که ویراسته‌اند ببینند (۵).

اگر یک فرم ابتدایی برای ویرایش مطلب نشان دهیم یا فرم ثبت‌شده نامعتبر باشد یک دیکشنری context درست می‌کنیم و صفحه را به کمک قالب edit_entry.html می‌سازیم.

قالب edit_entry

در ادامه قالب edit_entry.html را می‌سازیم که مشابه new_entry.html است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{% extends "learning_logs/base.html" %}

{% block content %}
  <p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></p>
  <p>Edit entry:</p>
  <form action="{% url 'learning_logs:edit_entry' entry.id %}" method='post'>
    {% csrf_token %}
    {{ form.as_p }}
    <button name="submit">Save changes</button>
  </form>

{% endblock content %}

نشان‌وند action در ۱ فرم را برای پردازش به تابع edit_entry() می‌فرستد. شماره‌ی مطلب را به عنوان نشان‌وندی در تگ {% url %} قرار می‌دهیم تا تابع نما بتواند شیء موضوع درست را تغییر دهد. عنوان دکمه‌ی ثبت را Save changes می‌گذاریم که به کاربر گوشزد کند در حال ویرایش مطلب است نه ایجاد مطلب جدید (۲).

ایجاد پیوند به صفحه‌ی edit_entry

باید به هر مطلبی در صفحه‌ی موضوعات پیوندی به صفحه‌ی edit_entry ایجاد کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
--snip--

  {% for entry in entries %}
    <li>
      <p>{{ entry.date_added|date:'M d, Y H:i' }}</p>
      <p>{{ entry.text|linebreaks }}</p>
      <p>
        <a href="{% url 'learning_logs:edit_entry' entry.id %}">Edit entry</a>
      </p>
    </li>

--snip--

پس از این که تاریخ و متن هر موضوع نمایش داده شد پیوند ویرایش را قرار می‌دهیم. از تگ قالب {% url %} و مشخّصه‌ی ID مطلب کنونی در حلقه (entry.id)، برای تعیین URL برای الگوی URL با نام edit_entry استفاده می‌کنیم. متن Edit Entry پیوند پس از هر مطلب در صفحه ظاهر می‌شود. شکل ۱۹-۳ نشان می‌دهد که صفحه‌ی موضوعات با این پیوندها چگونه به نظر می‌رسد.

![][image49]

شکل ۱۹-۳: اکنون هر مطلب پیوندی برای ویرایش دارد.

Learning Log اکنون بیشتر قابلیّت‌های مورد نیاز را دارد. کاربران می‌توانند موضوع‌ها و مطلب‌ها را اضافه کنند و هر مجموعه از مطالب را که بخواهند بخوانند. در بخش بعدی یک سیستم ثبت نام برای کاربران پیاده می‌کنیم تا هر کس بتواند در سایت حساب بسازد و موضوع‌ها و مطالب خود را ایجاد کند.

2.8.5 - خودآزمایی

۱۹-۱- بلاگ: پروژه‌ی جانگوی جدیدی به نام Blog راه بیندازید. برنامه‌ای به نام blogs در پروژه ایجاد کند و مدلی به نام BlogPost بسازید. مدل باید فیلدهایی مانند title، text و date_added داشته باشد. ابرکاربری برای پروژه بسازید و از طریق سایت مدیر چند نوشته‌ی کوتاه اضافه کنید. صفحه‌ی خانه‌ای بسازید که تمام نوشته‌ها را به ترتیب زمانی نشان می‌دهد.

یک فرم برای نوشتن مطالب جدید و یکی دیگر برای ویرایش نوشته‌های موجود بسازید. فرم‌های خود را پر کنید تا مطمئن شوید که کار می‌کنند.

2.8.6 - برپاکردن حساب‌های کاربری

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

برنامه users

کار را با ساختن برنامه‌ای به نام users به کمک فرمان startapp شروع می‌کنیم:

1
2
3
4
5
6
7
(ll_env)learning_log$ python manage.py startapp users

(ll_env)learning_log$ ls
db.sqlite3 learning_log learning_logs ll_env manage.py users

(ll_env)learning_log$ ls users
__init__.py admin.py apps.py migrations models.py tests.py views.py

این دستور پوشه‌ای جدید به نام users می‌سازد (۱) که ساختاری مشابه با برنامه‌ی learning_logs دارد (۲).

افزودن کاربران به settings.py

باید برنامه‌ی جدید خود را به INSTALLED_APPS در settings.py اضافه کنیم؛ مانند زیر:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--snip--

INSTALLED_APPS = [
    # My apps
    'learning_logs',
    'users',
    # Default django apps.
    --snip--
]

--snip--

جانگو اکنون برنامه‌ی users را به پروژه‌ی کلی اضافه می‌کند.

افزودن URLهای مربوط به users

حال فایل اصلی urls.py را تغییر می‌دهیم تا URLهایی را که برای برنامه‌ی users ایجاد می‌کنیم در بر بگیرد:

1
2
3
4
5
6
7
8
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('users/', include('users.urls')),
    path('', include('learning_logs.urls')),
]

خطّی را برای افزودن فایل urls.py از پوشه‌ی users اضافه می‌کنیم. این خط هر URLای را که با کلمه‌ی users شروع می‌شود تشخیص می‌دهد، مانند http://localhost:8000/users/login/.

2.8.7 - صفحه‌ی ورود

ما ابتدا صفحه‌ی ورود را پیاده‌سازی می‌کنیم. چون از نمای پیش‌فرض login که جانگو فراهم کرده استفاده می‌کنیم، الگوی URL این برنامه کمی متفاوت به نظر می‌آید. یک فایل urls.py جدید در پوشه‌ی learning_log/users/ بسازید و این‌ها را به آن اضافه کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
"""Defines URL patterns for users"""

from django.urls import path, include

app_name = 'users'

urlpatterns = [
    # Include default auth urls.
    path('', include('django.contrib.auth.urls')),
]

تابع path و سپس تابع include را وارد می‌کنیم تا برخی از URLهای پیش‌فرض احراز هویّت تعریف‌شده در جانگو را اضافه کنیم. این URLهای پیش‌فرض شامل چند الگوی اسم‌دار URL مانند 'login' و 'logout'اند. مقدار متغیّر app_name را 'users' می‌گذاریم تا جانگو بتواند بین این URLها و URLهای دیگر برنامه تفاوت قایل شود (۱). حتّی این URLهای پیش‌فرض جانگو هم اگر در فایل urls.py مربوط به برنامه‌ی users آورده شوند، از طریق فضای نام users در دسترس خواهند بود.

الگوی صفحه‌ی ورود به چنین URLای می‌خورد: http://localhost:8000/users/login (۲). زمانی که جانگو این URL را می‌خواند واژه‌ی users به آن می‌گوید که داخل users/urls.py را نگاه کن و login مشخّص می‌کند که درخواست‌ها را به نمای پیش‌فرض login جانگو بفرست.

قالب login

زمانی که کاربر صفحه‌ی ورود را درخواست می‌کند جانگو از یک تابع نمای پیش‌فرض استفاده می‌کند امّا ما باید همچنان قالبی برای این صفحه ارائه کنیم. نماهای پیش‌فرض احراز هویّت به دنبال قالب‌ها در پوشه‌ای به نام registration می‌گردند بنابراین باید آن پوشه را ایجاد کنیم. داخل پوشه‌ی learning_logs/users/ پوشه‌ای به نام templates بسازید و داخل آن پوشه‌ی دیگری به نام registration ایجاد کنید. این قالب login.html است و باید آن را در learning_logs/users/templates/registration ذخیره کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{% extends "learning_logs/base.html" %}

{% block content %}
  {% if form.errors %}
    <p>Your username and password didn't match. Please try again.</p>
  {% endif %}

  <form method="post" action="{% url 'users:login' %}">
    {% csrf_token %}
    {{ form.as_p }}
    <button name="submit">Log in</button>
    <input type="hidden" name="next"
      value="{% url 'learning_logs:index' %}" />
  </form>

{% endblock content %}

این قالب base.html را گسترش می‌دهد تا مطمئن باشیم که شکل و قیافه‌ی آن شبیه بقیه‌ی صفحات سایت است. توجّه کنید که قالب یک برنامه می‌تواند از قالب برنامه‌ی دیگری ارث ببرد.

اگر مشخّصه‌ی errors فرم مقدار داشته باشد در ۱ پیام خطایی نمایش می‌دهیم که بیان می‌کند نام کاربری و گذرواژه‌ی واردشده با هیچ یک از اطّلاعات موجود در پایگاه داده تطابق ندارد.

چون می‌خواهیم نمای ورود فرم را پردازش کند، نشان‌وند action را برابر با URL صفحه‌ی ورود قرار می‌دهیم (۲). نمای ورود فرمی را به قالب می‌فرستد و نمایش فرم (۳) و افزودن دکمه‌ی ثبت (۴) بر عهده‌ی ماست. در ۵ یک عنصر فرم مخفی اضافه می‌کنیم، 'next'؛ نشان‌وند value به جانگو می‌گوید که پس از ورود موفّق کاربر، او را به کدام صفحه هدایت کند. در این مورد کاربران را به صفحه‌ی خانه برمی‌گردانیم.

2.8.8 - پیوند دادن به صفحه‌ی ورود

حال پیوند ورود به base.html اضافه می‌کنیم تا در هر صفحه نمایش داده شود. چون نمی‌خواهیم وقتی کاربر وارد سیستم شده بود این پیوند را ببیند، آن را داخل یک تگ {% if %} می‌گذاریم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<p>
  <a href="{% url 'learning_logs:index' %}">Learning Log</a> -
  <a href="{% url 'learning_logs:topics' %}">Topics</a> -
  {% if user.is_authenticated %}
    Hello, {{ user.username }}.
  {% else %}
    <a href="{% url 'users:login' %}">Log in</a>
  {% endif %}
</p>

{% block content %}{% endblock content %}

در سیستم احراز هویّت جانگو در هر قالب یک متغیّر user وجود دارد که همیشه مشخّصه‌ی is_authenticated آن مقدار دارد: اگر کاربر وارد شده باشد True وگرنه False است. این مشخّصه به شما کمک می‌کند که به کاربران احرازِ هویّت‌شده یک پیام و به دیگر کاربران پیام دیگری نشان دهید.

اینجا به کاربران واردشده یک پیام خوش‌آمدگویی نشان می‌دهیم (۱). کاربران احراز هویّت‌شده یک مشخّصه‌ی username هم دارند که می‌توانیم از آن برای شخصی کردن پیام استفاده کنیم و به کاربر خاطرنشان کنیم که وارد شده است (۲). در ۳ پیوندی در اختیار کاربرانی که هنوز وارد نشده‌اند قرار می‌دهیم تا به صفحه‌ی ورود بروند.

استفاده از صفحه‌ی ورود

ما پیش از این یک حساب کاربری ساخته‌ایم پس بیایید با آن وارد شویم تا ببینیم آیا صفحه به درستی کار می‌کند یا نه. به آدرس http://localhost:8000/admin/ بروید. اگر همچنان به عنوان یک مدیر در سیستم حضور دارید به دنبال پیوند خروج بگردید و روی آن کلیک کنید.

اکنون که از سیستم خارج شده‌اید به http://localhost:8000/users/login/ بروید. باید صفحه‌ای مانند صفحه‌ی ورود نشان‌داده‌شده در شکل ۱۹-۴ ببینید. نام کاربری و گذرواژه‌ای که قبلاً ساخته‌اید وارد کنید. باید به صفحه‌ی اوّل برگردانده شوید. عنوان صفحه‌ی خانه باید پیامی خوش‌آمدی با نام کاربری شما نمایش دهد.

![][image50]

شکل ۱۹-۴: صفحه‌ی ورود

2.8.9 - خروج

باید راهی برای خروج از سایت در اختیار کاربران بگذاریم. پیوندی در base.html قرار دهیم که کاربران را از سیستم خارج می‌کند؛ زمانی که کاربران روی آن کلیک می‌کنند به صفحه‌ای خواهند رفت که تایید می‌کند آن‌ها از سیستم خارج شده‌اند.

افزودن یک پیوند خروج به base.html

پیوند خروج را به base.html اضافه می‌کنیم تا در هر صفحه وجود داشته باشد. آن را داخل {% if user.is_authenticated %} قرار می‌دهیم تا تنها کاربرانی که قبلاً وارد شده‌اند بتوانند آن را ببینند:

1
2
3
4
5
6
7
--snip--

  {% if user.is_authenticated %}
    Hello, {{ user.username }}.
    <a href="{% url 'users:logout' %}">Log out</a>
  {% else %}
    --snip--

الگوی URL اسم‌دار پیش‌فرض برای خروج 'logout' است.

صفحه‌ی تایید خروج

کاربران می‌خواهند بدانند که آیا خروج آن‌ها موفّق بوده یا نه، در نتیجه نمای پیش‌فرض خروج صفحه را به کمک قالب logged_out.html تولید می‌کند که آن را اکنون می‌سازیم. اینجا صفحه‌ی ساده‌ای می‌سازیم که نشان می‌دهد کاربر خارج شده است. این فایل را در templates/registration، همان‌جایی که login.html را قرار دادید، ذخیره کنید:

1
2
3
4
5
6
{% extends "learning_logs/base.html" %}

{% block content %}
  <p>You have been logged out. Thank you for visiting!</p>

{% endblock content %}

هیچ چیز دیگری در این صفحه نمی‌خواهیم زیرا base.html پیوندی بازگشت به صفحه‌ی خانه یا صفحه‌ی ورود را در اختیار می‌گذارد و کاربران می‌توانند به هرکدام که خواستند بروند.

شکل ۱۹-۵ صفحه‌ی خارج‌شده را نشان می‌دهد که به کاربری نشان داده می‌شود که روی پیوند Log out کلیک کرده است. شکل و شیوه‌ی صفحه بسیار مختصر است زیرا فعلاً روی ساختن سایتی که به درستی کار کند تمرکز کرده‌ایم. زمانی که مجموعه‌ی ویژگی‌های مورد نیاز ما به درستی پیاده شد شکل سایت را تغییر می‌دهیم تا حرفه‌ای‌تر به نظر بیاید.

![][image51]

شکل ۱۹-۵: صفحه‌ی خارج‌شده تایید می‌کند که کاربر با موفقیّت خارج شده است.

2.8.10 - صفحه‌ی ثبت نام

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

URL ثبت نام

کد زیر الگوی URL صفحه‌ی ثبت نام را فراهم می‌آورد که دوباره در users/urls.py قرار می‌گیرد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
"""Defines URL patterns for users"""

from django.urls import path, include

from . import views

app_name = 'users'

urlpatterns = [
    # Include default auth urls.
    path('', include('django.contrib.auth.urls')),
    # Registration page.
    path('register/', views.register, name='register'),
]

ماژول views را از users وارد می‌کنیم. آن را برای نوشتن نمای صفحه‌ی ثبت نام نیاز داریم. الگوی صفحه‌ی ثبت نام URLای مانند http://localhost:8000/users/register/ را می‌پذیرد و درخواست‌ها را به تابع register() که الان خواهیم نوشت، می‌فرستیم.

تابع نمای register()

تابع نمای register() باید وقتی صفحه‌ی ثبت نام برای اوّلین بار درخواست می‌شود، یک فرم خالی ثبت نام نمایش دهد و پس از تکمیل و ثبت آن اطّلاعات آن را پردازش کند. اگر ثبت نام موفّقیّت‌آمیز بود این تابع باید همچنین کاربر جدید را وارد کند. کد زیر را به users/views.py اضافه کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django.shortcuts import render, redirect

from django.contrib.auth import login
from django.contrib.auth.forms import UserCreationForm

def register(request):
    """Register a new user."""
    if request.method != 'POST':
        # Display blank registration form.
        form = UserCreationForm()
    else:
        # Process completed form.
        form = UserCreationForm(data=request.POST)
        if form.is_valid():
            new_user = form.save()
            # Log the user in and then redirect to home page.
            login(request, new_user)
            return redirect('learning_logs:index')

    # Display a blank or invalid form.
    context = {'form': form}
    return render(request, 'registration/register.html', context)

تابع‌های render() و redirect() را وارد می‌کنیم. سپس تابع login() را وارد می‌کنیم تا در صورت ثبت نام موفّق کاربر او را وارد سایت کنیم. ما همچنین UserCreationForm پیش‌فرض را وارد می‌کنیم. در تابع register() بررسی می‌کنیم که آیا به یک درخواست POST پاسخ می‌دهیم یا نه. اگر چنین نباشد نمونه‌ای از UserCreationForm را بدون هیچ داده‌ی اوّلیّه‌ای می‌سازیم (۱).

اگر به درخواست POST پاسخ می‌دادیم یک نمونه از UserCreationForm را بر اساس داده‌های ثبت‌شده ایجاد می‌کنیم (۲). بررسی می‌کنیم که آیا داده‌ها معتبرند یا نه (۳)—در این مورد، آیا نام کاربری شامل حرف‌های مناسب است، دو گذرواژه با یکدیگر برابرند و این که کاربر هنگام ثبت داده‌های خود هیچ خرابکاری‌ای انجام ندهد.

اگر داده‌های ثبت‌شده معتبر باشند متد save() فرم را فرامی‌خوانیم تا نام کاربری و معادل درهم‌ریخته‌ی گذرواژه را در پایگاه داده ذخیره می‌کنیم (۴). متد save() شیء کاربر جدیدی را که ساخته‌ایم برمی‌گرداند و ما آن را به new_user نسبت می‌دهیم. زمانی که اطّلاعات کاربران ذخیره شد آن‌ها را به صدا کردن تابع login() و دادن شیءهای request و new_user به سایت وارد می‌کنیم (۵) که یک نشست معتبر برای کاربر جدید ایجاد می‌کند. در نهایت کاربر را به صفحه‌ی خانه برمی‌گردانیم (۶) که در آن یک پیام شخصی در عنوان دیده می‌شود که به کاربران می‌گوید ثبت نام آن‌ها موفق بوده است.

در پایان تابع صفحه را می‌سازیم که می‌تواند یک فرم خالی یا فرم ثبت‌شده با اطّلاعات نامعتبر باشد.

قالب register

اکنون قالبی برای صفحه‌ی ثبت نام می‌سازیم که شبیه به صفحه‌ی ورود خواهد بود. حتماً آن را در همان پوشه‌ی حاوی login.html ذخیره کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{% extends "learning_logs/base.html" %}

{% block content %}
  <form method="post" action="{% url 'users:register' %}">
    {% csrf_token %}
    {{ form.as_p }}
    <button name="submit">Register</button>
    <input type="hidden" name="next" value="{% url 'learning_logs:index' %}" />
  </form>

{% endblock content %}

دوباره از متد as_p استفاده می‌کنیم تا جانگو تمام فیلدهای فرم را به همراه پیام‌های خطای مربوط به درست پر نشدن آن، به درستی نمایش دهد.

پیوند دادن به صفحه‌ی ثبت نام

در ادامه کدِ نمایش پیوند صفحه‌ی ثبت نام را برای کاربرانی که هنوز وارد نشده‌اند اضافه می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--snip--

  {% if user.is_authenticated %}
    Hello, {{ user.username }}.
    <a href="{% url 'users:logout' %}">Log out</a>
  {% else %}
    <a href="{% url 'users:register' %}">Register</a> -
    <a href="{% url 'users:login' %}">Log in</a>
  {% endif %}

--snip--

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

در بخش بعدی برخی از صفحه‌ها را محدود می‌کنیم تا تنها در دسترس کاربرانی باشند که ثبت نام کرده‌اند و مطمئن خواهیم شد که هر موضوع به کاربر خاصّی تعلّق دارد.

نکته
سیستم ثبت نامی که ما برپا کرده‌ایم به هر کس اجازه می‌دهد که هر تعداد حساب کاربری که بخواهد بسازد. امّا برخی سیستم‌ها از کاربران می‌خواند که هویّت خود را باید پاسخ دادن به یک ایمیل تایید کنند. انجام چنین کاری باعث می‌شود که سیستم نسبت به سیستم ساده‌ی ما، تعداد کمتری حساب کاربری هرز ایجاد کند. با این حال زمانی که ساختن برنامه‌ها را می‌آموزید تمرین کردن با یک سیستم ثبت نام ساده مثل این بسیار عالی خواهد بود.

2.8.11 - خودآزمایی

۱۹-۲: حساب‌های کاربری بلاگ: یک سیستم احراز هویّت و ثبت نام برای کاربران پروژه‌ی Blog که در تمرین ۱۹-۱ شروع کرده‌اید، بسازید. مطمئن شوید که کاربرانِ واردشده نام کاربری خود را در جایی از صفحه می‌بینند و کاربران ثبت نام نشده پیوندی به صفحه‌ی ثبت نام می‌بینند.

2.8.12 - فراهم آوردن امکان در اختیار گرفتن داده‌ها برای کاربران

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

مدل Topic را تغییر می‌دهیم تا هر موضوع به یک کاربر خاص تعلّق داشته باشد. چنین کاری مطالب را هم مدیریّت خواهد کرد زیرا هر مطلب به یک موضوع خاص مرتبط است. کار را به محدود کردن دسترسی به صفحات خاص آغاز می‌کنیم.

محدود کردن دسترسی با @login_required

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

محدود کردن دسترسی به صفحه‌ی موضوعات

هر موضوع به یک کاربر تعلّق دارد پس تنها کاربرانی که ثبت نام کرده‌اند می‌توانند صفحه‌ی موضوعات را درخواست کنند. کد زیر را به learning_logs/views.py اضافه کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from django.shortcuts import render, redirect

from django.contrib.auth.decorators import login_required

from .models import Topic, Entry

--snip--

@login_required
def topics(request):
    """Show all topics."""
    --snip--

ابتدا تابع login_required() را وارد می‌کنیم. با افزودن یک علامت @ به ابتدای تابع login_required()، آن را به عنوان یک شکل‌دهنده به تابع نمای topics() اضافه می‌کنیم. در نتیجه پایتون می‌فهمد که کد داخل login_required() را پیش از کد topics() اجرا کند.

کد داخل login_required() بررسی می‌کند که آیا کاربری وارد شده است یا نه و جانگو تنها در صورتی کد topics() را اجرا می‌کند که کاربری وارد شده باشد. اگر کاربر وارد نشده باشد، به صفحه‌ی ورود هدایت خواهد شد.

برای این که بازگردانی کاربر درست کار کند باید settings.py را طوری تغییر دهیم که جانگو بداند صفحه‌ی ورود کجاست. خط زیر را به آخر settings.py اضافه کنید:

1
2
3
4
--snip--

# My settings
LOGIN_URL = 'users:login'

حال اگر کاربری که هویّت او احراز نشده صفحه‌ای را درخواست کند که با @login_required محافظت شده جانگو او را به آدرسی می‌فرستد که در LOGIN_URL در فایل settings.py تعیین شده است.

این تنظیمات را می‌توانید با خارج شدن از یکی از حساب‌های کاربری و رفتن به صفحه‌ی خانه امتحان کنید. روی پیوند Topics کلیک کنید که باید شما را به صفحه‌ی ورود رهنمون کند. سپس با یکی از حساب‌های کاربری وارد شوید و در صفحه‌ی خانه دوباره روی پیوند Topics کلیک کنید. باید به صفحه‌ی موضوعات دسترسی داشته باشید.

ایجاد محدودیّت در سراسر سایت

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

در Learning Log صفحه‌ی خانه و صفحه‌ی ثبت نام را محدود نمی‌کنیم. دسترسی به دیگر صفحه‌ها محدود به کاربران خواهد بود.

این learning_logs/views.py است که در آن شکل‌دهنده‌ی @login_required به تمام نماها به جز index() اعمال می‌شود:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
--snip--

@login_required
def topics(request):
    --snip--

@login_required
def topic(request, topic_id):
    --snip--

@login_required
def new_topic(request):
    --snip--

@login_required
def new_entry(request, topic_id):
    --snip--

@login_required
def edit_entry(request, entry_id):
    --snip--

سعی کنید پس از خروج از سایت به تمام صفحه‌های آن دسترسی پیدا کنید: شما به صفحه‌ی ورود برگردانده می‌شوید. نمی‌توانید روی پیوندها به صفحه‌هایی مانند new_topic کلیک کنید. امّا اگر آدرس https://localhost:8000/new_topic/ را وارد کنید به صفحه‌ی ورود هدایت می‌شوید. دسترسی شما به هر آدرسی که به طور عمومی در دسترس است امّا حاوی داده‌های خصوصی کاربران است، باید محدود باشد.

2.8.13 - ایجاد ارتباط بین داده‌ها و کاربران

در ادامه باید بین داده‌ها و کاربری که آن را ثبت کرده ارتباط برقرار کنیم. کافی است تنها داده‌ها در بالاترین سطح سلسله‌مراتب را به یک کاربر متّصل کنیم و داده‌های دیگر در سطح‌های پایین‌تر به دنبال آن خواهند آمد. برای مثال در Learning Log موضوع‌ها بالاترین سطح اطّلاعات در برنامه‌اند و تمام مطلب‌ها به یک موضوع وابسته‌اند. مادامی‌که هر موضوع به یک کاربر خاص تعلّق داشته باشد می‌توانیم مالکیّت هر مطلب را در پایگاه داده ردگیری کنیم.

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

اصلاح مدل Topic

تغییرات models.py تنها در دو خط انجام می‌پذیرد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from django.db import models

from django.contrib.auth.models import User

class Topic(models.Model):
    """A topic the user is learning about."""
    text = models.CharField(max_length=200)
    date_added = models.DateTimeField(auto_now_add=True)
    owner = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        """Return a string representation of the model."""
        return self.text

class Entry(models.Model):
    --snip--

مدل User را از django.contrib.auth وارد می‌کنیم. سپس فیلدی به نام owner به Topic اضافه می‌کنیم که یک ارتباط کلید خارجی به مدل User برقرار می‌کند. اگر کاربری حذف شود تمام موضوع‌های مرتبط با آن نیز پاک خواهد شد.

2.8.14 - انتقال پایگاه داده

اکنون که می‌خواهیم پایگاه داده را منتقل کنیم، جانگو آن را تغییر می‌دهد تا بتواند بین هر موضوع و یک کاربر ارتباطی برقرار کند. برای انجام انتقال جانگو باید بداند موضوعاتی را که قبلاً ایجاد شده‌اند به کدام کاربر ربط دهد. ساده‌ترین رویکرد این است که برای شروع تمام موضوع‌های موجود را به یک کاربر نسبت دهیم—مثلاً ابرکاربر. امّا ابتدا باید شماره‌ی آن کاربر را بدانیم.

بیایید به شماره‌ی تمام کاربرانی که تا الان ساخته‌ایم نگاهی بیندازیم. یک نشست پوسته‌ی جانگو اجرا کنید و دستورهای زیر را وارد کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
(ll_env)learning_log$ python manage.py shell

>>> from django.contrib.auth.models import User

>>> User.objects.all()

<QuerySet [<User: ll_admin>, <User: eric>, <User: willie>]>

>>> for user in User.objects.all():
...     print(user.username, user.id)
...

ll_admin 1
eric 2
willie 3

>>>

در ۱ مدل User را داخل نشست پوسته وارد می‌کنیم. سپس به تمام کاربرانی که تا کنون ساخته‌ایم نگاهی می‌اندازیم (۲). خروجی سه کاربر را نشان می‌دهد: ll_admin، eric و willie.

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

انتقال پایگاه داده

حال که شماره‌ی کاربران را می‌دانیم می‌توانیم پایگاه داده را انتقال دهیم. زمانی که می‌خواهیم این کار را انجام دهیم پایتون دو گزینه پیش روی ما می‌گذارد: یا مدل Topic را موقّتاً به یکی از کاربران نسبت دهیم یا خودمان در فایل models.py یک حالت پیش‌فرض تعیین کنیم تا پایتون بر اساس آن عمل کند. گزینه‌ی ۱ را انتخاب کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
(ll_env)learning_log$ python manage.py makemigrations learning_logs

You are trying to add a non-nullable field 'owner' to topic without a default;
we can't do that (the database needs something to populate existing rows).

Please select a fix:

1) Provide a one-off default now (will be set on all existing rows with a
   null value for this column)

2) Quit, and let me add a default in models.py

Select an option: 1

Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do
e.g. timezone.now

Type 'exit' to exit this prompt

>>> 1

Migrations for 'learning_logs':
  learning_logs/migrations/0003_topic_owner.py
- Add field owner to topic

(ll_env)learning_log$

کار را با صدور فرمان makemigrations در ۱ آغاز می‌کنیم. در خروجی ۲، جانگو اعلام می‌کند که در حال افزودن یک فیلد اجباری (non-nullable) به مدل موجود (topic) هستیم و هیچ مقدار پیش‌فرضی تعیین نشده است. جانگو به ما دو گزینه می‌دهد (۳): می‌توانیم همین الان یک مقدار پیش‌فرض اعلام کنیم یا خارج شویم و مقدار پیش‌فرضی در models.py تعیین کنیم. در ۴ گزینه‌ی یکم را انتخاب کرده‌ایم. جانگو سپس مقدار پیش‌فرض را از ما می‌پرسد (۵).

برای ربط دادن به کاربر مدیر اوّلیّه، ll_admin، شماره‌ی کاربر ۱ را وارد کردم (۶). می‌توانید شماره‌ی هر کاربری را وارد کنید؛ حتماً نباید ابرکاربر باشد. سپس جانگو به کمک این مقدار پایگاه داده را منتقل کرده و فایل 0003_topic_owner.py را می‌سازد که فیلد owner را به مدل Topic اضافه می‌کند.

حال می‌توانیم عملیّات انتقال را اجرایی کنیم. این دستورها را در یک محیط مجازی فعّال وارد کنید:

1
2
3
4
5
6
7
8
9
(ll_env)learning_log$ python manage.py migrate

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, learning_logs, sessions

Running migrations:
  Applying learning_logs.0003_topic_owner... OK

(ll_env)learning_log$

جانگو انتقال جدید را اعمال می‌کند و نتیجه OK است (۱).

می‌توانیم در یک نشست پوسته بررسی کنیم که آیا انتقال مطابق انتظار ما انجام شده یا نه:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> from learning_logs.models import Topic

>>> for topic in Topic.objects.all():
...           print(topic, topic.owner)
...

Chess ll_admin
Rock Climbing ll_admin

>>>

Topic را از learning_logs.models وارد می‌کنیم و سپس در تمام موضوعات موجود حلقه می‌زنیم تا هر موضوع و کاربر صاحب آن را چاپ کنیم (۲). می‌توانید ببینید که اکنون تمام موضوعات به کاربر ll_admin تعلّق دارند. (اگر هنگام اجرای این کد به مشکل خوردید یک بار پوسته را ببندید و دوباره باز کنید.)

نکته
شما می‌توانید به جای جابه‌جایی پایگاه داده، به سادگی آن را بازنشانی کنید امّا تمام اطّلاعات موجود در آن را از دست خواهید داد. آموختن نحوه‌ی جابه‌جایی یک پایگاه‌داده با حفظ یکپارچگی داده‌های کاربران تمرین خوبی است. اگر می‌خواهید یک پایگاه داده‌ی تازه راه بیندازید فرمان python manage.py flush را برای بازسازی ساختار پایگاه داده صادر کنید. شما باید یک ابرکاربر جدید بسازید و تمام داده‌هایتان از دست خواهد رفت.

2.8.15 - محدود کردن موضوعات به کاربران مربوطه

هم‌اکنون اگر وارد سایت شده باشید قادر خواهید بود که صرف نظر از این که چه کسی هستید، تمام موضوع‌ها را ببینید. این وضعیّت را به گونه‌ای تغییر می‌دهیم که کاربران تنها موضوعاتی را ببینند که به آن‌ها تعلّق دارد.

تغییر زیر را در تابع topics() در views.py اعمال کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--snip--

@login_required
def topics(request):
    """Show all topics."""
    topics = Topic.objects.filter(owner=request.user).order_by('date_added')
    context = {'topics': topics}
    return render(request, 'learning_logs/topics.html', context)

--snip--

زمانی که کاربری وارد می‌شود، شیء درخواست مشخّصه‌ای به نام request.user دارد که اطّلاعات آن کاربر را ذخیره می‌کند. پرس‌وجوی topics = Topic.objects.filter(owner=request.user).order_by('date_added') به جانگو می‌گوید که تنها شیءهای Topicای را از پایگاه داده استخراج کن که مشخّصه‌ی owner آن با کاربر فعلی مطابقت داشته باشد. چون نحوه‌ی نمایش موضوع‌ها را تغییر نداده‌ایم نیازی نیست که قالب صفحه‌ی موضوعات را دست بزنیم.

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

2.8.16 - محافظت از موضوع‌های یک کاربر

ما هنوز دسترسی به صفحه‌های موضوعات را محدود نکرده‌ایم در نتیجه هر کاربری که ثبت نام کرده باشد می‌تواند تعدادی آدرس به شکل http://localhost:8000/topics/1/ را امتحان کند تا بالاخره به یکی از صفحه‌ها با شماره‌ی موجود دست یابد.

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

این مشکل با انجام یک بررسی پیش از دریافت مطالب درخواستی در تابع نمای topic() برطرف می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from django.shortcuts import render, redirect

from django.contrib.auth.decorators import login_required
from django.http import Http404

--snip--

@login_required
def topic(request, topic_id):
    """Show a single topic and all its entries."""
    topic = Topic.objects.get(id=topic_id)
    # Make sure the topic belongs to the current user.
    if topic.owner != request.user:
        raise Http404

    entries = topic.entry_set.order_by('-date_added')
    context = {'topic': topic, 'entries': entries}
    return render(request, 'learning_logs/topic.html', context)

--snip--

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

اکنون اگر تلاش کنید که مطالب مربوط به موضوع کاربری دیگری را ببینید پیام Page Not Found را از سوی جانگو خواهید دید. در فصل ۲۰ پروژه را جوری تنظیم می‌کنیم که یک صفحه‌ی خطای مناسب نمایش داده شود.

محافظت از صفحه‌ی edit_entry

صفحه‌های edit_entry آدرسی به شکل http://localhost:8000/edit_entry/entry_id/ هستند که entry_id یک عدد است. بیایید از این صفحه محافظت کنیم تا هیچ‌کس نتواند از این آدرس برای دست یافتن به مطالب کس دیگری استفاده کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
--snip--

@login_required
def edit_entry(request, entry_id):
    """Edit an existing entry."""
    entry = Entry.objects.get(id=entry_id)
    topic = entry.topic
    if topic.owner != request.user:
        raise Http404

    if request.method != 'POST':
        --snip--

مطلب و موضوع مربوط به آن را دریافت می‌کنیم. سپس بررسی می‌کنیم که آیا صاحب این موضوع کاربری است که الان وارد شده یا نه؛ اگر چنین نبود استثنای Http404 را اعلام می‌کنیم.

2.8.17 - نسبت دادن موضوع‌های جدید به کاربر فعلی

در حال حاضر صفحه‌ی مربوط به اضافه کردن موضوع‌های جدید خراب است زیرا موضوع‌های جدید را به هیچ کاربر خاصّی نسبت نمی‌دهد. اگر تلاش کنید که موضوع جدیدی ایجاد کنید با پیام خطای IntegrityError به همراه NOT NULL constraint failed: learning_logs_topic.owner_id برمی‌خورید. جانگو می‌گوید که شما نمی‌توانید بدون تعیین کردن مقداری برای فیلد owner موضوع جدیدی بسازید.

راه حل این مشکل بسیار سرراست است زیرا از طریق شیء request به کاربر فعلی دسترسی داریم. کد زیر را اضافه کنید که هر موضوع جدید را به کاربر فعلی نسبت می‌دهد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
--snip--

@login_required
def new_topic(request):
    """Add a new topic."""
    if request.method != 'POST':
        # No data submitted; create a blank form.
        form = TopicForm()
    else:
        # POST data submitted; process data.
        form = TopicForm(data=request.POST)
        if form.is_valid():
            new_topic = form.save(commit=False)
            new_topic.owner = request.user
            new_topic.save()
            return redirect('learning_logs:topics')

    # Display a blank or invalid form.
    context = {'form': form}
    return render(request, 'learning_logs/new_topic.html', context)

--snip--

ابتدا form.save() صدا می‌کنیم و نشان‌وند commit=False را به آن می‌دهیم زیرا پیش از ذخیره کردن موضوع جدید در پایگاه داده باید آن را تغییر دهیم (۱). سپس مشخّصه‌ی owner موضوع جدید را برابر با کاربر فعلی می‌گذاریم (۲). در نهایت save() را صدا می‌کنیم و نمونه‌ی موضوعی که تعریف کرده‌ایم به آن می‌دهیم (۳). حال موضوع تمام داده‌های مورد نیاز را دارد و با موفقّیت ذخیره خواهد شد.

شما باید بتوانید هر تعداد موضوع جدید را برای هر تعداد کاربر که بخواهید ایجاد کنید. هر کاربر تنها به داده‌های خود دسترسی خواهد داشت؛ چه بخواهد داده‌ها را ببیند، چه داده‌ی جدیدی وارد کند یا داده‌های قدیمی را ویرایش کند.

2.8.18 - خودآزمایی

۱۹-۳- بازآرایی: در views.py دو جا وجود دارد که می‌خواهیم مطمئن شویم کاربری مالک یک موضوع با کاربری فعلی تطابق دارد یا نه. کد این بررسی را در تابعی به نام check_topic_owner() قرار دهید و آن را در جای مناسب صدا کنید.

۱۹-۴- محافظت از new_entry: در حال حاضر یک کاربر می‌تواند با وارد کردن یک URL با شماره‌ی موضوع متعلّق به کاربر دیگری، یک موضوع جدید به دفترچه‌ی آموخته‌های او اضافه کند. با بررسی مرتبط بودن کاربر فعلی با موضوع مطلب پیش از ذخیره‌ی مطلب جدید، از این دستکاری جلوگیری کنید.

۱۹-۵- بلاگ محافظت‌شده: در پروژه‌ی Blog خود مطمئن شوید که هر پست وبلاگ به یک کاربر خاص اختصاص دارد. اطمینان حاصل کنید که تمام پست‌های در دسترس عموم‌اند امّا تنها کاربران ثبت نام کرده می‌توانند پست‌های جدیدی اضافه یا قبلی‌ها را ویرایش کنند. در نمایی که به کاربران اجازه‌ی ویرایش پست‌هایشان را می‌دهد پیش از پردازش فرم مطمئن شوید که کاربران در حال ویرایش پست خود هستند.

2.8.19 - خلاصه

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

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

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

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

اکنون شما یک پروژه‌ی کاملاً کارآمد دارید که روی ماشین محلی‌تان اجرا می‌شود. در فصل پایانی شکل و شیوه‌ی Learning Log را تغییر می‌دهید تا زیبا به نظر بیاید و پروژه را روی یک سرور قرار می‌دهید تا هر کس که به اینترنت دسترسی دارد بتواند ثبت نام کند و یک حساب کاربری بسازد.


2.9 - فصل بیستم

2.9.1 - شکل‌دهی و استقرار

سایت Learning Log اکنون کاملاً کارآمد است امّا هیچ شکل و سیاقی ندارد و روی ماشین محلی شما اجرا می‌شود. در این فصل پروژه را به شکلی ساده امّا حرفه‌ای درمی‌آوریم و سپس آن را روی سرور در حال اجرا مستقر می‌کنیم تا هر کس در هر جای دنیا بتواند حساب کاربری بسازد و از آن استفاده کند.

برای شکل‌دهی به سایت از کتابخانه‌ی Bootstrap استفاده می‌کنیم که مجموعه‌ای از ابزارها برای سر و شکل دادن به برنامه‌های کاربردی تحت وب است تا آن‌ها در افزاره‌های مدرن حرفه‌ای به نظر برسند، از نمایشگرهای تخت بزرگ گرفته تا گوشی‌های هوشمند. برای چنین کاری از برنامه‌ی django-bootstrap4 کمک می‌گیریم تا علاوه بر هدف اصلی خود، در کار کردن با برنامه‌هایی که دیگر توسعه‌دهندگان جانگو ساخته‌اند تجربه کسب کنید.

برای استقرار پروژه از Heroku استفاده می‌کنیم، سایتی که به شما امکان می‌دهد که پروژه‌ی خود را روی یکی از سرورهای آن قرار دهید تا هر کس با اتّصال اینترنت به آن دسترسی داشته باشد. افزون بر این کار با یک سیستم کنترل نسخه به نام Git برای ردگیری تغییرات پروژه شروع می‌کنیم.

زمانی که پروژه‌ی Learning Log را به پایان برسانید قادر خواهید بود که برنامه‌های کاربردی ساده‌ی تحت وب را توسعه دهید، آن‌ها را به شکل و قیافه‌ی مناسبی درآورید و آن‌ها را روی یک سرور زنده مستقر کنید. علاوه بر این‌ها می‌توانید از منابع آموزشی پیشرفته‌تر استفاده کنید تا مهارت‌های خود را گسترش دهید.

2.9.2 - شکل دادن به پروژه

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

در این بخش برنامه‌ی django-bootstrap4 را معرّفی می‌کنم و به شما نشان می‌دهم که چگونه می‌توان آن را در یک پروژه تجمیع کرد تا برای استقرار آماده شود.

برنامه django-bootstrap4

برای مجتمع کردن بوت‌استرپ در پروژه‌ی خود از django-bootstrap4 استفاده می‌کنیم. این برنامه فایل‌های مورد نیاز بوت‌استرپ را دریافت کرده و آن‌ها را در مکان مناسب داخل پروژه‌ی شما قرار می‌دهد و استفاده از راهنماهای شیوه‌دهی را در قالب‌های پروژه ممکن می‌سازد.

برای نصب django-bootstrap4 فرمان زیر را در یک محیط مجازی فعّال صادر کنید:

1
2
3
(ll_env)learning_log$ pip install django-bootstrap4
--snip--
Successfully installed django-bootstrap4-0.0.7

سپس باید کد زیر را به settings.py اضافه کنید تا django-bootstrap4 در INSTALLED_APPS قرار بگیرد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--snip--
INSTALLED_APPS = [
    # My apps.
    'learning_logs',
    'users',
    # Third party apps.
    'bootstrap4',
    # Default django apps.
    'django.contrib.admin',
--snip--

بخش جدیدی به نام Third party apps (برنامه‌های شخص ثالث) برای برنامه‌هایی ایجاد کنید که دیگر توسعه‌دهندگان ساخته‌اند و 'bootstrap4' را به آن بیفزایید. مطمئن شوید که این بخش را پس از # My apps و پیش از بخشی که برنامه‌های پیش‌فرض جانگو را در برمی‌گیرد قرار دهید.

استفاده از بوت‌استرپ برای شکل دادن به Learning Log

بوت‌استرپ مجموعه‌ای بزرگ از ابزارهای شکل‌دهی است.173 همچنین تعدادی قالب دارد که می‌توانید آن‌ها را به پروژه‌ی خود اعمال کنید تا شکلی کلّی پیدا کند. استفاده کردن از این قالب‌ها به مراتب ساده‌تر از به‌کارگیری ابزارهای شکل‌دهی متفرقّه است. برای دیدن قالب‌هایی که بوت‌استرپ عرضه می‌کند به https://getbootstrap.com/ بروید، روی Examples کلیک کنید و به دنبال بخش Navbars بگردید. ما از قالب نوار پیمایش ایستا (Navbar Static) استفاده می‌کنیم که یک نوار پیمایش ساده در بالا و یک دربردارنده برای مطالب سایت ارائه می‌کند.

شکل ۲۰-۱ صفحه‌ی خانه را پس از اعمال قالب بوت‌استرپ به base.html و تغییر جزئی index.html نشان می‌دهد.

شکل ۲۰-۱- صفحه‌ی خانه با استفاده از بوت‌استرپ

2.9.3 - تغییر دادن base.html

باید برای جای دادن قالب بوت‌استرپ در قالب base.html آن را تغییر داد. فایل base.html جدید را بخش به بخش معرّفی می‌کنم.

تعیین سربرگ‌های HTML

اوّلین تغییری که در base.html ایجاد می‌کنیم تعریف174 سربرگ‌های HTML در این فایل است تا هر گاه صفحه‌ی Learning Log باز بود مرورگر در نوار عنوان خود نام سایت را نمایش دهد. علاوه بر این تعدادی از نیازمندی‌های مربوط به به‌کارگیری بوت‌استرپ در قالب‌های خود را نیز اضافه می‌کنیم. هر آن‌چه در base.html وجود دارد پاک کنید و کد زیر را جایگزین کنید:

{% load bootstrap4 %}

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1,
        shrink-to-fit=no">
    <title>Learning Log</title>
      {% bootstrap_css %}
      {% bootstrap_javascript jquery='full' %}
</head>

در ۱ مجموعه‌هایی از تگ‌های قالبی موجود در django-bootstrap4 را بارگذاری می‌کنیم. سپس تعیین می‌کنیم که این یک سند HTML است(۲) که به زبان انگلیسی نوشته شده است (۳) هر فایل HTML به دو بخش اصلی تقسیم می‌شود سربرگ و بدنه—سربرگ فایل از ۴ شروع می‌شود. سربرگ یک فایل HTML هیچ محتوایی را در بر نمی‌گیرد: این بخش تنها به مرورگر می‌گوید که برای نمایش صحیح این صفحه باید چه چیزهایی را بداند. در ۵ عنصر title صفحه را اضافه می‌کنیم که هر گاه صفحه‌ی سایت باز باشد، در نوار عنوان مرورگر نمایش داده می‌شود.

در ۶ از یکی از تگ‌های قالبی شخصی django-bootstrap4 استفاده می‌کنیم که به جانگو می‌گوید تمام فایل‌های شکل‌دهی بوت‌استرپ را وارد کند. تگی که بعد از این می‌آید تمام رفتارهای تعاملی مورد استفاده در صفحه را شامل فعّال می‌کند، مانند نوارهای پیمایش باز و بسته‌شونده. تگ بستن </head> در ۷ می‌آید.

2.9.4 - تعریف کردن نوار پیمایش

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

این اوّلین بخش نوار پیمایش است:

--snip--
</head>
<body>
<nav class="navbar navbar-expand-md navbar-light bg-light mb-4 border">
  <a class="navbar-brand" href="{% url 'learning_logs:index'%}">
    Learning Log</a>
<button class="navbar-toggler" type="button" data-toggle="collapse"
    data-target="#navbarCollapse" aria-controls="navbarCollapse"
    aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span></button>

اوّلین عنصر، تگ گشایش <body> است (۱). بدنه‌ی یک فایل HTML دربردارنده‌ی محتویّا175 تی است که کاربران بر روی صفحه خواهند دید. در ۲ یک عنصر <nav> وجود دارد که نشانگر بخش پیوندهای پیمایش صفحه است. سیاق هر چه که داخل این عنصر باشد مطابق قواعد شکل‌دهی بوت‌استرپ تعیین می‌شود که با انتخاب‌گرهای navbar، navbar-expand-md و دیگر موارد مشخّص شده‌اند. یک انتخاب‌گر تعیین می‌کند که مجموعه‌ای از قوانین شکل‌دهی به کدام عنصرها در صفحه اعمال شوند. انتخاب‌گرهای navbar-light و bg-light پس‌زمینه‌ی نوار پیمایش را به رنگ روشن درمی‌آورند. mb در mb-4 سرواژه‌ی margin-bottom (حاشیه‌ی پایین) است؛ این انتخاب‌گر وجود یک فاصله‌ی کوچک بین نوار پیمایش و دیگر اجزای صفحه را تضمین می‌کند. انتخاب‌گر border یک لبه‌ی نازک به پس‌زمینه‌ی روشن اضافه می‌کند تا آن را کمی از باقی قسمت‌های صفحه متمایز کند.

در ۳ کاری می‌کنیم که نام پروژه در منتها الیه سمت چپ نوار پیمایش پدیدار شود و آن را به صورت پیوندی به صفحه‌ی خانه درمی‌آوریم؛ این نام در تمام صفحه‌های پروژه دیده خواهد شد. انتخاب‌گر navbar-brand این پیوند را به شکلی درمی‌آورد که با بقیه‌ی پیوندها متفاوت باشد و به نوعی نشان سایت به شمار آید.

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

این بخش بعدی کدی است که نوار پیمایش را تعریف می‌کند:

--snip--
       <span class="navbar-toggler-icon"></span></button>
     <div class="collapse navbar-collapse" id="navbarCollapse">
        <ul class="navbar-nav mr-auto">
            <li class="nav-item">
<a class="nav-link" href="{% url 'learning_logs:topics'%}">
      Topics</a></li>
       </ul>

در ۱ بخش جدیدی از نوار پیمایش را باز می‌کنیم. عبارت div مخفّف division به معنای قسمت است؛ شما صفحه‌های وب را با تقسیم کردن آن به بخش‌های مختلف و تعیین شیوه‌ها و قواعد رفتاری به آن بخش طرّاحی می‌کنید. هر شکل و شیوه یا قاعده‌ی رفتاری که در یک تگ گشایش div نوشته شود، روی تمام عنصرهایی که قبل از تگ بستن به صورت </div> قرار می‌گیرند تأثیر می‌گذارند. این آغاز بخشی از نوار پیمایش است که هنگام نمایش بر روی صفحه‌ها و پنجره‌های باریک جمع می‌شود.

در ۲ مجموعه‌ی جدیدی از پیوندها را تعریف می‌کنیم. ب176 وت‌استرپ عنصرهای پیمایش را در قالب یک فهرست نامرتّب تعریف می‌کند ولی قواعد شکل‌دهی آن به شکلی است که به صورت یک فهرست دیده نمی‌شود. هر پیوند یا عنصری که بخواهید روی نوار نمایش دهید می‌تواند به صورت عضوی از این لیست‌ها باشد. اینجا تنها مورد در فهرست ما پیوندی به صفحه‌ی Topics است (۳).

این بخش بعدی نوار پیمایش است:

--snip--
</ul>
<ul class="navbar-nav ml-auto">
  {% if user.is_authenticated %}
     <li class="nav-item">
<span class="navbar-text"}">Hello, {{ user.username }}.</span>
     </li>
     <li class="nav-item">
    	<a class="nav-link" href="{% url 'users:logout' %}">Log out</a>
     </li>
{% else %}
     <li class="nav-item">
<a class="nav-link" href="{% url 'users:register' %}">Register</a>
     </li>
      <li class="nav-item">
<a class="nav-link" href="{% url 'users:login' %}">Log in</a></li>
{% endif %}
     </ul>
  </div>
</nav>

در ۱ مجموعه‌ای دیگر از پیوندها را با یک تگ گشایش <ul> آغاز می‌کنیم. شما می‌توانید هر تعداد مجموعه پیوند که نیاز داشته باشید به صفحه اضافه کنید. این مجموعه پیوندهایی خواهند بود که به ورود و ثبت نام مرتبطند و در سمت راست نوار پیمایش نمایش داده می‌شوند. انتخاب‌گر ml-auto مخفّف margin-left-automatic است: این انتخاب‌گر دیگر عناصر نوار پیمایش را می‌سنجد و حاشیه‌ای در سمت چپ ایجاد می‌کند که این گروه از پیوندها را به سمت راست صفحه براند.

بخش if در ۲ همان شرط‌هایی است که پیش‌تر، بسته به وارد شدن یا نشدن177 کاربران، برای نمایش پیام‌های مناسب به آن‌ها استفاده می‌کردیم. این بخش کمی طولانی‌تر است زیرا چند قاعده‌ی شکل‌دهی هم در داخل تگ‌های شرطی وجود دارد. در ۳ یک عنصر <span> داریم. عنصر محدوده بخشی از متن یا عنصرهای صفحه را شکل‌دهی می‌کند که خود جزئی از یک خط طولانی‌تر هستند. درحالی‌که عنصرهای div بخش‌های مختلفی در صفحه ایجاد می‌کنند، عنصرهای محدوده در داخل یک بخش بزرگ‌تر قرار می‌گیرند. اینجا از این عنصر محدوده برای شکل‌دهی به متن‌های اطّلاع‌دهنده مانند نام کاربری واردشده، استفاده می‌کنیم. هدف ما این است که این اطّلاعات از یک پیوند متمایز باشند تا کاربران تلاش نکنند روی آن کلیک کنند.

در ۴ بخشی را می‌بندیم که قسمت‌های پنهان‌شونده‌ی نوار پیمایش در صفحه‌های باریک را در برمی‌گیرد و در نهایت کل قسمت مربوط به نوار پیمایش را می‌بندیم. اگر بخواهید پیوندهای بیشتری به نوار پیمایش اضافه کنید می‌توانید یک <li> دیگر به هر یک از گروه‌های <ul> که تعریف کردیم اضافه کنید و از همان روش‌های شیوه‌دهی که اینجا دیده‌اید استفاده کنید.

2.9.5 - تعریف قسمت اصلی صفحه

هنوز موارد اندکی مانده تا به base.html اضافه کنیم. باید دو بلوک اضافه کنیم تا صفحه‌های جداگانه بتوانند از آن‌ها برای قرار دادن محتوای مخصوص به خود استفاده کنند.

باقی base.html قسمت اصلی صفحه را در برمی‌گیرد:

--snip--
</nav>
<main role="main" class="container">
    <div class="pb-2 mb-2 border-bottom">
{% block page_header %}{% endblock page_header %}
    </div>
    <div>
{% block content %}{% endblock content %}
    </div>
</main>
</body>
</html>

در ۱ یک تگ <main> باز می‌کنیم. عنصر بخش اصلی برای نمایش برجسته‌ترین بخش بدنه‌ی صفحه‌های شما استفاده می‌شود. اینجا از انتخاب‌گر container بوت‌استرپ بهره می‌گیریم که راهی ساده برای گروه‌بندی عنصرها در یک صفحه است. دو عنصر div در آن قرار می‌دهیم.

اوّلین عنصر div در ۲ بلوک page_header را در برمی‌گیرد. از این قسمت برای تعیین عنوان بیشتر صفحه‌180 179 178 ها استفاده می‌کنیم. برای این که این بخش را از دیگر قسمت‌های صفحه متمایز کنیم، در داخل بخش زیرین عنوان کمی فاصله ایجاد می‌کنیم. این درون‌فاصله به فضایی اشاره دارد که بین محتویات یک عنصر و کناره‌ی آن وجود دارد. انتخاب‌گر pb-2 بوت‌استرپ درون‌فاصله‌ای متوسط در پایین عنصر ایجاد می‌کند. حاشیه فاصله‌ای است که بین کناره‌ی یک عنصر و دیگر عنصرهای صفحه وجود دارد. از آن‌جایی که تنها در پایین صفحه به کناره نیاز داریم از انتخاب‌گر border-bottom کمک می‌گیریم که کناره‌ای نازک در پایین بخش page_header می‌اندازد.

در ۳ یک عنصر div دیگر تعریف می‌کنیم که شامل بلوک content است. هیچ شیوه‌ی خاصّی را در این بلوک اعمال نمی‌کنیم تا هر صفحه بتواند با شکل و شیوه‌ی مناسب خود در اینجا نشان داده شود. کار فایل base.html را با افزودن تگ‌های پایانی برای عنصرهای main، body و html خاتمه می‌دهیم.

اگر اکنون صفحه‌ی خانه‌ی Learning Log را در مرورگری باز کنید باید یک نوار پیمایش با ظاهر حرفه‌ای مطابق آن‌چه در شکل ۲۰-۱ نمایش داده شده ببینید. سعی کنید که پنجره را خیلی باریک کنید؛ باید به جای نوار پیمایش یک دکمه قرار بگیرد. اگر روی آن کلیک کنید باید تمام پیوندها را به صورت یک فهرست پایین‌افتادنی ببینید.

2.9.6 - شکل‌دهی به صفحه‌ی خانه به کمک جامباترون

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

این فایل index.html بازبینی‌شده است:

{% extends "learning_logs/base.html" %}

{% block page_header %}
<div class="jumbotron">
<h1 class="display-3">Track your learning.</h1>
<p class="lead">Make your own Learning Log, and keep a list of the
topics you're learning about. Whenever you learn something new
about a topic, make an entry summarizing what you've learned.</p>
<a class="btn btn-lg btn-primary" href="{% url 'users:register' %}"
role="button">Register &raquo;</a>
</div>
{% endblock page_header %}

در ۱ به جانگو می‌گوییم که آن‌چه در قسمت page_header قرار می‌گیرد تعریف کند. جامباترون در عمل یک عنصر div است که تعدادی دستور شیوه‌دهی به آن اعمال می‌شود (۲). انتخاب‌گر jumbotron این دستورهای شیوه‌دهی را از کتابخانه‌ی بوت‌استرپ بر این عنصر اعمال می‌کند.

داخل جامباترون سه عنصر قرار دارد. اوّلی پیامی کوتاه است، Track your learning، که به کاربران تازه‌وارد دیدی کلّی از کل پروژه می‌دهد. کلاس h1 عنوان سطح اوّل است و انتخاب‌گر display-3 این عنوان خاص را کمی نازک‌تر و بلندتر می‌کند (۳). در ۴ پیامی طولانی‌تر می‌آوریم که حاوی اطّلاعات بیشتری درباره‌ی آن‌چه کاربر می‌تواند با دفترچه‌ی آموخته‌های خود بکند است.

به جای استفاده از یک پیوند متنی، در ۵ یک دکمه می‌سازیم که کاربران را به ایجاد حساب دعوت می‌کند. این همان پیوند موجود در عنوان است امّا دکمه در صفحه به چشم می‌آید و به بیننده می‌گوید برای شروع کار در پروژه باید چه بکند. انتخاب‌گرهایی که اینجا می‌بینید این دکمه را بزرگ می‌کنند که بیانگر درخواست انجام یک عمل است. کد &raquo; یک عنصر HTML است که مانند دو علامت بزرگ‌تری ترکیب‌شده (>>) به نظر می‌آید. در ۶ بلوک page_header را می‌بندیم. چون هیچ مطلب دیگری قرار نیست به این صفحه اضافه کنیم، به تعریف بلوک content احتیاجی نیست.

اکنون صفحه‌ی اوّل مانند شکل ۲۰-۱ دیده می‌شود و این پیشرفت محسوسی نسبت پروژه‌ی بدون شکل ما به حساب می‌آید.

2.9.7 - آراستن صفحه‌ی ورود

ما ظاهر کلی صفحه‌ی ورود را بازنگری کردیم امّا هنوز به فرم ورود نپرداخته‌ایم. حال با تغییر دادن فایل login.html کاری می‌کنیم که این فرم مشابه بخش‌های صفحه به نظر بیاید:

{% extends "learning_logs/base.html" %}
{% load bootstrap4 %}

{% block page_header %}
  <h2>Log in to your account.</h2>
{% endblock page_header %}

{% block content %}
  <form method="post" action="{% url 'users:login' %}" class="form">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
  <button name="submit" class="btn btn-primary">Log in</button>
{% endbuttons %}
<input type="hidden" name="next"
   value="{% url 'learning_logs:index' %}" />
  </form>
{% endblock content %}

در ۱ تگ‌های قالبی bootstrap4 را وارد قالب خود می‌کنیم. در ۲ قسمت page_header را تعریف می‌کنیم که به کاربر توضیح می‌دهد این صفحه چه می‌کند. دقّت کنید که بخش {% if form.errors %} را از قالب حذف کرده‌ایم زیرا django-bootstrap4 به طور خودکار خطاهای فرم را مدیریّت می‌کند.

در ۳ مشخّصه‌ی class="form" را اضافه می‌کنیم و سپس از تگ قالبی {% bootstrap_form %} برای نمایش فرم استفاده می‌کنیم (۴)؛ این تگ جایگزین {{ form.as_p }} به‌کاررفته در فصل ۱۹ می‌شود. تگ قالبی {% bootstrap_form %} هنگام تولید فرم قواعد شکل‌دهی بوت‌استرپ را به تک تک اعضای آن اعمال می‌کند. در ۵ یک تگ قالبی {% buttons %} باز می‌کنیم که شکل‌دهی بوت‌استرپ را به دکمه‌ها اعمال می‌کند.

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

شکل ۲۰-۲: صفحه‌ی ورود به کمک بوت‌استرپ شکل گرفته است.

2.9.8 - شکل دادن به صفحه‌ی موضوعات

حال باید مطمئن شویم که صفحه‌های مربوط به دیدن اطّلاعات هم به خوبی شکل گرفته‌اند، کار را از صفحه‌ی موضوعات شروع می‌کنیم:

{% extends "learning_logs/base.html" %}

{% block page_header %}
  <h1>Topics</h1>
{% endblock page_header %}

{% block content %}
  <ul>
{% for topic in topics %}
  <li><h3>
    <a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a>
  </h3></li>
{% empty %}
  <li><h3>No topics have been added yet.</h3></li>
{% endfor %}
  </ul>
  <h3><a href="{% url 'learning_logs:new_topic' %}">Add a new topic</a></h3>
{% endblock content %}

اینجا به تگ {% load bootstrap4 %} دیگری احتیاجی نداریم زیرا در این فایل از هیچ‌کدام از تگ‌های قالبی bootstrap4 استفاده نمی‌کنیم. عنوان Topics را به بخش page_header منتقل می‌کنیم و به جای استفاده از یک تگ ساده‌ی پاراگراف، به آن شکل عنوان می‌دهیم (۱). هر موضوع را به شکل یک عنصر <h3> درمی‌آوریم تا در صفحه کمی بزرگ‌تر به چشم بیاید (۲) و همین کار را برای پیوند افزودن موضوع جدید انجام می‌دهیم.

2.9.9 - شکل دادن به مطالب در صفحه‌ی موضوع‌ها

صفحه‌ی موضوع‌ها بیشترین محتوا را در بین صفحه‌های سایت دارد پس کمی کار بیشتری می‌برد. از مولفه‌ی کارت بوت‌استرپ استفاده می‌کنیم تا هر موضوع متمایز به نظر بیاید. کارت یک div با مجموعه‌ای از شیوه‌های منعطف و ازپیش‌تعریف‌شده است که برای نمایش مطالب مرتبط با یک موضوع عالی هستند:

{% extends 'learning_logs/base.html' %}

{% block page_header %}
  <h3>{{ topic }}</h3>
{% endblock page_header %}

{% block content %}
  <p>
  	<a href="{% url 'learning_logs:new_entry' topic.id %}">Add new entry</a>
  </p>
  {% for entry in entries %}
<div class="card mb-3">
  	   <h4 class="card-header">
        {{ entry.date_added|date:'M d, Y H:i' }}
        <small><a href="{% url 'learning_logs:edit_entry' entry.id %}">
        edit entry</a></small>
   </h4>
   <div class="card-body">
      {{ entry.text|linebreaks }}
  	   </div>
 </div>
  {% empty %}
<p>There are no entries for this topic yet.</p>
  {% endfor %}
{% endblock content %}

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

اوّلین عنصر داخل کارت عنوانی از نوع <h4> با انتخاب‌گر card-header است (۳). این کارت شامل تاریخ نوشته شدنِ مطلب و پیوندی برای ویرایش آن است. تگ <small> پیرامون پیوند edit_entry باعث می‌شود کوچک‌تر از برچسب زمانی دیده شود (۴). دومین عنصر یک div با انتخاب‌گر card-body است (۵) که متن مطلب را داخل یک جعبه‌ی ساده در کارت قرار می‌دهد. توجّه کنید که کد جانگو برای آوردن اطّلاعات در صفحه تغییر نکرده است؛ تنها عنصرهایی تغییر کرده‌اند که ظاهراً صفحه را تحت تأثیر قرار می‌دهند.

شکل ۲۰-۳ صفحه‌ی موضوع را با ظاهر جدید نمایش می‌دهد. عملکرد سایت عوض نشده امّا اکنون به نظر کاربران حرفه‌ای‌تر و جذّاب‌تر می‌آید.

شکل ۲۰-۳: صفحه‌ی موضوع‌ها با شکل‌دهی بوت‌استرپ

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

خودآزمایی

۲۰-۱- فرم‌های دیگر: شیوه‌های بوت‌استرپ را به صفحه‌ی ورود اعمال کردیم. تغییراتی مشابه را در بقیه‌ی صفحه‌های فرم‌دار مانند new_topic، new_entry، edit_entry و register اعمال کنید.

۲۰-۲- بلاگ خوش‌قیافه: از بوت‌استرپ برای شکل دادن به پروژه‌ی وبلاگی که در فصل ۱۹ ساختید استفاده کنید.

2.9.10 - استقرار پروژه

اکنون که یک پروژه با قیافه‌ای حرفه‌ای داریم بیایید آن را روی یک سرور در حال کار مستقر کنیم تا هر شخص متّصل به اینترنت بتواند از آن استفاده کند. ما از Heroku استفاده می‌کنیم که یک بستر مبتنی بر وب است و به شما امکان می‌دهد که استقرار نرم‌افزارهای تحت وب خود را مدیریّت کنید. ما پروژه‌ی Learning Log را برپا کرده و روی هروکو اجرا می‌کنیم.

ساختن یک حساب کاربری هروکو

برای ساختن یک حساب کاربری به https://heroku.com/ بروید و روی یکی از پیوندهای ثبت نام کلیک کنید. ساختن حساب رایگان است، هروکو یک گزینه‌ی رایگان دارد که به شما اجازه می‌دهد پروژه‌های خود را پیش از استقرار کامل در یک محیط زنده آزمایش کنید.

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

نصب کردن رابط خط فرمان هروکو

برای استقرار و مدیریّت یک پروژه روی سرورهای هروکو182 به ابزارهایی نیاز دارید که در رابط خطّ فرمان (CLI) هروکو در دسترس‌اند. برای نصب کردن آخرین نسخه‌ی این رابط به https://devcenter.heroku.com/articles/heroku-cli/ بروید و دستورالعمل‌های مربوط به سیستم عامل خود را پی بگیرید. این دستورالعمل‌ها یا شامل یک فرمان ترمینال یک‌خطی است یا یک نصب‌کننده است که می‌توانید آن را دریافت و اجرا کنید.

نصب کردن بسته‌های مورد نیاز

شما باید سه بسته را نیز نصب کنید که کمک می‌کنند تا پروژه‌های جانگو را روی یک سرور زنده قرار دهید. در یک محیط فعّال مجازی فرمان‌های زیر را صادر کنید:

1
2
3
4
5
(ll_env)learning_log$ pip install psycopg2==2.7.*

(ll_env)learning_log$ pip install django-heroku

(ll_env)learning_log$ pip install gunicorn

بسته‌ی psycopg2 برای مدیریّت پایگاه‌داده‌ی مورد استفاده‌ی هروکو م183 ورد نیاز است. بسته‌ی django-heroku تقریباً تمام تنظیمات مورد نیاز را برای اجرای برنامه روی سرورهای هروکو مدیریّت می‌کند. این شامل مدیریّت پایگاه داده و ذخیره کردن فایل‌های ایستا در جایی که بتواند به خوبی عرضه شود می‌شود. فایل‌های ایستا قواعد شکل‌دهی و فایل‌های جاوااسکریپت را شامل می‌شوند. بسته‌ی gunicorn سروری ارائه می‌کند که قادر به ارائه‌ی برنامه‌ها در محیطی زنده است.

2.9.11 - ساختن فایل‌های پیکربندی

ساختن یک فایل requirements.txt

هروکو باید بداند که پروژه‌ی ما روی چه بسته‌هایی بنا شده است بنابراین از pip برای تولید فایلی شامل آن‌ها استفاده می‌کنیم. دوباره در یک محیط مجازی فعّال فرمان زیر را صادر کنید:

1
(ll_env)learning_log$ pip freeze > requirements.txt

دستور freeze از pip می‌خواهد که نام تمام بسته‌هایی که در حال حاضر در پروژه‌ی ما نصب شده در فایل requirements.txt بنویسد. این فایل را باز کنید تا نام و نسخه‌ی بسته‌های نصب‌شده در پروژه‌ی خود را ببینید:

dj-database-url==0.5.0
Django==2.2.0
django-bootstrap4==0.0.7
django-heroku==0.3.1
gunicorn==19.9.0
psycopg2==2.7.7
pytz==2018.9
sqlparse==0.2.4
whitenoise==4.1.2

Learning Log در حال حاضر به هشت بسته با نسخه‌های مشخّص وابسته است بنابراین به محیطی مشخّص برای اجرا شدن نیاز دارد. (چهار بسته را خودمان نصب کرده‌ایم و چهار بسته‌ی دیگر به دلیل وابستگی این بسته‌ها به صورت خودکار نصب شده‌اند.)

زمانی که Learning Log را مستقر می‌کنیم هروکو تمام بسته‌های فهرست‌شده در requirements.txt را نصب می‌کند و محیطی پدید می‌آورد که بسته‌های آن مشابه آن چیزی است که به صورت محلّی استفاده کرده‌ایم. به همین دلیل ما می‌توانیم مطمئن باشیم که رفتار پروژه‌ی استقراریافته مشابه پروژه‌ای خواهد بود که در سیستم محلّی ما اجرا می‌شود. هنگامی که ساختن و نگهداری پروژه‌های مختلف را بر سیستم خود آغاز می‌کنید، این مزیّتی بسیار بزرگ به شمار می‌آید.

نکته
اگر بسته‌ای که در سیستم شما فهرست شده نسخه‌ای متفاوت با آن‌چه اینجا می‌بینید دارد، شماره‌ی نسخه‌ی موجود در سیستم خود را نگه‌دارید و آن را دست نزنید.

تعیین نسخه‌ی پایتون اجرایی

تا زمانی که نسخه‌ی پایتون را مشخّص نکنید هروکو از نسخه‌ی پایتون پیش‌فرض فعلی خود استفاده می‌کند. بیایید مطمئن شویم که هروکو از همان نسخه‌ی پایتون ما استفاده می‌کند. در یک محیط مجازی فعّال فرمان python --version را صادر کنید:

1
2
(ll_env)learning_log$ python --version
Python 3.7.2

در این مثال من پایتون نسخه‌ی ۳.۷.۲ را اجرا می‌کنم. فایل جدیدی به نام runtime.txt در همان پوشه‌ی manage.py بسازید و این را داخل آن فایل وارد کنید:

python-3.7.2

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

نکته
اگر به خطایی برخوردید که می‌گفت نسخه‌ی اجرایی پایتونی که می‌خواهید وجود ندارد به https://devcenter.heroku.com/categories/language-support بروید و به دنبال پیوند Specifying a Python Runtime بگردید. داخل این مقاله بگردید تا نسخه‌های موجود را پیدا کنید و آنی را استفاده کنید که به نسخه‌ی پایتون شما خیلی نزدیک است.

تغییر settings.py برای هروکو

حال باید به انتهای settings.py قسمتی اضافه کنیم تا برخی تنظیمات را برای محیط هروکو تعریف کنیم:

1
2
3
4
5
6
7
--snip--
# My settings
LOGIN_URL = 'users:login'

# Heroku settings.
import django_heroku
django_heroku.settings(locals())

اینجا ما ماژول django_heroku را وارد کرده و تابع settings() را صدا می‌زنیم. این تابع برخی تنظیمات را تغییر می‌دهد که برای محیط هروکو باید مقدارهای خاصّی داشته باشند.

ساختن یک Procfile برای آغاز کردن فرآیندها

Procfile به هروکو می‌گوید که برای ارائه‌ی درست پروژه چه فرآیندهایی باید آغاز شوند. این فایل یک‌خطی را با نام Procfile، با P بزرگ و بدونِ پسوند، در همان پوشه‌ی حاوی manage.py ذخیره کنید.

این خطّی است که در فایل Procfile قرار می‌گیرد:

web: gunicorn learning_log.wsgi --log-file –

این خط به هروکو می‌گوید که از gunicorn به عنوان سرور و از تنظیمات داخل learning_log/wsgi.py برای اجرای برنامه استفاده کند. پرچم log-file به هروکو می‌گوید که چه نوع رویدادهایی را ثبت کند.

2.9.12 - استفاده از Git برای ردگیری فایل‌های پروژه

چنان‌که در فصل ۱۷ گفتیم گیت یک برنامه‌ی کنترل نسخه است که به شما ا184 مکان می‌دهد هر بار که ویژگی جدیدی را با موفقیّت پیاده می‌کنید تصویری لحظه‌ای از کد بگیرید. اگر هر مشکلی پیش آمد می‌توانید به آخرین تصویری که کار می‌کرده برگردید؛ برای مثال اگر هنگام کار کردن روی ویژگی جدید به طور اتّفاقی ایرادی ایجاد کردید. به هر کدام از این تصویرها یک سپارش می‌گویند.

به کمک گیت می‌توانید بدون نگرانی از خراب شدن پروژه‌ی خود، پیاده‌سازی ویژگی‌های جدید را امتحان کنید. زمانی که برنامه‌ای را در سرور در حال کار مستقر می‌کنید باید مطمئن باشید که یک نسخه‌ی عملیّاتی از پروژه‌ی خود را استقرار می‌دهید. برای کسب اطّلاعات بیشتر درباره‌ی گیت و کنترل نسخه به پیوست ت مراجعه کنید.

نصب کردن گیت

ممکن است گیت قبلاً روی سیستم شما نصب شده باشد. برای دریافتن این موضوع یک پنجره‌ی ترمینال جدید باز کنید و دستور git --version را وارد کنید:

1
2
(ll_env)learning_log$ git --version
git version 2.17.0

اگر به دلایلی با پیام خطا مواجه شدید، دستورالعمل نصب گیت را در پیوست ت ببینید.

پیکربندی گیت

گیت مراقب است که چه کسی در حال ایجاد تغییرات در یک پروژه است، حتّی اگر فقط یک نفر روی پروژه کار کند. برای چنین کار گیت باید نام کاربری و ایمیل شما را بداند. شما باید یک نام کاربری ارائه کنید ولی در پروژه‌های تمرینی می‌توانید از یک ایمیل ساختگی استفاده کنید.

1
2
3
(ll_env)learning_log$ git config --global user.name "ehmatthes"

(ll_env)learning_log$ git config --global user.email eric@example.com

اگر این گام را فراموش کنید، هنگام انجام اوّلین سپارش گیت این اطّلاعات را از شما خواهد پرسید.

نادیده گرفتن فایل‌ها

ما نیازی نداریم که گیت تمام فایل‌های پروژه را ردگیری کند بنابراین از آن می‌خواهیم که از برخی فایل‌ها صرف نظر کند. فایلی به نام .gitignore در پوشه‌ای که حاوی manage.py است بسازید. توجّه کنید که نام فایل با یک نقطه آغاز می‌شود و پسوند ندارد. این کدی است که در .gitignore قرار می‌گیرد:

ll_env/
__pycache__/
*.sqlite3

به گیت می‌گوییم که کل پوشه‌ی ll_env را نادیده بگیرد زیرا می‌توانیم آن را هر زمان به طور خودکار بازتولید کنیم. همچنین نمی‌خواهیم که پوشه‌ی __pycache__ را دنبال کنید چون فایل‌های .pyc است که جانگو هنگام اجرای فایل‌های .py به طور خودکار می‌سازد. ما تغییرات اعمالی در پایگاه داده‌ی محلّی را هم دنبال نمی‌کنیم زیرا این کار عادت بدی است: اگر از SQLite روی یک سرور استفاده می‌کنید، ممکن است هنگام ارسال پروژه‌ی خود به آن، به طور تصادفی پایگاه داده‌ی آزمایشی محلّی خود را جایگزین پایگاه داده‌ی در حال کار کنید. علامت ستاره در *.sqlite3 به گیت می‌گوید که هر فایلی با پسوند .sqlite3 را نادیده بگیرد.

نکته
اگر از macOS استفاده می‌کنید .DS_Store را به فایل .gitignore خود اضافه کنید. این فایلی است که اطّلاعاتی در مورد تنظیمات پوشه در macOS را دربرمی‌گیرد و هیچ ربطی به پروژه‌ی ما ندارد.

هویدا کردن فایل‌های پنهان

بیشتر سیستم‌عامل‌ها فایل و پوشه‌هایی را که با نقطه آغاز می‌شوند پنهان می‌کنند، مانند .gitignore. اگر از مرورگر فایل یا برنامه‌هایی مانند Sublime Text برای باز کردن آن‌ها استفاده کنید، این نوع فایل‌ها را به صورت پیش‌فرض نمی‌بینید. امّا به عنوان یک برنامه‌نویس باید بتوانید آن‌ها را ببینید. این روش هویدا کردن این فایل‌هاست بسته به این که از چه سیستم عاملی استفاده کنید:

  • در ویندوز Windows Explorer را باز کنید و سپس پوشه‌ای مانند میز کار را باز کنید. روی برگه‌ی Tab بزنید و مطمئن شوید که File name extensions و Hidden items علامت خورده باشند.
  • در macOS می‌توانید دکمه‌های SHIFT-. را در پنجره‌ی هر مرورگر فایلی بزنید تا فایل و پوشه‌های پنهان را ببینید.
  • در سیستم‌های لینوکسی مانند اوبونتو، با فشار دادن CTRL-H در هر مرورگر فایل می‌توانید فایل‌ها و پوشه‌های پنهان را ببینید. برای این که این تنظیمات را دائمی کنید مرورگر فایلی مانند Nautilus را باز کنید و روی برگه‌ی گزینه‌ها (که با سه خط نشان داده شده) کلیک کنید. گزینه‌ی Show Hidden Files را انتخاب کنید.

سپارش پروژه

ما باید برای Learning Log یک مخزن گیت راه بینداز185 یم، تمام فایل‌های لازم را به آن اضافه کنیم و وضعّیت اوّلیّه‌ی پروژه را برون‌سپاری کنیم. این روش انجام این کار است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(ll_env)learning_log$ git init
Initialized empty Git repository in /home/ehmatthes/pcc/learning_log/.git/

(ll_env)learning_log$ git add .

(ll_env)learning_log$ git commit -am "Ready for deployment to heroku."
[master (root-commit) 79fef72] Ready for deployment to heroku.
  create mode 100644 users/views.py

(ll_env)learning_log$ git status
On branch master
nothing to commit, working tree clean

(ll_env)learning_log$

در ۱ فرمان git init را برای راه‌اندازی یک مخزن خالی در پوشه‌ی حاوی Learning Log صادر می‌کنیم. در ۲ از git add . استفاده می‌کنیم که تمام فایل‌هایی را که نادیده گرفته نشده‌اند، به مخزن اضافه می‌کند. (نقطه را فراموش نکنید.) در ۳ فرمان git commit -am commit message را صادر می‌کنیم: پرچم -a به گیت می‌گوید که تمام فایل‌های تغییریافته را هنگام سپارش در نظر بگیر و پرچم -m یک پیام پیگیری را درخواست می‌کند.

صدور فرمان git status در ۴ نشان می‌دهد که در شاخه‌ی master هستیم و درخت کاری ما تمیز است. این وضعیّتی است که پس از هر بار ارسال پروژه به هروکو می‌خواهید ببینید.

2.9.13 - ارسال به هروکو و برپا کردن پایگاه داده

ارسال به هروکو

در نهایت می‌خواهیم پروژه را به هروکو بفرستیم. در یک محیط فعّال مجازی فرمان‌های زیر را وارد کنید:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
(ll_env)learning_log$ heroku login
heroku: Press any key to open up the browser to login or q to exit:
Logging in... done
Logged in as eric@example.com

(ll_env)learning_log$ heroku create
Creating app... done,  secret-lowlands-82594
https://secret-lowlands-82594.herokuapp.com/ |
https://git.heroku.com/secret-lowlands-82594.git

(ll_env)learning_log$ git push heroku master
--snip--
remote: -----> Launching...
remote: Released v5
remote: https://secret-lowlands-82594.herokuapp.com/ deployed to Heroku
remote: Verifying deploy... done.
To https://git.heroku.com/secret-lowlands-82594.git
  * [new branch] master -> master

(ll_env)learning_log$

ابتدا فرمان heroku login را وارد می‌کنید که شما را به صفحه‌ای در مرورگرتان می‌برد تا در آنجا به حساب هروکوی خود وارد شوید (۱). سپس به هروکو می‌گویید که یک پروژه‌ی خالی درست کند (۲). هروکو نامی متشکّل از دو کلمه و یک عدد درست می‌کند؛ می‌توانید آن را بعداً عوض کنید. سپس دستور git push heroku master را وارد می‌کنیم (۳) که به گیت می‌گوید شاخه‌ی اصلی پروژه را در مخزنی که هروکو ساخته بفرست. هروکو با استفاده از این فایل‌ها پروژه را در سرورهای خود می‌سازد. در ۴ آدرسی قرار دارد که از آن برای دسترسی به پروژه‌ی در حال کار استفاده می‌کنیم و می‌توانیم آن را به همراه نام پروژه عوض کنیم.

زمانی که این فرمان‌ها را صادر می‌کنید پروژه استقرار می‌یابد امّا به طور کامل پیکربندی نشده است. برای بررسی این که فرآیندهای سرور به درستی آغاز شده‌اند یا نه، از دستور heroku ps استفاده کنید:

1
2
3
4
5
6
7
8
9
(ll_env)learning_log$ heroku ps
Free dyno hours quota remaining this month: 450h 44m (81%)
Free dyno usage for this app: 0h 0m (0%)
For more information on dyno sleeping and how to upgrade, see:
https://devcenter.heroku.com/articles/dyno-sleeping
=== web (Free): gunicorn learning_log.wsgi --log-file - (1)
web.1: up 2019/02/19 23:40:12 -0900 (~ 10m ago)

(ll_env)learning_log$

خروجی نشان می‌دهد که این پروژه در ماه آینده چه‌قدر دیگر می‌تواند فعّال بماند (۱). در زمان نگارش این مطلب هروکو به استقرارهای رایگان اجازه می‌دهد که حداکثر ۵۵۰ ساعت در ماه فعّال باشند. اگر پروژه‌ای این حد را بگذراند یک صفحه‌ی خطای سرور استاندارد نمایش داده خواهد شد؛ که آن را به زودی شخصی می‌کنیم. در ۲ می‌بینیم که فرآیند تعریف‌شده در Procfile شروع شده است.

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

1
2
3
(ll_env)learning_log$ heroku open

(ll_env)learning_log$

این دستور شما را از باز کردن یک مرورگر و وارد کردن آدرسی که هروکو نشان داده خلاص می‌کند و این راهی دیگر برای باز کردن سایت است. شما باید صفحه‌ی خانه‌ی Learning Log را با شیوه‌ی درست ببینید. با این حال هنوز نمی‌توانید از آن استفاده کنید چون پایگاه داده را برپا نکرده‌اید.

نکته
فرآیند استقرار هروکو ممکن است هر دفعه فرق کند. اگر به مشکل غیر قابل حلّی خوردید برای کمک گرفتن به مستندات هروکو نگاهی بیندازید. به https://devcenter.heroku.com بروید و روی Python کلیک کنید و به دنبال پیوندی به Get Started with Python یا Deploying Python and Django Apps on Heroku بگردید. اگر آنچه را که اینجا دیدید متوجّه نشدید توصیه‌های پیوست پ را ببینید.

برپا کردن پایگاه داده در هروکو

ما نیاز داریم که migrate را یک بار اجرا کنیم تا پایگاه داده‌ی در حال اجرا را برپا و تمام انتقال‌هایی را که طیّ توسعه‌ی پروژه انجام داده‌ایم در آن اعمال کنیم. شما می‌توانید به کمک دستور heroku run دستورهای جانگو و پایتون را روی پروژه‌ی هروکو اجرا کنید. این روش اجرای migrate روی هروکو است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(ll_env)learning_log$ heroku run python manage.py migrate
Running 'python manage.py migrate' on secret-lowlands-82594... up, run.3060
  --snip--
Running migrations:
  --snip--
  Applying learning_logs.0001_initial... OK
  Applying learning_logs.0002_entry... OK
  Applying learning_logs.0003_topic_owner... OK
  Applying sessions.0001_initial... OK

(ll_env)learning_log$

ابتدا دستور heroku run python manage.py migrate را اجرا می‌کنیم (۱). هروکو سپس یک نشست ترمینال می‌سازد تا فرمان migrate را اجرا کنید (۲). در ۳ جانگو انتقال پیش‌فرض و دیگر انتقال‌هایی را که طی توسعه‌ی پروژه ساخته شده‌اند، اعمال می‌کند.

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

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

2.9.14 - بهبود استقرار هروکو

اکنون فرآیند استقرار را با ساختن یک ابرکاربر بهبود می‌بخشیم، درست همانند آن چه به صورت محلّی کردیم. همچنین پروژه را با تغییر تنظیم DEBUG به False امن‌تر می‌کنیم تا کاربران در پیام‌های خطا هیچ اطّلاعات اضافی‌تری که با آن‌ها در حمله به سرور کمک می‌کند، مشاهده نکنند.

ساختن یک ابرکاربر در هروکو

شما قبلاً دیده‌اید که می‌توانیم دستورهای یک‌طرفه را به کمک فرمان heroku run اجرا کنیم. امّا علاوه بر این شما می‌توانید هنگامی که با دستور heroku run bash به سرور هروکو متّصل شده‌اید، دستورها را با باز کردن یک نشست ترمینال Bash اجرا کنید. بَش یک زبان است که در بسیاری از ترمینال‌های لینوکسی اجرا می‌شود. ما از نشست ترمینال بَش برای ساختن یک ابرکاربر استفاده می‌کنیم تا بتوانیم روی برنامه‌ی در حال کار به سایت مدیر دسترسی داشته باشیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
(ll_env)learning_log$ heroku run bash
Running 'bash' on secret-lowlands-82594... up, run.9858

~ $ ls
learning_log learning_logs manage.py Procfile requirements.txt runtime.txt
staticfiles users

~ $ python manage.py createsuperuser
Username (leave blank to use 'u47318'): ll_admin
Email address:
Password:
Password (again):
Superuser created successfully.

~ $ exit
exit

(ll_env)learning_log$

در ۱ ls را اجرا می‌کنیم تا ببینیم چه فایل‌ها و پوشه‌هایی روی سرور وجود دارد که این موارد باید مشابه آن‌هایی باشند که در سیستم محلّی داریم. شما می‌توانید مانند هر سیستم فایلی دیگر در این فایل‌ها و پوشه‌ها بگردید.

نکته
کاربران ویندوز هم از همین دستورهای بالا استفاده خواهند کرد (مانند ls به جای dir) زیرا شما از طریق یک ارتباط از راه دور در حال اجرای یک ترمینال لینوکسی هستید.

در ۲ فرمان ساختن ابرکاربر را اجرا می‌کنیم که خروجی آن مشابه با خروجی‌ای است که در سیستم محلّی خود هنگام ساختن ابرکاربر در فصل ۱۸ دیدیم. زمانی که کار ساختن ابرکاربر در این ترمینال به پایان رسید فرمان exit را اجرا کنید تا به نشست ترمینال سیستم محلّی خود برگردید.

حال می‌توانید /admin/ را به انتهای آدرس برنامه‌ی در حال کار اضافه کنید و به سایت مدیر وارد شوید. برای من آدرس https://secret-lowlands-82594.herokuapp.com/admin/ است.

اگر دیگران استفاده از پروژه‌ی شما را آغاز کرده‌اند، دقّت کنید که به تمام اطّلاعات آن‌ها دسترسی دارید! این را ساده نگیرید تا کاربران همچنان به شما اعتماد کنند.

ساختن یک آدرس کاربرپسند در هروکو

به احتمال فراوان شما دوست دارید که آدرس پروژه دوستانه‌تر و به‌یادماندنی‌تر از https://secret-lowlands-82594.herokuapp.com باشد. می‌توانید نام برنامه را با یک دستور عوض کنید:

1
2
3
4
5
6
7
(ll_env)learning_log$ heroku apps:rename learning-log
Renaming secret-lowlands-82594 to learning-log-2e... done
https://learning-log.herokuapp.com/ | https://git.heroku.com/learning-log.git
Git remote heroku updated
Don't forget to update git remotes for all other local checkouts of the app.

(ll_env)learning_log$

برای نام‌گذاری برنامه‌ی خود می‌توانید از حرف‌ها، اعداد و خط‌های فاصله استفاده کنید و آن را هر چه که بخواهید بنامید مگر این که کسی قبلاً آن نام را گرفته باشد. این استقرار اکنون بر https://learning-log.herokuapp.com در حال اجراست. این پروژه دیگر از آدرس قبلی در دسترس نخواهد بود؛ دستور apps:rename پروژه را کاملاً به URL جدید منتقل می‌کند.

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

2.9.15 - امن کردن پروژه‌ی در حال کار

یک ایراد امنیّتی چشم‌گیر در نحوه‌ی استقرار فعلی پروژه‌ی ما این است: تنظیم DEBUG=True در settings.py که هنگام بروز خطا پیام‌های اشکال‌زدایی را نمایش می‌دهد. صفحه‌های خطای جانگو هنگام توسعه پروژه اطّلاعاتی حیاتی برای اشکال‌زدایی در اختیار شما قرار می‌دهند؛ با این حال اگر آن‌ها را در سرور در حال کار فعّال بگذاریم به مهاجمان اطّلاعاتی بیش از اندازه می‌دهند.

این که اطّلاعات اشکال‌زدایی روی سرور زنده نمایش دا188 ده شوند یا نه با تنظیم یک متغیّر محیطی کنترل می‌شوند. متغیّرهای محیطی مقدارهایی هستند که در یک محیط خاص تنظیم می‌شوند. این یکی از راه‌های نگهداری اطّلاعات حسّاس روی سرور است: آن‌ها را از دیگر کدهای پروژه جدا می‌کنیم.

اجازه دهید settings.py را تغییر دهیم تا هنگام اجرای پروژه روی هروکو به دنبال یک متغیّر محیطی بگردد:

1
2
3
4
5
6
7
8
9
--snip--
# Heroku settings.
import django_heroku
django_heroku.settings(locals())

if os.environ.get('DEBUG') == 'TRUE':
    DEBUG = True
elif os.environ.get('DEBUG') == 'FALSE':
    DEBUG = False

متد os.environ.get() مقدار مرتبط با یک متغیّر محیطی خاص را در هر محیطی که پروژه در آن اجرا می‌شود می‌خواند. اگر متغیّری که به دنبال آن هستیم مقدار داشته باشد، متد آن را برمی‌گرداند و اگر نه متد None برمی‌گرداند. استفاده از متغیّرهای محیطی برای ذخیره کردن مقدارهای بولی ممکن است گیج‌کننده باشد. در بسیاری از موارد متغیّرهای محیطی به صورت رشته ذخیره می‌شوند و شما باید مراقب این باشید. این قطعه‌کد را از یک نشست ترمینال پایتون در نظر بگیرید:

1
2
>>> bool('False')
True

مقدار بولی رشته‌ی 'False' برابر با True است زیرا هر رشته‌ی غیر تهی True ارزیابی می‌شود. بنابراین از رشته‌های 'TRUE' و 'FALSE' با حروف بزرگ استفاده می‌کنیم تا شفّاف بگوییم مقدارهای بولی True و False پایتون را ذخیره نمی‌کنیم. زمانی که جانگو متغیّری محیطی را با کلید 'DEBUG' در هروکو می‌خواند، اگر مقدار آن 'TRUE' باشد، مقدار DEBUG را True می‌گذارد و اگر 'FALSE' باشد مقدار متغیّر DEBUG را False می‌گذارد.

سپارش و انتقال تغییرات

حال باید تغییراتی را که در settings.py اعمال کرده‌ایم در مخزن گیت اعمال کنیم و آن‌ها را به هروکو بفرستیم. این نشست ترمینالی است که این فرآیند را نشان می‌دهد:

1
2
3
4
5
6
7
8
9
(ll_env)learning_log$ git commit -am "Set DEBUG based on environment variables."
[master 3427244] Set DEBUG based on environment variables.
1 file changed, 4 insertions(+)

(ll_env)learning_log$ git status
On branch master
nothing to commit, working tree clean

(ll_env)learning_log$

فرمان git commit را به همراه یک پیام سپارش ساده امّا گویا اجرا می‌کنیم (۱). به یاد بیاورید که پرچم -am باعث می‌شود گیت تمام فایل‌های تغییریافته را اعمال کند و پیامی برای پیگیری ضبط کند. گیت تشخیص می‌دهد که یک فایل تغییر کرده است و این تغییر را در مخزن اعمال می‌کند.

در ۲ وضعیّت نشان می‌دهد که ما در حال کار روی شاخه‌ی اصلی مخزن هستیم و اکنون تغییر جدیدی برای اعمال وجود ندارد. مهم است که پیش از ارسال تغییرات به هروکو وضعیّت را بررسی کنید. اگر این پیام را نبینید برخی از تغییرات هنوز اعمال نشده‌اند و آن تغییرات به سرور ارسال نخواهند شد. در این صورت می‌توانید دستور commit را دوباره صادر کنید و اگر مطمئن نیستید که چگونه این مشکل را حل کنید پیوست ت را ببینید تا کار کردن با گیت را بهتر درک کنید.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
(ll_env)learning_log$ git push heroku master
remote: Building source:
remote:
remote: -----> Python app detected
remote: -----> Installing requirements with pip
--snip--
remote: -----> Launching...
remote: Released v6
remote: https://learning-log.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/learning-log.git
144f020..d5075a1 master -> master

(ll_env)learning_log$

هروکو تشخیص می‌دهد که مخزن به‌روزشده است و این باعث می‌شود که پروژه بازسازی شود تا مطمئن شود تمام تغییرات در نظر گرفته شده‌اند. این کار باعث بازسازی پایگاه داده نمی‌شود بنابراین نیازی نیست migrate را برای این به‌روزرسانی اجرا کنیم.

تنظیم متغیّرهای محیطی در هروکو

حال می‌توانیم مقدار مورد نظر خود برای DEBUG در settings.py را از طریق هروکو تنظیم کنیم. دستور heroku config:set یک متغیّر محیطی را برای ما تنظیم می‌کند:

1
2
3
4
5
(ll_env)learning_log$ heroku config:set DEBUG='FALSE'
Setting DEBUG and restarting learning-log... done, v7
DEBUG: FALSE

(ll_env)learning_log$

زمانی که در هروکو یک متغیّر محیطی را تنظیم می‌کنید، پروژه را به طور خودکار دوباره اجرا می‌کند تا متغیّر محیطی عمل کند.

برای این که ببینید استقرار پروژه اکنون امن‌تر شده است آدرس پروژه را با مسیری که تعریف نکرده‌ایم وارد کنید. برای مثال سعی کنید http://learning-log.herokuapp.com/letmein را باز کنید. باید یک صفحه‌ی خطای نوعی را ببینید که به شما هیچ اطّلاعات خاصّی درباره‌ی پروژه نمی‌دهد. اگر همین درخواست را روی نسخه‌ی محلّی پروژه به آدرس http://localhost:8000/letmein/ بدهید صفحه‌ی خطای کامل جانگو را خواهید دید. نتیجه عالی است: شما هنگام توسعه‌ی پروژه روی سیستم خود پیام‌های خطای پرمعنا می‌بینید امّا کاربران روی سایت در حال کار هیچ اطّلاعات حیاتی‌ای درباره‌ی برنامه‌ی پروژه نخواهند دید.

اگر قصد شما تنها استقرار برنامه و اشکال‌زدایی اوّلیّه‌ی آن است می‌توانید heroku config:set DEBUG='TRUE' را اجرا کنید و به طور موقّت یک گزارش خطای کامل ببینید. مطمئن شوید که پس از پایان اشکال‌زدایی این مقدار را به 'FALSE' برمی‌گردانید. همچنین مراقب باشید هنگامی که کاربران به طور معمول به سایت شما دسترسی دارند این کار را انجام ندهید.

2.9.16 - ساختن صفحه‌های خطای شخصی

در فصل ۱۹ پروژه را طوری پیکربندی کردیم که اگر کاربر صفحه‌ای را درخواست کرد که وجود نداشت یا نوشته‌ای را که متعلّق به او نبود درخواست کرد، یک خطای 404 برگرداند. شما احتمالاً تا الان به برخی خطاهای سرور 500 (خطاهای داخلی) برخورده‌اید. خطای 404 معمولاً به این معنی است که کد جانگو درست است امّا چیزی که درخواست می‌شود وجود ندارد؛ خطای 500 به طور معمول به این معنی است که در کدی که نوشته‌اید خطایی وجود دارد، مانند خطایی در یک تابع در views.py. هم‌اکنون جانگو همان صفحه‌ی خطای عمومی را در هر دو وضعیّت برمی‌گرداند. امّا می‌توانیم قالب صفحه‌های خطای 404 و 500 خودمان را بنویسیم که به ظاهر کلّی پروژه می‌خورد. این قالب‌ها باید در پوشه‌ی ریشه‌ی قالب‌ها قرار بگیرند.

ساختن قالب‌های شخصی

در بیرونی‌ترین پوشه‌ی learning_log پوشه‌ای به نام templates بسازید. سپس فایلی جدید به نام 404.html بسازید؛ مسیر این فایل باید learning_log/templates/404.html باشد. این کد این فایل است:

{% extends "learning_logs/base.html" %}

{% block page_header %}
  <h2>The item you requested is not available. (404)</h2>
{% endblock page_header %}

این قالب ساده اطّلاعات صفحه‌ی خطای 404 را در اختیار می‌گذارد امّا شکل آن مانند بقیه‌ی سایت است.

فایلی دیگر به نام 500.html با کد زیر بسازید:

{% extends "learning_logs/base.html" %}

{% block page_header %}
  <h2>There has been an internal error. (500)</h2>
{% endblock page_header %}

این دو فایل جدید به تغییر کوچک در settings.py نیاز دارند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
--snip--
TEMPLATES = [
{
    'BACKEND': 'django.template.backends.django.DjangoTemplates',
    'DIRS': [os.path.join(BASE_DIR, 'templates')],
    'APP_DIRS': True,
--snip--
},
]
--snip--

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

دیدن صفحه‌های خطا به صورت محلّی

اگر بخواهید پیش از ارسال پروژه به هروکو صفحه‌های خطا را مانند قبل در سیستم خود ببینید باید ابتدا در تنظیمات محلّی خود DEBUG=False را اعمال کنید تا صفحه‌های اشکال‌زدایی پیش‌فرض جانگو غیرفعّال شوند. برای این کار تغییر زیر را در settings.py اعمال کنید (مطمئن شوید روی بخشی از settings.py کار می‌کنید که به محیط محلّی مربوط است نه بخشی که به هروکو ارتباط دارد):

1
2
3
4
--snip--
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
--snip--

حال یک موضوع یا مطلب را که به شما مربوط نیست درخواست کنید تا صفحه‌ی خطای 404 را ببینید. برای امتحان صفحه‌ی خطای 500 موضوع یا مطلبی را درخواست کنید که وجود ندارد. برای مثال آدرس http://localhost:8000/topics/999 باید یک خطای 500 ایجاد کند مگر این که قبلاً ۹۹۹ موضوع آزمایشی ایجاد کرده باشید!

پس از این که آزمایش صفحه‌های خطا به پایان رسید برای توسعه‌ی پروژه در آینده مقدار محلّی DEBUG را به True برگردانید. (مطمئن شوید روشی را که DEBUG در بخش تنظیمات محیط هروکو مدیریّت می‌شود تغییر ندهید.)

نکته
صفحه‌ی خطای 500 هیچ اطّلاعاتی درباره‌ی کاربری که وارد شده نمی‌دهد چون جانگو هنگامی که خطای سرور رخ می‌دهد در پاسخ هیچ اطّلاعاتی درباره‌ی بستر ارتباط نمی‌فرستد.

انتقال تغییرات به هروکو

حال باید تغییراتی که در صفحه‌های خطا ایجاد کرده‌ایم اعمال و آن را به سرور هروکو بفرستیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(ll_env)learning_log$ git add .

(ll_env)learning_log$ git commit -am "Added custom 404 and 500 error pages."
 3 files changed, 15 insertions(+), 10 deletions(-)
  create mode 100644 templates/404.html
  create mode 100644 templates/500.html

(ll_env)learning_log$ git push heroku master
--snip--
remote: Verifying deploy.... done.
To https://git.heroku.com/learning-log.git
    d5075a1..4bd3b1c master -> master

(ll_env)learning_log$

فرمان git add . را در ۱ صادر می‌کنیم زیرا چند فایل جدید به پروژه اضافه کرده‌ایم و باید به گیت بگوییم که آن‌ها را ردگیری کند. سپس در ۲ تغییرات را اعمال و در ۳ پروژه‌ی به‌روزشده را به هروکو می‌فرستیم.

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

2.9.17 - استفاده از get_object_or_404()

تا اینجا اگر کاربر موضوع یا مطلبی را که وجود ندارد به صورت دستی درخواست کند به خطای سرور 500 برمی‌خورد. جانگو سعی می‌کند صفحه‌ای را که وجود ندارد بسازد امّا چون برای این کار اطّلاعات کافی ندارد خطای 500 ایجاد می‌شود. این وضعّیت به طور دقیق‌تر شبیه به خطای 404 است و می‌توانیم این رفتار را به کمک تابع میان‌بر get_object_or_404() جانگو پیاده‌سازی کنیم. این تابع تلاش می‌کند که شیء درخواستی را از پایگاه داده بگیرد، امّا اگر وجود نداشت یک استثنای 404 ایجاد می‌کند. این تابع را در views.py وارد می‌کنیم و آن را به جای get() می‌گذاریم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
--snip--

@login_required
def topic(request, topic_id):
    """Show a single topic and all its entries."""
    topic = get_object_or_404(Topic, id=topic_id)
    # Make sure the topic belongs to the current user.
--snip--

حال اگر موضوعی را درخواست کنید که وجود ندارد (برای مثال http://localhost:8000/topics/999) یک صفحه‌ی خطای 404 خواهید دید. برای استقرار این تغییر یک سپارش جدید ایجاد کنید و پروژه را به هروکو انتقال دهید.

2.9.18 - توسعه‌های آینده و نکته‌های پایانی

شما ممکن است بخواهید پس از انتقال اوّلیّه‌ی پروژه به سرور آن را توسعه دهید یا پروژه‌های خود را برای استقرار توسعه دهید. فرآیندی تقریباً یکسان برای به‌روزرسانی پروژه‌ها وجود دارد.

ابتدا هر تغییری که مورد نیاز است در پروژه‌ی محلّی خود ایجاد می‌کنید. اگر این تغییرات به ایجاد فایل‌های جدید منجر می‌شود آن‌ها را با دستور git add . به مخزن گیت اضافه کنید. (مطمئن شوید که نقطه را در پایان دستور می‌آورید.) هر تغییری در انتقال پایگاه داده به این دستور نیاز دارد چون در هر انتقال یک فایل انتقال جدید ساخته می‌شود.

دوم تغییرات را در مخزن خود به کمک دستور git commit -am "commit message" اعمال کنید. پس از آن تغییرات را با دستور git push heroku master به سرور منتقل کنید. اگر پایگاه داده‌ی محلّی خود را هم انتقال داده باشید باید پایگاه داده‌ی در حال کار روی سرور را هم انتقال دهید. یا باید از دستورهای یک‌طرفه مانند heroku run python manage.py migrate استفاده کنید یا یک نشست ترمینال از راه دور با دستور heroku run bash باز کنید و دستور python manage.py migrate را اجرا کنید. سپس پروژه‌ی در حال کار خود را ببینید و مطمئن شوید که تغییرات مورد انتظارتان اعمال شده است یا نه.

تنظیمات SECRET_KEY

جانگو از مقدار تنظیم‌شده‌ی SECRET_KEY در settings.py برای پیاده‌سازی تعدادی از پروتکل‌های امنیّتی استفاده می‌کند. در این پروژه فایل تنظیمات خود را به همراه تنظیم SECRET_KEY اعمال کرده‌ایم. این برای پروژه‌های تمرینی خوب است امّا برای محصول نهایی باید این تنظیم را با دقّت بیشتری انجام داد. اگر پروژه‌ای می‌سازید که کاربردی جدّی‌تر دارد مطمئن شوید که درباره‌ی روش مدیریّت امن‌تر SECRET_KEY تحقیق کنید.

پاک کردن پروژه‌ای از هروکو

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

به وب‌سایت هروکو (https://heroku.com) وارد شوید؛ به صفحه‌ای که نمایانگر پروژه‌های شماست راهنمایی می‌شوید. روی پروژه‌ای که می‌خواهید پاک کنید کلیک کنید. صفحه‌ای جدید با اطّلاعاتی درباره‌ی آن پروژه خواهید دید. روی پیوند Settings کلیک کنید و پایین بروید تا به پیوندی برای پاک کردن پروژه برسید. این عمل برگشت‌پذیر نیست پس هروکو از شما می‌خواهد که با وارد کردن دستی نام پروژه درخواست پاک کردن آن را تایید کنید.

اگر کار کردن از یک ترمینال را ترجیح می‌دهید می‌توانید پروژه را با صدور فرمان destroy هم پاک کنید:

1
(ll_env)learning_log$ heroku apps:destroy --app appname

در اینجا appname نام پروژه‌ی شماست که باید چیزی شبیه به secret-lowlands-82594 یا اگر نام پروژه را عوض کرده‌اید، learning-log باشد. از شما خواسته می‌شود که نام پروژه را دوباره وارد کنید تا حذف کردن تایید شود.

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

خودآزمایی

۲۰-۳- وبلاگ زنده: پروژه‌ی بلاگی را که روی آن کار می‌کرده‌اید روی هروکو مستقر کنید. مطمئن شوید که DEBUG را برابر با False می‌گذارید تا صفحه‌های خطای کامل جانگو هنگام بروز خطا دیده نشوند.

۲۰-۴- 404های بیشتر: تابع get_object_or_404() باید در نماهای new_entry() و edit_entry() هم استفاده شود. این تغییر را ایجاد کنید و با وارد کردن آدرسی مانند http://localhost:8000/new_entry/999 بررسی کنید که آیا خطای 404 را می‌بینید یا نه.

۲۰-۵- Learning Log توسعه‌یافته: یک ویژگی جدید به پروژه اضافه کنید و تغییر را به استقرار در حال کار خود بفرستید. تغییری ساده را امتحان کنید، مثلاً در صفحه‌ی خانه کمی بیشتر درباره‌ی پروژه بنویسید. یک ویژگی پیشرفته‌تر را امتحان کنید، مثلاً این که به کاربران امکان عمومی کردن یک موضوع را بدهید. برای این کار باید مشخّصه‌ای به نام public را به مدل Topic اضافه کنید (که به صورت پیش‌فرض False باشد) و همچنین عنصری به فرم صفحه‌ی new_topic اضافه کنید که به کاربر اجازه‌ی تغییر موضوع از خصوصی به عمومی را بدهد. در ادامه باید پروژه را انتقال دهید و views.py را بازنگری کنید تا هر موضوعی که عمومی بود برای کاربران واردنشده هم قابل دیدن باشد. فراموش نکنید که پایگاه داده‌ی در حال کار را هم پس از ارسال تغییرات به هروکو انتقال دهید.

2.9.19 - خلاصه

در این فصل آموختید که چگونه به کمک کتابخانه‌ی بوت‌استرپ و برنامه‌ی django-bootstrap4 پروژه‌های خود ظاهری ساده امّا حرفه‌ای بدهید. به کمک بوت‌استرپ شکل و شیوه‌ای که انتخاب می‌کنید تقریباً روی هر افزاره‌ای که مردم برای دیدن پروژه‌ی شما استفاده می‌کنند به یک شکل به نظر می‌آید.

شما درباره‌ی قالب‌های بوت‌استرپ آموختید و از قالب Navbar static برای ساختن یک شکل و ظاهر ساده برای Learning Log استفاده کردید. از جامباترون برای متمایز کردن پیام صفحه‌ی خانه استفاده کردید و آموختید که چگونه تمام صفحه‌ها را به یک صورت شکل دهید.

در آخرین بخش پروژه یاد گرفتید که چگونه یک پروژه را روی سرورهای هروکو مستقر کنید تا هر کس بتواند به آن دسترسی داشته باشد. یک حساب کاربری هروکو ساختید و چند ابزار نصب کردید که در مدیریّت فرآیند استقرار به شما کمک می‌کند. از گیت برای سپارش پروژه‌ی در حال کار به یک مخزن و سپس انتقال آن به سرورهای هروکو کمک گرفتید. در نهایت یاد گرفتید که با تنظیم DEBUG=False روی سرور امن کردن برنامه‌ی خود را شروع کنید.

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


3 - موخره

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

4 - پیوست‌ها

تبریک می‌گویم. اکنون شما آن اندازه‌‌ از پایتون می‌دانید که بتوانید شروع به ساختن پروژه‌های تعاملی و معنادار کنید. با ساختنِ این پروژه‌ مهارت‌های جدیدی می‌آموزید و مفاهیم یادگرفته‌شده در بخش یکم تثبیت می‌شوند. بخش دوم سه نوع پروژه را در برمی‌گیرد و می‌توانید هر کدام از آن‌ها را به هر ترتیبی که خواستید انجام دهید. اینجا شرحی مختصر از هر کدام می‌آید تا به شما کمک کند در مورد انتخاب اوّلین پروژه تصمیم بگیرید.

تهاجمِ موجودات بیگانه: ساختنِ یک بازی با پایتون در پروژه‌ی تهاجم موجودات بیگانه (فصل‌های ۱۲، ۱۳ و ۱۴) از بسته‌ی Pygame برای توسعه دادن یک بازی دوبعدی بهره می‌گیرید که هدف در آن، شلیک کردن به ناوگانی از موجودات بیگانه است. این موجودات مرحله به مرحله از صفحه پایین می‌آیند و در هر مرحله سرعت و میزان مشکل بودن بازی افزایش می‌یابد. در پایان پروژه مهارت‌هایی کسب خواهید کرد که به شما امکان خواهد داد بازی دوبعدی دلخواه خود را به کمک Pygame توسعه دهید. بصری‌سازی داده‌ها پروژه‌ی بصری‌سازی داده‌ها از فصل ۱۵ شروع می‌شود و در آن می‌آموزید که چگونه داده‌ها را تولید کنید و سپس تعدادی نمایشِ بصریِ زیبا و کاربردی از آن‌ها به کمک Matplotlib و Plotly بسازید. فصل ۱۶ به شما یاد می‌دهد که به داده‌های منابع آنلاین دسترسی پیدا کنید و آن‌ها را به یک بسته‌ی بصری‌سازی بدهید تا نمودارهایی از داده‌های هواشناسی و یک نقشه‌ی لرزه‌نگار جهانی رسم کند. سرانجام در فصل ۱۷ خواهید دید که چگونه برنامه‌ای برای دریافت خودکار و بصری‌سازی داده بنویسید. آموختن ساختن نمایش‌های بصری به شما امکان می‌دهد که حوزه‌ی داده‌کاوی را بررسی کنید که یکی از مهارت‌های مورد توجّه در دنیای امروز است. برنامه‌ی تحت وب در پروژه‌ برنامه‌های تحت وب (فصل‌های ۱۸، ۱۹ و ۲۰) از بسته‌ی Django برای ساختن یک برنامه‌ی وب ساده استفاده می‌کنید که به کاربران امکان می‌دهد هر تعداد موضوع مورد مطالعه را در قالب یک نشریه ضبط کنند. کاربران یک حساب کاربری با نام کاربری و گذرواژه می‌سازند، موضوعی را وارد می‌کنند و سپس مواردی را که یاد گرفته‌اند مورد به مورد اضافه می‌کنند. همچنین یاد خواهید گرفت که چگونه برنامه‌ی خود را استقرار دهید تا از همه جای دنیا قابل دسترسی باشد. پس از تمام کردن این پروژه قادر خواهید بود که ساختن برنامه‌های تحت وب ساده‌ی خود را شروع کنید و این آمادگی را خواهید داشت که در منابع مربوط به ساختن برنامه‌ها با Django پژوهش کنید.