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

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

بخش دوم - پروژه‌ها

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

تهاجمِ موجودات بیگانه: ساختنِ یک بازی با پایتون در پروژه‌ی تهاجم موجودات بیگانه (فصل ۱۲, فصل ۱۳ و فصل ۱۴) از بسته‌ی Pygame برای توسعه دادن یک بازی دوبعدی بهره می‌گیرید که هدف در آن، شلیک کردن به ناوگانی از موجودات بیگانه است. این موجودات مرحله به مرحله از صفحه پایین می‌آیند و در هر مرحله سرعت و میزان مشکل بودن بازی افزایش می‌یابد. در پایان پروژه مهارت‌هایی کسب خواهید کرد که به شما امکان خواهد داد بازی دوبعدی دلخواه خود را به کمک Pygame توسعه دهید.

بصری‌سازی داده‌ها پروژه‌ی بصری‌سازی داده‌ها از فصل ۱۵ شروع می‌شود و در آن می‌آموزید که چگونه داده‌ها را تولید کنید و سپس تعدادی نمایشِ بصریِ زیبا و کاربردی از آن‌ها به کمک Matplotlib و Plotly بسازید. فصل ۱۶ به شما یاد می‌دهد که به داده‌های منابع آنلاین دسترسی پیدا کنید و آن‌ها را به یک بسته‌ی بصری‌سازی بدهید تا نمودارهایی از داده‌های هواشناسی و یک نقشه‌ی لرزه‌نگار جهانی رسم کند. سرانجام در فصل ۱۷ خواهید دید که چگونه برنامه‌ای برای دریافت خودکار و بصری‌سازی داده بنویسید. آموختن ساختن نمایش‌های بصری به شما امکان می‌دهد که حوزه‌ی داده‌کاوی را بررسی کنید که یکی از مهارت‌های مورد توجّه در دنیای امروز است.

برنامه‌ی تحت وب در پروژه‌ برنامه‌های تحت وب (فصل ۱۸, فصل ۱۹ و فصل ۲۰) از بسته‌ی Django برای ساختن یک برنامه‌ی وب ساده استفاده می‌کنید که به کاربران امکان می‌دهد هر تعداد موضوع مورد مطالعه را در قالب یک نشریه ضبط کنند. کاربران یک حساب کاربری با نام کاربری و گذرواژه می‌سازند، موضوعی را وارد می‌کنند و سپس مواردی را که یاد گرفته‌اند مورد به مورد اضافه می‌کنند. همچنین یاد خواهید گرفت که چگونه برنامه‌ی خود را استقرار دهید تا از همه جای دنیا قابل دسترسی باشد. پس از تمام کردن این پروژه قادر خواهید بود که ساختن برنامه‌های تحت وب ساده‌ی خود را شروع کنید و این آمادگی را خواهید داشت که در منابع مربوط به ساختن برنامه‌ها با Django پژوهش کنید.

1 - فصل دوازدهم

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]

شکل ۱۳-۲ اوّلین موجود بیگانه نمایان می‌شود.

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

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)

این کد عددی تصادفی بین ۱۰- و ۱۰ برمی‌گرداند. با استفاده از کد خود در تمرین ۱۳-۱ موقعّیت هر ستاره را به صورت تصادفی تعیین کنید.

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() پیش از به‌روزرسانی موقعیّت هر موجود بیگانه است.

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

خودآزمایی

۱۳-۳ قطره‌های باران: تصویر یک قطره‌ی باران را پیدا کنید و شبکه‌ای از قطره‌های باران بسازید. کاری کنید که قطره‌ها به سمت پایین صفحه حرکت کنند تا جایی که ناپدید شوند.

۱۳-۴ باران مداوم: کد خود در تمرین ۱۳-۳ را جوری تغییر دهید که با ناپدید شدن هر ردیف قطره‌های باران در پایین صفحه، یک ردیف جدید در بالای آن اضافه شود و شروع به حرکت کند.

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() می‌شود و توسعه را در آینده ساده‌تر می‌کند.

خودآزمایی

۱۳-۵ تیرانداز جانبی بخش ۲: از تمرین ۱۲-۶ (تیراندازان جانبی) تا اینجا راه درازی آمده‌ایم. برای این تمرین آن بازی را به جایی برسانید که هجوم بیگانگان را توسعه داده‌ایم. ناوگانی از موجودات بیگانه اضافه کنید و کاری کنید که در عرض صفحه به سمت سفینه حرکت کند. یا کدی بنویسید که موجودات بیگانه را در موقعّیت‌های تصادفی در سمت راست صفحه قرار دهد و آن را به سمت سفینه بفرستد. همچنین کدی بنویسید که موجودات بیگانه را پس از اصابت ناپدید کند.

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.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 - رسم دکمه روی صفحه و آغاز بازی

از کلاس 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.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.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.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.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.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.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.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.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 که راهنمایی برای چگونه بازی کردن نشان می‌دهد. همچنین یاد گرفتید که چگونه سرعت بازی را همزمان با پیشرفت آن افزایش دهید، یک سیستم امتیازدهی پیش‌رو طرّاحی کنید و اطّلاعات را به روش‌های متنی و غیرمتنی نمایش دهید.


3 - فصل چهاردهم

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/ بزنید. اگر روی هر کدام از بصری‌سازی‌های موجود در گالری کلیک کنید کدی که آن را ایجاد کرده خواهید دید.

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 را باز می‌کند و نمودار را نشان می‌دهد. نمایشگر به شما امکان بزرگنمایی و پیمایش نمودار را می‌دهد و اگر روی آیکون ذخیره کلید کنید می‌توانید هر نموداری را که خواستید ذخیره کنید.

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).

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

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() می‌توانید نشان‌وندهای متعدّدی را تعیین کنید و از تعداد زیادی تابع برای شخصی‌سازی نمودارهای خود بهره بگیرید. این تابع‌های شخصی‌سازی را هنگام کار کردن با مجموعه‌داده‌های جذّاب‌تر در طول فصل بیشتر بررسی خواهیم کرد.

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 استفاده می‌کند. گستره‌ی بزرگی از این شیوه‌های شکل‌دهی وجود دارد؛ با آن‌ها سر و کله بزنید تا چیزی مناسب سلیقه‌ی خود پیدا کنید.

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 را بین ۰ و ۱،۱۰۰،۰۰۰ قرار می‌دهیم.

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 ذخیره خواهد شد. نشانوند دوم فاصله‌های خالی اضافی اطراف نمودار را حذف می‌کند. اگر می‌خواهید این فضای خالی باقی بماند کافی‌است این نشاوند را حذف کنید.

خودآزمایی

۱۵-۱- عددهای مکعب‌: توان سوم هر عدد را مکعّب آن می‌خوانند. ابتدا نمودار ۵ عدد مکعّب نخست را بکشید سپس این کار را برای ۵۰۰۰ عدد مکعّب نخست تکرار کنید.

۱۵-۲- اعداد مکعّب رنگی: یک نقشه‌ی رنگی روی نمودار اعداد مکعّب اعمال کنید.

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) قرار می‌دهیم.

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() می‌دهیم و اندازه‌ی مناسبی برای نقطه‌ها انتخاب می‌کنیم.


4 - فصل پانزدهم

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

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 ذخیره خواهد شد. نشان‌وند دوم فاصله‌های خالی اضافی اطراف نمودار را حذف می‌کند. اگر می‌خواهید این فضای خالی باقی بماند کافی‌است این نشان‌وند را حذف کنید.

خودآزمایی

۱۵-۱. عددهای مکعب‌: توان سوم هر عدد را مکعّب آن می‌خوانند. ابتدا نمودار ۵ عدد مکعّب نخست را بکشید سپس این کار را برای ۵۰۰۰ عدد مکعّب نخست تکرار کنید.

۱۵-۲. اعداد مکعّب رنگی: یک نقشه‌ی رنگی روی نمودار اعداد مکعّب اعمال کنید.

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 را بزنید.

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() را کاهش داده و خواندن و درک متد را آسان‌تر کند.

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]

شکل ۱۵-۱۲: نمودار میله‌ای ساده‌ای که با پلات‌لی درست شده است.

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

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 و پلات‌لی برای کاویدن آن داده‌ها ادامه خواهید داد.


5 - فصل شانزدهم

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]

ما بیشینه‌ی دمای هر روز را استخراج و در لیستی ذخیره کرده‌ایم. اکنون بیایید این داده‌ها را بصری کنیم.

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 تعیین نمی‌کنیم امّا اندازه‌ی متن آن را برای افزایش خوانایی برچسب‌های پیش‌فرض تغییر می‌دهیم. شکل ۱۶-۱ نمودار حاصل را نشان می‌دهد: یک نمودار خطّی ساده مربوط به بیشینه‌های دما در ژوئیه‌ی ۲۰۱۸ در سیتکای آلاسکا.

شکل ۱۶-۱- نموداری خطّی که بیشینه‌ی دمای روزانه در ماه ژوئیه‌ی سال ۲۰۱۸ در سیتکای آلاسکا نشان می‌دهد.

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)

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 خود دارد معنادارتر است.

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 استفاده کنیم و عنوان نمودار خود را عوض می‌کنیم تا بیانگر تغییر محتویّات آن باشد. شکل ۱۶-۳ نمودار حاصل را نشان می‌دهد.

شکل ۱۶-۳- داده‌های یک ساله

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() دیگر برای دماهای کمینه اضافه می‌کنیم و این مقادیر را آبی می‌کنیم. در نهایت عنوان را به‌روز می‌کنیم. شکل ۱۶-۴ نمودار حاصل را نشان می‌دهد.

شکل ۱۶-۴- دو دنباله‌ی داده در یک نمودار

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ی آن را ۰٫۱ می‌گذاریم تا ناحیه‌ی پرشده بدون منحرف کردن توجّه از اطّلاعات نمایش‌داده‌شده، دو منحنی را به هم وصل کند. شکل ۱۶-۵ نمودار را با ناحیه‌ی سایه‌خورده بین مقدارهای کمینه و بیشینه نشان می‌دهد.

شکل ۱۶-۵- ناحیه‌ی بین دو مجموعه داده سایه خورده است.

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

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() برای حذف کردن برخی از داده‌های استخراج‌شده استفاده می‌کنید. هر رویکردی را که کار می‌کند پی بگیرید تا زمانی که نتیجه‌ی آن بصری‌سازی دقیق و معنادار باشد.

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 را دستی تعیین کردیم. از سطر عنوان‌ها برای تعیین این شاخص‌ها برای مقدارها استفاده کنید تا برنامه‌ی شما برای هر دو جا، سیتکا یا دره‌ی مرگ، کار کند. از نام ایستگاه هواشناسی هم برای تولید خودکار عنوان مناسب برای نمودار خود استفاده کنید.

۱۶-۵- کند و کاو: چند بصری‌سازی دیگر بسازید تا دیگر جنبه‌های آب و هوایی مورد علاقه‌ی خود را در هر موقعیّتی که درباره‌ی آن کنجکاو هستید، بررسی کنید.

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 از قرارداد (طول، عرض) پیروی می‌کند و اگر شما از چارچوب دیگری استفاده می‌کنید مهم است که قرارداد مورد استفاده‌ی آن را یاد بگیرید.

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]

با این داده‌ها می‌توانیم به سمت نگاشت هر زمین‌لرزه حرکت کنیم.

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 قرار دهید خواهید دید که نمودار مشابهی حاصل خواهد شد. این شکل تعریف به ما امکان می‌دهد که شخصی‌سازی‌ها را نسبت به حالت قبلی آسان‌تر تعیین کنیم.

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 خواهید دید. در شکل ۱۶-۹ مقیاس رنگی شدت یک‌یک زمین‌لرزه‌ها را نشان می‌دهد. رسم این تعداد زمین‌لرزه به طور واضح نشان می‌دهد که مرزهای صفحه‌های تکتونیکی کجاست!

شکل ۱۶-۹: در زمین‌لرزه‌های ۳۰ روزه رنگ و اندازه برای نمایش بزرگی هر زمین‌لرزه به کار می‌رود.

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 اضافه می‌کنیم، پلات‌لی از این مقدار به عنوان برچسب هر نشان‌گر استفاده می‌کند که هنگام ایستادن روی آن نمایش داده می‌شود. زمانی که لیستی که طول آن به تعداد نشان‌گر می‌خورد، ارائه کنیم پلات‌لی هر برچسب را به یک نشان‌وند نسبت می‌دهد. زمانی که این برنامه را اجرا کنید باید بتوانید روی هر نشان‌گر بایستید و شرحی از محل وقوع زمین‌لرزه و بزرگی دقیق آن ببینید.

این تاثیرگذار است! در تقریباً ۴۰ خط کد نقشه‌ای زیبا و پرمعنی از فعالیّت‌های زمین‌لرزه‌ای جهانی ساختیم که ساختار زمین‌شناختی سیّاره را هم به تصویر می‌کشد. پلات‌لی چندین راه برای شخصی‌سازی ظاهر و رفتار بصری‌سازی‌ها به شما ارائه می‌کند. با استفاده از این گزینه‌های پلات‌لی می‌توانید جدول‌ها و نقشه‌هایی بسازید که دقیقاً مطابق با خواسته‌های شما هستند.

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 پیدا کنید.

5.16 - خلاصه

در این فصل کار کردن با مجموعه‌داده‌های دنیای واقعی را آموختید. فایل‌های CSV و JSON را پردازش و داده‌های مورد نظر خود را از آن‌ها استخراج کردید. با استفاده از داده‌های تاریخی آب و هوا درباره‌ی کار کردن با Matplotlib بیشتر آموختید، مانند چگونگی کار با ماژول datetime و نحوه‌ی رسم چند دنباله‌ی داده بر روی یک نمودار. شما در پلات‌لی داده‌های جغرافیایی را روی نقشه‌ی جهان رسم کردید و نقشه‌ها و نمودارهای پلات‌لی را نیز شکل دادیم.

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

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


6 - فصل هفدهم

6.1 - کار کردن با APIها

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

6.2 - استفاده از یک API تحت وب

API تحت وب بخشی از یک وب‌سایت است که برای ارتباط ب136 ا برنامه‌ها طرّاحی شده است. برنامه‌ها از URLهای خاصّی برای درخواست اطّلاعات استفاده می‌کنند. این درخواست‌ها فراخوانی API خوانده می‌شود. داده‌ی درخواستی در قالبی دریافت می‌شود که پردازش آن آسان باشد، مانند JSON یا CSV. بسیاری از برنامه‌هایی که بر منابع داده‌های خارجی متّکی هستند، مانند برنامه‌هایی که با سایت‌های شبکه‌های اجتماعی ادغام می‌شوند، به این فراخوانی‌ها وابسته‌اند.

6.3 - Git و GitHub

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

گیت‌هاب (https://github.com/) نام خود را از Git گرفته که یک سیستم کنترل نسخه‌ی توزیع‌شد138 137 ه است. گیت به افراد کمک می‌کند که کار خود را در یک پروژه مدیریّت کنند تا تغییراتی که یک نفر ایجاد می‌کند در کار دیگران اختلالی ایجاد نکند. زمانی که ویژگی جدیدی به یک پروژه اضافه می‌کنید، گیت تغییرات شما را در هر فایل دنبال می‌کند. اگر کد شما کار کند، شما تغییرات اعمالی خود را اجرایی می‌کنید و گیت وضعیّت جدید پروژه را ثبت می‌کند. اگر اشتباهی کرده باشید و بخواهید تغییرات خود را برگردانید، به سادگی می‌توانید به یک وضعّیت درست قبلی برگردید. (برای آشنایی بیشتر با کنترل نسخه با گیت پیوست ت را ببینید) پروژه‌های گیت‌هاب در مخزن‌ها نگهداری می‌شوند که دربردارنده‌ی تمام چیزهای مربوط به یک پروژه است: کد آن، اطّلاعات مشارکت‌کنندگان، مشکلات یا گزارش ایرادات و …

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

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 می‌آید که لیستی شامل جزئیات محبوب‌ترین پروژه‌های پایتون روی گیت‌هاب است. (۳)

6.5 - نصب کردن Requests

بسته‌ی Requests به برنامه‌های پایتون امکان می‌دهد که به سادگی از یک وب‌سایت درخواست اطّلاعات کرده و پاسخ آن‌ها را بررسی کنند. از pip برای نصب آن استفاده می‌کنیم:

1
$ python -m pip install --user requests

این خط به پایتون می‌گوید که ماژول pip را اجرا و بسته‌ی Requests را به نصب پایتون کاربر فعلی اضافه کند. اگر از python3 یا دستور متفاوتی برای اجرای برنامه‌ها و نصب بسته‌ها استفاده می‌کنید، مطمئن شوید که اینجا هم از آن‌ها استفاده می‌کنید.

نکته
اگر این دستور در macOS کار نکرد آن را بدون پرچم --user امتحان کنید.

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 اشکالی ندارد. امّا اگر فراخوانی‌های پیچیده‌تری انجام می‌دهید برنامه‌ی شما باید این مقدار را بررسی کند.

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 مخزن پروژه، تاریخ ایجاد آن در ژوئن ۲۰۱۴ و به‌روزرسانی شدن آن را به تازگی ببینیم. علاوه بر این شرح مخزن به ما می‌گوید که این پروژه شامل فهرستی از محبوب‌ترین منابع پایتون است.

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

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 دریافت کنید محدودیّت استفاده‌ی شما خیلی کم‌تر خواهد بود.

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، یک دیکشنری می‌سازیم که مشخّصات چیدمان داخل آن قرار می‌گیرد. عنوانی برای نمودار کلّی و برچسبی برای هر محور تعیین می‌کنیم.

شکل ۱۷-۱ نمودار حاصل را نشان می‌دهد. می‌بینیم که چند پروژه‌ی اوّل به طور چشم‌گیری از باقی پروژه‌ها محبوب‌ترند ولی همه‌ی آن‌ها پروژه‌های مهمّی در زیست‌بوم پایتون به شمار می‌آیند.

شکل ۱۷-۱: محبوب‌ترین پروژه‌های پایتون در گیت‌هاب

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 انجام می‌دهیم.

شکل ۱۷-۲ نمودار دوباره‌طرّاحی‌شده را نشان می‌دهد:

شکل ۱۷-۲: شکل و سیاق نمودار بازنگری شده است.

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 اضافه می‌کنیم و لیستی را که هم‌اکنون ساخته‌ایم به آن نسبت می‌دهیم. پلات‌لی همچنان‌که هر میله را می‌سازد برچسب متناظرش را از این لیست بیرون می‌کشد و تنها زمانی آن را نمایش می‌دهد که بیننده روی آن میله توقّف کند.

شکل ۱۷-۳ نمودار حاصل را نشان می‌دهد.

شکل ۱۷-۳: قرار دادن نشان‌گر روی یک میله نام مالک و شرح پروژه را نشان می‌دهد.

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 دریافت شده‌اند.

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/ مراجعه کنید. اینجا خواهید آموخت که چگونه گستره‌ی بزرگی از اطّلاعات تخصّصی را از گیت‌هاب بیرون بکشید. اگر حساب کاربریِ گیت‌هاب دارید می‌توانید با داده‌های مربوط به خود یا داده‌های مخزن‌های دیگر کاربران که در دسترس عموم‌اند کار کنید.

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/ مراجعه کنید.

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 این فصل را ببینید. از اطّلاعاتی که پیدا می‌کنید استفاده کنید تا شکل نمودارها را شخصی کنید یا اطّلاعاتی دیگری در آن‌ها قرار دهید و بصری‌سازی‌های خود را بسازید.

6.17 - خلاصه

در این فصل آموختید که چگونه از APIها برای نوشتن برنامه‌های خودمتّکی استفاده کنید که داده‌های مورد نیاز خود را جمع‌آوری و آن‌ها را بصری می‌کنند. از API گیت‌هاب برای بررسی پرستاره‌ترین پروژه‌های پایتون در گیت‌هاب استفاده کردید و نگاهی گذرا به API سایت Hacker News انداختید. یاد گرفتید که چگونه از بسته‌ی Requests برای ارسال خودکار فراخوانی API به گیت‌هاب و روش پردازش نتایج آن استفاده کنید. برخی از تنظیمات پلات‌لی نیز برای شخصی‌سازی بیشتر نمودارهایی که تولید می‌کنند معرّفی شدند.

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


7 - فصل هجدهم

7.1 - شروع کار با جانگو

وب‌سایت‌های امروز، در پشتِ صحنه، نرم‌افزارهای کارب144 ردی غنی و کاملی هستند که مانند برنامه‌های رومیزی کاملاً توسعه‌یافته رفتار می‌کنند. پایتون مجموعه‌ی بزرگ از ابزارها به نام جانگو برای ساختن برنامه‌های کاربردی تحت وب دارد. جانگو یک چهارچوب یا فریم‌ورک برای وب است— مجموعه‌ای از ابزارها که برای کمک به شما در ساختن وب‌سایت‌های تعاملی طرّاحی شده‌اند. در این فصل خواهید آموخت که چگونه از جانگو (https://djangoproject.com/) برای ساختن پروژه‌ای به نام Learning Log استفاده کنید— یک سیستم گزارش کار آنلاین که به شما امکان می‌دهد که دانسته‌هایی که درباره‌ی موضوعات خاصّی کسب کرده‌اید ثبت کنید.

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

جانگو می‌تواند به درخواست‌های صفحه پاسخ دهد و خواندن و نوشتن در پایگاه داده، مدیرّیت کاربران و … را ساده می‌کند. در فصل‌های ۱۹ و۲۰ این پروژه را بهبود می‌بخشید و سپس آن‌ را روی یک سرور مستقر می‌کنید تا خود و دوستانتان بتوانند از آن استفاده کنند.

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 مقایسه کنید. این کار را چند بار انجام دهید که با آن‌چه جانگو هنگام شروع یک پروژه‌ی جدید می‌کند آشنا شوید. پس از آن اگر خواستید پوشه‌ها را پاک کنید.

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 کردن پروژه.

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__() برای جانگو مشخّص می‌کند که چه هنگام نمایش هر موضوع چه چیزی را نشان دهد. از آن‌جایی که طول متن می‌تواند خیلی زیاد باشد از جانگو می‌خواهیم که تنها ۵۰ حرف آن را نمایش دهد. یک سه‌نقطه نیز اضافه می‌کنیم که شفّاف کنیم همیشه تمام مطلب را نمایش نمی‌دهیم.

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 را ذخیره کند.

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

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 و یک صفحه‌ی خانه‌ی ساده برای این پروژه بسازید.

۱۸-۶: صفحه‌ی خانه‌ی پیتزافروشی: به پروژه‌ی پیتزافروشی که در تمرین ۱۸-۴ شروع کردید، به صفحه‌ی خانه اضافه کنید.

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 را خواهید دید. اگر روی آن کلیک کنید به صفحه‌ی موضوعات هدایت خواهید شد.

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 مرور کنید. می‌توانید هنگامی که بر روی پروژه‌های خود کار می‌کنید به آن دوباره مراجعه کنید.

۱۸-۸: صفحه‌های پیتزافروشی: صفحه‌ای به پروژه‌ی پیتزافروشی در تمرین ۱۸-۶ اضافه کنید که نام پیتزاهای موجود را نمایش دهد. سپس نام هر پیتزا را به صفحه‌ای که مخلّفات آن را نشان می‌دهد پیوند دهید. مطمئن شوید که برای ساختن موثّر صفحه‌های خود از ارث‌بری قالب استفاده می‌کنید.

7.9 - خلاصه

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

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

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


8 - فصل نوزدهم

8.1 - فراهم آوردن امکان ورود داده برای کاربران

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

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

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

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

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]

شکل ۱۹-۱: صفحه‌ی اضافه کردن موضوع جدید

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

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 اکنون بیشتر قابلیّت‌های مورد نیاز را دارد. کاربران می‌توانند موضوع‌ها و مطلب‌ها را اضافه کنند و هر مجموعه از مطالب را که بخواهند بخوانند. در بخش بعدی یک سیستم ثبت نام برای کاربران پیاده می‌کنیم تا هر کس بتواند در سایت حساب بسازد و موضوع‌ها و مطالب خود را ایجاد کند.

8.5 - خودآزمایی

۱۹-۱- بلاگ: پروژه‌ی جانگوی جدیدی به نام Blog راه بیندازید. برنامه‌ای به نام blogs در پروژه ایجاد کند و مدلی به نام BlogPost بسازید. مدل باید فیلدهایی مانند title، text و date_added داشته باشد. ابرکاربری برای پروژه بسازید و از طریق سایت مدیر چند نوشته‌ی کوتاه اضافه کنید. صفحه‌ی خانه‌ای بسازید که تمام نوشته‌ها را به ترتیب زمانی نشان می‌دهد.

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

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/.

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 به جانگو می‌گوید که پس از ورود موفّق کاربر، او را به کدام صفحه هدایت کند. در این مورد کاربران را به صفحه‌ی خانه برمی‌گردانیم.

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]

شکل ۱۹-۴: صفحه‌ی ورود

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]

شکل ۱۹-۵: صفحه‌ی خارج‌شده تایید می‌کند که کاربر با موفقیّت خارج شده است.

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--

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

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

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

8.11 - خودآزمایی

۱۹-۲: حساب‌های کاربری بلاگ: یک سیستم احراز هویّت و ثبت نام برای کاربران پروژه‌ی Blog که در تمرین ۱۹-۱ شروع کرده‌اید، بسازید. مطمئن شوید که کاربرانِ واردشده نام کاربری خود را در جایی از صفحه می‌بینند و کاربران ثبت نام نشده پیوندی به صفحه‌ی ثبت نام می‌بینند.

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/ را وارد کنید به صفحه‌ی ورود هدایت می‌شوید. دسترسی شما به هر آدرسی که به طور عمومی در دسترس است امّا حاوی داده‌های خصوصی کاربران است، باید محدود باشد.

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 برقرار می‌کند. اگر کاربری حذف شود تمام موضوع‌های مرتبط با آن نیز پاک خواهد شد.

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 را برای بازسازی ساختار پایگاه داده صادر کنید. شما باید یک ابرکاربر جدید بسازید و تمام داده‌هایتان از دست خواهد رفت.

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 آن با کاربر فعلی مطابقت داشته باشد. چون نحوه‌ی نمایش موضوع‌ها را تغییر نداده‌ایم نیازی نیست که قالب صفحه‌ی موضوعات را دست بزنیم.

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

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 را اعلام می‌کنیم.

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() را صدا می‌کنیم و نمونه‌ی موضوعی که تعریف کرده‌ایم به آن می‌دهیم (۳). حال موضوع تمام داده‌های مورد نیاز را دارد و با موفقّیت ذخیره خواهد شد.

شما باید بتوانید هر تعداد موضوع جدید را برای هر تعداد کاربر که بخواهید ایجاد کنید. هر کاربر تنها به داده‌های خود دسترسی خواهد داشت؛ چه بخواهد داده‌ها را ببیند، چه داده‌ی جدیدی وارد کند یا داده‌های قدیمی را ویرایش کند.

8.18 - خودآزمایی

۱۹-۳- بازآرایی: در views.py دو جا وجود دارد که می‌خواهیم مطمئن شویم کاربری مالک یک موضوع با کاربری فعلی تطابق دارد یا نه. کد این بررسی را در تابعی به نام check_topic_owner() قرار دهید و آن را در جای مناسب صدا کنید.

۱۹-۴- محافظت از new_entry: در حال حاضر یک کاربر می‌تواند با وارد کردن یک URL با شماره‌ی موضوع متعلّق به کاربر دیگری، یک موضوع جدید به دفترچه‌ی آموخته‌های او اضافه کند. با بررسی مرتبط بودن کاربر فعلی با موضوع مطلب پیش از ذخیره‌ی مطلب جدید، از این دستکاری جلوگیری کنید.

۱۹-۵- بلاگ محافظت‌شده: در پروژه‌ی Blog خود مطمئن شوید که هر پست وبلاگ به یک کاربر خاص اختصاص دارد. اطمینان حاصل کنید که تمام پست‌های در دسترس عموم‌اند امّا تنها کاربران ثبت نام کرده می‌توانند پست‌های جدیدی اضافه یا قبلی‌ها را ویرایش کنند. در نمایی که به کاربران اجازه‌ی ویرایش پست‌هایشان را می‌دهد پیش از پردازش فرم مطمئن شوید که کاربران در حال ویرایش پست خود هستند.

8.19 - خلاصه

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

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

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

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

اکنون شما یک پروژه‌ی کاملاً کارآمد دارید که روی ماشین محلی‌تان اجرا می‌شود. در فصل پایانی شکل و شیوه‌ی Learning Log را تغییر می‌دهید تا زیبا به نظر بیاید و پروژه را روی یک سرور قرار می‌دهید تا هر کس که به اینترنت دسترسی دارد بتواند ثبت نام کند و یک حساب کاربری بسازد.


9 - فصل بیستم

9.1 - شکل‌دهی و استقرار

سایت Learning Log اکنون کاملاً کارآمد است امّا هیچ شکل و سیاقی ندارد و روی ماشین محلی شما اجرا می‌شود. در این فصل پروژه را به شکلی ساده امّا حرفه‌ای درمی‌آوریم و سپس آن را روی سرور در حال اجرا مستقر می‌کنیم تا هر کس در هر جای دنیا بتواند حساب کاربری بسازد و از آن استفاده کند.

برای شکل‌دهی به سایت از کتابخانه‌ی Bootstrap استفاده می‌کنیم که مجموعه‌ای از ابزارها برای سر و شکل دادن به برنامه‌های کاربردی تحت وب است تا آن‌ها در افزاره‌های مدرن حرفه‌ای به نظر برسند، از نمایشگرهای تخت بزرگ گرفته تا گوشی‌های هوشمند. برای چنین کاری از برنامه‌ی django-bootstrap4 کمک می‌گیریم تا علاوه بر هدف اصلی خود، در کار کردن با برنامه‌هایی که دیگر توسعه‌دهندگان جانگو ساخته‌اند تجربه کسب کنید.

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

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

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 نشان می‌دهد.

شکل ۲۰-۱- صفحه‌ی خانه با استفاده از بوت‌استرپ

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> در ۷ می‌آید.

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> که تعریف کردیم اضافه کنید و از همان روش‌های شیوه‌دهی که اینجا دیده‌اید استفاده کنید.

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 را در مرورگری باز کنید باید یک نوار پیمایش با ظاهر حرفه‌ای مطابق آن‌چه در شکل ۲۰-۱ نمایش داده شده ببینید. سعی کنید که پنجره را خیلی باریک کنید؛ باید به جای نوار پیمایش یک دکمه قرار بگیرد. اگر روی آن کلیک کنید باید تمام پیوندها را به صورت یک فهرست پایین‌افتادنی ببینید.

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 احتیاجی نیست.

اکنون صفحه‌ی اوّل مانند شکل ۲۰-۱ دیده می‌شود و این پیشرفت محسوسی نسبت پروژه‌ی بدون شکل ما به حساب می‌آید.

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 %} باز می‌کنیم که شکل‌دهی بوت‌استرپ را به دکمه‌ها اعمال می‌کند.

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

شکل ۲۰-۲: صفحه‌ی ورود به کمک بوت‌استرپ شکل گرفته است.

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> درمی‌آوریم تا در صفحه کمی بزرگ‌تر به چشم بیاید (۲) و همین کار را برای پیوند افزودن موضوع جدید انجام می‌دهیم.

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 اعمال کنید.

۲۰-۲- بلاگ خوش‌قیافه: از بوت‌استرپ برای شکل دادن به پروژه‌ی وبلاگی که در فصل ۱۹ ساختید استفاده کنید.

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 سروری ارائه می‌کند که قادر به ارائه‌ی برنامه‌ها در محیطی زنده است.

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 به هروکو می‌گوید که چه نوع رویدادهایی را ثبت کند.

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 هستیم و درخت کاری ما تمیز است. این وضعیّتی است که پس از هر بار ارسال پروژه به هروکو می‌خواهید ببینید.

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 راه بیندازد.

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 جدید منتقل می‌کند.

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

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' برمی‌گردانید. همچنین مراقب باشید هنگامی که کاربران به طور معمول به سایت شما دسترسی دارند این کار را انجام ندهید.

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 . را در ۱ صادر می‌کنیم زیرا چند فایل جدید به پروژه اضافه کرده‌ایم و باید به گیت بگوییم که آن‌ها را ردگیری کند. سپس در ۲ تغییرات را اعمال و در ۳ پروژه‌ی به‌روزشده را به هروکو می‌فرستیم.

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

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 خواهید دید. برای استقرار این تغییر یک سپارش جدید ایجاد کنید و پروژه را به هروکو انتقال دهید.

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 را بازنگری کنید تا هر موضوعی که عمومی بود برای کاربران واردنشده هم قابل دیدن باشد. فراموش نکنید که پایگاه داده‌ی در حال کار را هم پس از ارسال تغییرات به هروکو انتقال دهید.

9.19 - خلاصه

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

شما درباره‌ی قالب‌های بوت‌استرپ آموختید و از قالب Navbar static برای ساختن یک شکل و ظاهر ساده برای Learning Log استفاده کردید. از جامباترون برای متمایز کردن پیام صفحه‌ی خانه استفاده کردید و آموختید که چگونه تمام صفحه‌ها را به یک صورت شکل دهید.

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

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