1 - ساختن و به‌کارگیری یک کلاس

برنامه ‌نویسی شیء‌گرا یکی از موثّ 79 رترین رویکردها برای نوشتن نرم‌افزار است. در برنامه‌نویسی شیءگرا شما کلاس‌هایی می‌سازید که بیانگر مسائل دنیای واقعی هستند و سپس شیءها را بر اساس آن‌ها ایجاد می‌کنید. با ساختنِ یک کلاس رفتاری عمومی را برای دسته‌ای از اشیاء تعیین می‌کنید.

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

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

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

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

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

ساختن کلاس Dog

هر نمونه‌ای که از کلاس Dog ساخته شود یک نام (name) و یک سن (age) ذخیره می‌کند و به هر سگ دو قابلیّت نشستن (sit()) و غلت زدن (roll_over()) خواهد داد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Dog:
    """A simple attempt to model a dog."""

    def __init__(self, name, age):
        """Initialize name and age attributes."""
        self.name = name
        self.age = age

    def sit(self):
        """Simulate a dog sitting in response to a command."""
        print(f"{self.name} is now sitting.")

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

متد __init__()

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

تابع __init__() را طوری تعریف می‌کنیم که سه پارامتر داشته باشد: self، name و age. پارامتر self در تعریفِ متد الزامی است و باید پیش از دیگر پارامترها ذکر شود. این پارامتر باید در تعریفِ متد آورده شود زیرا زمانی‌که پایتون بعداً (هنگامِ ساختن یک نمونه از Dog) این متد را فراخوانی می‌کند، به طور خودکار نشان‌وند self را به آن انتقال می‌دهد. فراخوانی هر کدام از متدهای مربوط به یک نمونه به طور خودکار نشان‌وند self را انتقال می‌دهد که در واقع ارجاعی به خودِ نمونه است و به یک نمونه‌ی مجزّا امکان می‌دهد به مشخّصه‌ها و متدهای یک کلاس دسترسی داشته باشد. در زمانِ ساختنِ نمونه‌ای از Dog، پایتون متد __init__() را از کلاس Dog فراخوانی می‌کند. یک نام و یک سن را به عنوان نشان‌وند به Dog() می‌دهیم؛ self به طور خودکار منتقل می‌شود و نیازی به ذکر آن نیست. هر بار خواستیم نمونه‌ای از کلاس Dog بسازیم، تنها مقدارهای دو پارامتر آخر، name و age را ارائه می‌کنیم.

دو متغیّرِ تعریف‌شده در __init__() هر دو پیش‌وند self دارند. هر متغیّری که پیش‌وندِ self داشته باشد در دسترسِ تمام متدهای کلاس خواهد بود و ما نیز از82 طریقِ نمونه‌هایی که از کلاس ساخته می‌شود به این متغیّرها دسترسی داریم. خط self.name = name مقداری را که به پارامتر name داده شده به متغیّر name نسبت می‌دهد و سپس این متغیّر، به نمونه‌ای که ساخته می‌شود الصاق می‌شود. چنین فرآیندی برای self.age = age هم طی می‌شود. به متغیّری که به این شکل و از طریق نمونه‌ها در دسترس است مشخّصه می‌گویند.

در کلاس Dog دو متد دیگر نیز تعریف شده است: sit() و roll_over(). چون این متدها برای اجرا شدن به اطّلاعات اضافی نیاز ندارند آن را تنها با یک پارامتر، self، تعریف می‌کنیم. نمونه‌هایی که بعداً می‌سازیم به این متدها دسترسی خواهند داشت. به بیانِ دیگر آن‌ها می‌توانند بنشینند یا غلت بزنند. فعلاً sit() و roll_over() کار زیادی نمی‌کنند. آن‌ها تنها پیامی چاپ می‌کنند که سگ در حال نشستن یا غلت زدن است. امّا این مفهوم را می‌توان به وضعیّت‌های واقعی بسط داد: اگر این کلاس بخشی از یک بازی رایانه‌ای واقعی بود، این متدها کدی را شامل می‌شدند که باعث می‌شد سگی انیمیشنی بنشیند یا غلت بزند. اگر این کلاس برای کنترل کردن رباتی نوشته بود، این متدها حرکت‌هایی را طوری هدایت می‌کردند که به نشستن یا غلت زدن سگِ رباتی منجر می‌شوند.

ساختن یک نمونه از یک کلاس

به کلاس به عنوان مجموعه‌ای دستورالعمل‌ها برای چگونگی ساختنِ یک نمونه نگاه کنید. کلاس Dog مجموعه‌ای از دستورالعمل‌هاست که به پایتون می‌گوید چگونه نمونه‌های جداگانه‌ای بسازد که هر کدام بیانگر سگِ خاصّی هستند.

اکنون نمونه‌ای که نماینده‌ی سگ خاصّی باشد می‌سازیم:

1
2
3
4
5
6
7
8
class Dog:
    --snip--

my_dog = Dog('Willie', 6)

print(f"My dog's name is {my_dog.name}.")

print(f"My dog is {my_dog.age} years old.")

کلاس Dog در اینجا همانی است که در مثال قبلی تعریف کردیم. از پایتون می‌خواهیم که سگی به نام 'Willie' با ۶ سال سن بسازد. هنگامی که پایتون این خط را می‌خواند، متد __init__() را در Dog با نشان‌وندهای 'Willie' و 6 فراخوانی می‌کند. متد __init__() نمونه‌ای بیانگر این سگِ خاص می‌سازد و مشخّصه‌های name و age را با توجه به مقادیرِ ارائه شده، تعیین می‌کند. سپس پایتون نمونه‌ای را که بیانگر این سگ است برمی‌گرداند. این نمونه را به متغیّر my_dog نسبت می‌دهیم. قرارداد نام‌گذاری اینجا سودمند است: معمولاً می‌توانیم فرض کنیم که نام‌هایی مانند Dog که حرف اوّل آن‌ها بزرگ است به یک کلاس و آن‌هایی که با حروف کوچک نوشته می‌شوند، مانند my_dog، به یک نمونه از آن کلاس اشاره دارند.

دسترسی به مشخّصه‌ها

برای دسترسی به مشخّصه‌های یک نمونه، از نمادگذاریِ نقطه استفاده می‌کنید. به این ترتیب به مقدار مشخّصه‌ی name از نمونه‌ی my_dog دسترسی پیدا می‌کنیم:

1
my_dog.name

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

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

1
2
3
My dog's name is Willie.

My dog is 6 years old.

فراخوانی متدها

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

1
2
3
4
5
6
7
8
class Dog:
    --snip--

my_dog = Dog('Willie', 6)

my_dog.sit()

my_dog.roll_over()

برای فراخوانیِ یک متد، نامِ نمونه (در این مورد my_dog) و نام متدی را که می‌خواهید فراخوانی کنید بنویسید و بین آن‌ها یک نقطه بگذارید. زمانی که پایتون my_dog.sit() را می‌خواند به دنبال متد sit() در کلاس Dog می‌گردد و کد آن را اجرا می‌کند. پایتون خط my_dog.roll_over() را به همین ترتیب تفسیر می‌کند.

اکنون ویلی آن‌چه را به او فرمان داده‌ایم اجرا می‌کند:

1
2
3
Willie is now sitting.

Willie rolled over!

این نوع نوشتن بسیار سودمند است. زمانی که مشخّصه‌ها و متدها نام‌های گویایی داشته باشند، مانند name، age، sit() و roll_over()، می‌توانیم به سادگی به آن‌چه کد قرار است انجام دهد پی ببریم هر چند قبلاً هرگز آن را ندیده باشیم.

ساختن چند نمونه

می‌توانید هر تعداد نمونه که بخواهید از یک کلاس بسازید. بیاید سگ دومی به نام your_dog بسازیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Dog:
    --snip--

my_dog = Dog('Willie', 6)

your_dog = Dog('Lucy', 3)

print(f"My dog's name is {my_dog.name}.")

print(f"My dog is {my_dog.age} years old.")

my_dog.sit()

print(f"\nYour dog's name is {your_dog.name}.")

print(f"Your dog is {your_dog.age} years old.")

your_dog.sit()

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
My dog's name is Willie.

My dog is 6 years old.

Willie is now sitting.

Your dog's name is Lucy.

Your dog is 3 years old.

Lucy is now sitting.

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

خودآزمایی

۹-۱. رستوران: کلاسی به نام Restaurant بسازید. متد __init__() در Restaurant باید دو مشخّصه را ذخیره کند: restaurant_name و cuisine_type. متدی به نام describe_restaurant() بسازید که این دو دسته اطّلاعات را چاپ کند، متدی هم به نام open_restaurant() بسازید که پیامی برای باز کردنِ رستوران چاپ کند.

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

۹-۲. سه رستوران: از کلاسی که در تمرین ۹-۱ تعریف کردید شروع کنید. سه نمونه‌ی مختلف از این کلاس بسازید و describe_restaurant() را برای هر نمونه فراخوانی کنید.

۹-۳. کاربران: کلاسی به نام User بسازید. دو مشخّصه به نام‌های first_name و last_name بسازید و سپس چند مشخّصه‌ی دیگر که معمولاً در نمایه‌ی یک کاربر ذخیره می‌شود به آن اضافه کنید. متدی به نام describe_user() بسازید که خلاصه‌ای از اطّلاعات یک کاربر را چاپ می‌کند. متد دیگری به نام greet_user() بسازید که یک پیام احوال‌پرسی شخصی برای کاربر چاپ می‌کند.

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

2 - کار کردن با کلاس‌ها و نمونه‌ها

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

کلاس Car

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Car:
    """A simple attempt to represent a car."""

    def __init__(self, make, model, year):
        """Initialize attributes to describe a car."""
        self.make = make
        self.model = model
        self.year = year

    def get_descriptive_name(self):
        """Return a neatly formatted descriptive name."""
        long_name = f"{self.year} {self.manufacturer} {self.model}"
        return long_name.title()

my_new_car = Car('audi', 'a4', 2019)

print(my_new_car.get_descriptive_name())

متد __init__() را با پارامتر ابتدایی self تعریف می‌کنیم. همچنین سه پارامتر دیگر به آن می‌دهیم: make، model و year. متد __init__() این پارامترها را می‌گیرد و آن‌ها را به مشخّصه‌هایی که با نمونه‌های ساخته‌شده از این کلاس در ارتباطند، نسبت می‌دهد. زمانی که نمونه‌ی جدیدی از کلاس Car می‌سازیم باید نام شرکت سازنده، مدل و سال ساخت آن را برای نمونه‌ی خود تعیین کنیم.

متدی به نام get_descriptive_name() تعریف می‌کنیم که year، make و model را در یک رشته قرار می‌دهد که خودرو را با سادگی و آراستگی توصیف می‌کند. این متد ما را از چاپ کردن جداگانه‌ی مقدار هر مشخّصه خلاص می‌کند. برای کار کردن با مقادیر مشخّصه‌های در این متد از self.make، self.model و self.year استفاده می‌کنیم. نمونه‌ای از کلاس Car می‌سازیم و آن را به متغیّر my_new_car نسبت می‌دهیم. سپس get_descriptive_name() را فراخوانی می‌کنید تا نوع خودروی ما را مشخّص کند:

1
2019 Audi A4

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

تعیین کردن مقداری پیش‌فرض برای یک مشخّصه

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

مشخّصه‌ای به نام odometer_reading اضافه می‌کنیم که همیشه در ابتدا مقدار ۰ دارد. همچنین متدی به نام read_odometer() اضافه می‌کنیم که ما را در خواندن مقدار کیلومترشمار هر خودرو کمک می‌کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Car:

    def __init__(self, make, model, year):
        """Initialize attributes to describe a car."""
        self.make = make
        self.model = model
        self.year = year
        self.odometer_reading = 0

    def get_descriptive_name(self):
        --snip

    def read_odometer(self):
        """Print a statement showing the car's mileage."""
        print(f"This car has {self.odometer_reading} miles on it.")

my_new_car = Car('audi', 'a4', 2019)

print(my_new_car.get_descriptive_name())

my_new_car.read_odometer()

این بار پایتون برای ساختن نمونه‌ی جدید متد __init__() را فرامی‌خواند که مقدار سازنده، مدل و سال را مانند مثال قبلی به عنوان مشخّصه ذخیره می‌کند. سپس پایتون مشخّصه‌ای جدید به نام odometer_reading می‌سازد و به آن مقدارِ اوّلیّه‌ی ۰ می‌دهد. همچنین متد جدیدی به نام read_odometer() دارید که خواندن کارکرد خودرو را ساده می‌کند.

کارکرد خودروی ما در ابتدا ۰ است:

1
2
3
2019 Audi A4

This car has 0 miles on it.

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

تغییردادن مقدار مشخّصه‌ها

مقدار یک مشخّصه را می‌توانید به سه روش عوض کنید: تغییر دادن مستقیم از طریق یک نمونه، تعیین کردن مقدار از طریق یک متد یا افزایش دادن مقدار (افزودن مقداری به آن) به کمک یک متد. به هر یک از این رویکردها نگاهی می‌اندازیم.

تغییر دادن مستقیمِ مقدار یک مشخّصه

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Car:
    --snip--

my_new_car = Car('audi', 'a4', 2019)

print(my_new_car.get_descriptive_name())

my_new_car.odometer_reading = 23

my_new_car.read_odometer()

از نماد نقطه برای دسترسی به مشخّصه‌ی odometer_reading خودرو استفاده کرده و مقدار آن را به طور مستقیم تعیین می‌کنیم. این خط به پایتون می‌گوید که نمونه‌ی my_new_car را بگیر، مشخّصه‌ی odometer_reading مربوط به آن را پیدا کن و مقدار آن را برابر با ۲۳ بگذار:

1
2
3
2019 Audi A4

This car has 23 miles on it.

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

تغییر دادن مقدار یک مشخّصه از طریق یک متد

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

مثالِ اینجا متدی به نام update_odometer() را نشان می‌دهد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Car:
    --snip--

    def update_odometer(self, mileage):
        """Set the odometer reading to the given value."""
        self.odometer_reading = mileage

my_new_car = Car('audi', 'a4', 2019)

print(my_new_car.get_descriptive_name())

my_new_car.update_odometer(23)

my_new_car.read_odometer()

تنها تغییرِ ایجادشده در Car افزوده شدنِ update_odometer() است. این متد مقدار کارکرد را گرفته و به مشخّصه‌ی self.odometer_reading نسبت می‌دهد. update_odometer() را فراخوانی می‌کنیم و مقدار ۲۳ را به عنوان نشان‌وند به آن می‌دهیم (که به پارامتر mileage در تعریفِ متد مربوط است.) این متد مقدار کیلومترشمار را ۲۳ می‌کند و read_odometer() این مقدار را چاپ می‌کند:

1
2
3
2019 Audi A4

This car has 23 miles on it.

می‌توانیم متد update_odometer() را طوری گسترش دهیم که در هر بار تغییرِ مقدارِ کیلومترشمار، کاری انجام دهد. اجازه دهید منطقی کوچکی اضافه کنیم تا مطمئن شویم هیچ‌کس مقدار کیلومترشمار را به عقب برنمی‌گرداند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Car:
    --snip--

    def update_odometer(self, mileage):
        """
        Set the odometer reading to the given value.
        Reject the change if it attempts to roll the odometer back.
        """
        if mileage >= self.odometer_reading:
            self.odometer_reading = mileage
        else:
            print("You can't roll back an odometer!")

اکنون update_odometer() پیش از تغییر دادنِ مشخّصه، مقدار جدید را بررسی می‌کند. اگر کارکرد جدید، mileage، بزرگتر یا مساوی کارکرد فعلی (self.odometer_reading) باشد، می‌توانید مقدار فعلی کیلومترشمار را به مقدار جدید به‌روزرسانی کنید. اگر مقدار جدید کمتر از مقدار فعلی باشد، اخطاری دریافت می‌کنید مبنی بر این که نمی‌توانید مقدار کیلومترشمار را به عقب برگردانید.

افزایش مقدار مشخّصه از طریق یک متد

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Car:
    --snip--

    def update_odometer(self, mileage):
        --snip--

    def increment_odometer(self, miles):
        """Add the given amount to the odometer reading."""
        self.odometer_reading += miles

my_used_car = Car('subaru', 'outback', 2015)

print(my_used_car.get_descriptive_name())

my_used_car.update_odometer(23_500)

my_used_car.read_odometer()

my_used_car.increment_odometer(100)

my_used_car.read_odometer()

متد جدید increment_odometer() تعداد مایل پیموده‌شده را گرفته و آن را به self.odometer_reading اضافه می‌کند. یک خودروی کارکرده، my_used_car، ایجاد می‌کنیم. مقدار کیلومترشمار آن را با فراخوانی update_odometer() و انتقال مقدار 23_500 به آن، ۲۳،۵۰۰ می‌گذاریم. سپس increment_odometer() را صدا کرده و مقدار 100 را به آن می‌دهیم تا ۱۰۰ مایل پیموده‌شده بین خریدن و ثبت خودرو را اضافه کند:

1
2
3
4
5
2015 Subaru Outback

This car has 23500 miles on it.

This car has 23600 miles on it.

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

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

خودآزمایی

۹-۴. تعداد مشتری‌ها: از برنامه‌ی خود در تمرین ۹-۱ شروع کنید. مشخّصه‌ای به نام number_served با مقدار پیش‌فرض ۰ اضافه کنید. نمونه‌ای به نام restaurant از این کلاس بسازید. تعداد مشتری‌هایی را که رستوران پذیرای آن‌ها بوده چاپ کنید و سپس با تغییر این مقدار آن را دوباره چاپ کنید.

متدی به نام set_number_served() اضافه کنید که امکان تعیین تعداد مشتری‌ها را فراهم می‌آورد. این متد را با عدد جدیدی فراخوانی کنید و مقدار را دوباره چاپ کنید.

متدی به نام increment_number_served() اضافه کنید که به شما امکان افزایش تعداد مشتری‌ها را می‌دهد. این متد را با هر عددی که دوست دارید فرابخوانید که بیانگر تعداد مهمان‌های پذیرفته‌شده در یک روز کاری باشد.

۹-۵. تعداد تلاش‌ها برای ورود: مشخّصه‌ای به نام login_attempts به کلاس User در تمرین ۹-۳ اضافه کنید. متدی به نام increment_login_attempts() بنویسید که مقدار login_attempts را یکی افزایش می‌دهد. متد دیگری به نام reset_login_attempts() بنویسید که مقدار login_attempts را صفر می‌کند.

نمونه‌ای از کلاس User بسازید و increment_login_attempts() را چندین بار صدا کنید. مقدار login_attempts را چاپ کنید تا مطمئن شوید مقدار آن افزایش یافته است و سپس reset_login_attempts() را فراخوانی کنید. مقدار login_attempts را بار دیگر چاپ کنید تا مطمئن شوید مقدار آن صفر شده است.

3 - ارث‌بری

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

متد __init__() برای کلاس فرزند

زمانی که کلاس جدیدی را بر پایه‌ی یک کلاس موجود می‌نویسید، اغلب می‌خواهید متد __init__() کلاس والد را فراخوانی کنید. این کار موجب مقداردهی اوّلیّه‌ی تمام مشخّصه‌های تعریف‌شده در متد __init__() والد خواهد شد و آن‌ها را در کلاس فرزند در دسترس قرار می‌دهد.

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

از ساختن نسخه‌ی ساده‌ای از کلاس ElectricCar شروع می‌کنیم که رفتار آن مشابه کلاس Car است:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Car:
    """A simple attempt to represent a car."""

    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
        self.odometer_reading = 0

    def get_descriptive_name(self):
        long_name = f"{self.year} {self.manufacturer} {self.model}"
        return long_name.title()

    def read_odometer(self):
        print(f"This car has {self.odometer_reading} miles on it.")

    def update_odometer(self, mileage):
        if mileage >= self.odometer_reading:
            self.odometer_reading = mileage
        else:
            print("You can't roll back an odometer!")

    def increment_odometer(self, miles):
        self.odometer_reading += miles

class ElectricCar(Car):
    """Represent aspects of a car, specific to electric vehicles."""

    def __init__(self, make, model, year):
        """Initialize attributes of the parent class."""
        super().__init__(make, model, year)

my_tesla = ElectricCar('tesla', 'model s', 2019)

print(my_tesla.get_descriptive_name())

کلاس والد باید در فایل فعلی و پیش از تعریف کلاس فرزند در همان فایل ظاهر شود. کلاس فرزند، ElectricCar را تعریف می‌کنیم. نام کلاس والد باید هنگام تعریف کلاسِ فرزند، داخلِ پرانتز آورده شود. متد __init__() اطّلاعات لازم را برای ساختن یک نمونه از Car می‌گیرد.

تابع super() تابعی ویژه است که امکان فراخوانی متدی را از کلاس 87 86 ی87 ‌آورد. این خط به پایتون می‌گوید که متد __init__() را از Car فراخوانی کند، تا به یک نمونه‌ی ElectricCar تمام مشخّصه‌های تعریف‌شده در آن متد داده شود. نام super از قراردادی می‌آید که در آن به کلاس والد ابرکلاس و به کلاس فرزند زیرکلاس گویند.

اکنون پس از ساختنِ یک خودروی الکتریکی بر اساس اطّلاعاتِ خودروی معمولی، بررسی می‌کنیم که آیا ارث‌بری به خوبی کار می‌کند یا نه. نمونه‌ای از کلاس ElectricCar می‌سازیم و آن را به my_tesla نسبت می‌دهیم. این خط متد __init__() تعریف‌شده در ElectricCar را فراخوانی می‌کند که خود به پایتون می‌گوید که متد __init__() تعریف‌شده در کلاس والد Car را صدا کند. نشان‌وندهای 'tesla'، 'model s' و 2019 را ارائه می‌کنیم.

به جز __init__() هیچ مشخّصه یا متدی وجود ندارد که مخصوصِ یک خودروی الکتریکی باشد. تا اینجا فقط مطمئن می‌شویم که خودروی الکتریکی رفتارهای متناسب Car را دارد یا نه:

1
2019 Tesla Model S

نمونه‌ی ElectricCar درست مانند یک نمونه‌ی Car کار می‌کند، بنابراین اکنون تعریف کردنِ متدها و مشخّصه‌های مختصّ خودروهای الکتریکی را آغاز می‌کنیم.

تعریف کردن مشخّصه‌ها و متدهای کلاس فرزند

به محضِ ساختن یک کلاس فرزند که از کلاس والدی ارث می‌برد، می‌توانید هر مشخّصه یا متد جدیدی را که برای تمایز ایجاد کردن بین کلاس فرزند و والد نیاز است، اضافه کنید.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Car:
    --snip--

class ElectricCar(Car):
    """Represent aspects of a car, specific to electric vehicles."""

    def __init__(self, make, model, year):
        """
        Initialize attributes of the parent class.
        Then initialize attributes specific to an electric car.
        """
        super().__init__(make, model, year)
        self.battery_size = 75

    def describe_battery(self):
        """Print a statement describing the battery size."""
        print(f"This car has a {self.battery_size}-kWh battery.")

my_tesla = ElectricCar('tesla', 'model s', 2019)

print(my_tesla.get_descriptive_name())

my_tesla.describe_battery()

مشخّصه‌ی جدید self.battery_size را افزوده و مقدار اوّلیّه‌ی آن را ۷۵ گذاشته‌ایم. این مشخّصه به تمام نمونه‌هایی که از کلاس ElectricCar ساخته می‌شوند مربوط خواهد بود امّا در هیچ نمونه‌ای از Car وجود نخواهد داشت. همچنین متدی به نام describe_battery() اضافه می‌کنیم که اطّلاعات مربوط به باتری را چاپ می‌کند. زمانی که این متد را فراخوانی می‌کنیم توضیحی دریافت می‌کنیم که مختصّ یک خودروی الکتریکی است:

1
2
3
2019 Tesla Model S

This car has a 75-kWh battery.

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

بازتعریف 88 ِ متدهای کلاس والد

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

فرض کنید کلاس Car متدی به نام fill_gas_tank() دارد. این متد برای خودروهای تماماً الکتریکی بی‌معناست بنابراین ممکن است بخواهید آن را بازتعریف کنید. یک راه برای این کار چنین است:

1
2
3
4
5
6
class ElectricCar(Car):
    *--snip--*

    def fill_gas_tank(self):
        """Electric cars don't have gas tanks."""
        print("This car doesn't need a gas tank!")

اکنون اگر کسی بخواهد متد fill_gas_tank() را برای یک خودروی الکتریکی فراخوانی کند، پایتون از متد fill_gas_tank() در کلاس Car صرف نظر کرده و به جای آن این کد را اجرا می‌کند. هنگامِ استفاده از ارث‌بری می‌توانید کاری کنید که کلاس فرزند چیزهایی را که از کلاس والد نیاز دارید حفظ کند و هر آن‌چه را نمی‌خواهید بازتعریف کنید.

نمونه‌ها در قالب مشخّصه‌ها

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Car:
    *--snip--*

class Battery:
    """A simple attempt to model a battery for an electric car."""

    def __init__(self, battery_size=75):
        """Initialize the battery's attributes."""
        self.battery_size = battery_size

    def describe_battery(self):
        """Print a statement describing the battery size."""
        print(f"This car has a {self.battery_size}-kWh battery.")

class ElectricCar(Car):
    """Represent aspects of a car, specific to electric vehicles."""

    def __init__(self, make, model, year):
        """
        Initialize attributes of the parent class.
        Then initialize attributes specific to an electric car.
        """
        super().__init__(make, model, year)
        self.battery = Battery()

my_tesla = ElectricCar('tesla', 'model s', 2019)

print(my_tesla.get_descriptive_name())

my_tesla.battery.describe_battery()

کلاس جدیدی به نام Battery تعریف می‌کنیم که از هیچ کلاسی چیزی به ارث نمی‌برد. متد __init__() علاوه بر self یک پارامتر دارد، battery_size. این پارامتر اختیاری است و اگر مقداری برای آن ارائه نشود ظرفیّت باتری را برابر ۷۵ می‌گذارد. متد describe_battery() هم به این کلاس منتقل شده است.

اکنون به کلاس ElectricCar مشخّصه‌ای به نام self.battery اضافه می‌کنیم. این خط به پایتون می‌گوید که نمونه‌ای جدید از Battery بساز (که چون مقداری برای آن تعیین نکرده‌ایم ظرفیّت پیش‌فرض آن ۷۵ است) و این نمونه‌ را به مشخّصه‌ی self.battery نسبت بده. این اتّفاق هر بار که متد __init__() فراخوانده می‌شود، رخ می‌دهد؛ اکنون هر نمونه‌ی ElectricCar یک نمونه‌ی Battery هم دارد که به طور خودکار ساخته می‌شود.

یک خودروی الکتریکی ساخته و آن را به متغیّر my_tesla نسبت می‌دهیم. زمانی که می‌خواهیم درباره‌ی باتری توضیح دهیم، باید از طریق مشخّصه‌ی battery خودرو عمل کنیم:

1
my_tesla.battery.describe_battery()

با این خط پایتون می‌فهمد که باید به نمونه‌ی my_tesla نگاه کند، مشخّصه‌ی battery آن را بیاید و متد describe_battery() را اجرا کند که به نمونه‌ی Battery ذخیره‌شده در این مشخّصه مربوط است.

خروجی عیناً مشابه آن چیزی است که قبلاً دیده‌ایم:

1
2
3
2019 Tesla Model S

This car has a 75-kWh battery.

این به نظر زیاده‌کاری می‌آید امّا اکنون می‌توانید هر چه‌قدر جزئیات که بخواهیم به باتری اضافه کنیم بدون این که کلاس ElectricCar را شلوغ و درهم‌ریخته کنیم. حالا متد دیگری به Battery اضافه می‌کنیم که مسافت قابل طی کردن با این ظرفیّت باتری را گزارش می‌کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Car:
    --snip--

class Battery:
    --snip--

    def get_range(self):
        """Print a statement about the range this battery provides."""
        if self.battery_size == 75:
            range = 260
        elif self.battery_size == 100:
            range = 315

        print(f"This car can go about {range} miles on a full charge.")

class ElectricCar(Car):
    --snip--

my_tesla = ElectricCar('tesla', 'model s', 2019)

print(my_tesla.get_descriptive_name())

my_tesla.battery.describe_battery()

my_tesla.battery.get_range()

متد جدید get_range() تحلیل ساده‌ای انجام می‌دهد. اگر ظرفیّت باتری ۷۵ کیلووات‌ساعت باشد، فاصله‌ی قابل پیمایش را ۲۶۰ مایل و اگر ۱۰۰ کیلووات‌ساعت باشد برابر با ۳۱۵ مایل می‌گذارد. سپس مقدار را گزارش می‌کند. هرگاه بخواهیم این متد را فراخوانی کنیم، باید دوباره آن را از طریق مشخّصه‌ی battery خودرو صدا کنیم.

خروجی بسته به ظرفیّت باتری، مسافتی را که خودرو می‌تواند طی کند گزارش می‌کند:

1
2
3
4
5
2019 Tesla Model S

This car has a 75-kWh battery.

This car can go about 260 miles on a full charge.

مدل کردن اشیای دنیای واقعی

همزمان که مدل کردن چیزهای پیچیده‌تر مانند خودروهای الکتریکی را شروع می‌کنید، با پرسش‌های جالب‌تری سر و کار خواهید داشت. آیا مسافت قابل پیمایش ویژگیِ خودروی الکتریکی است، یا باتری یا خودرو؟ اگر تنها یک خودرو را توصیف کنیم احتمالاً برقرار کردن ارتباط بین متد get_range() و کلاس Battery خوب است. امّا اگر بخواهیم کل خطّ تولید یک کارخانه را توصیف کنیم، ممکن است get_range() را به کلاس ElectricCar منتقل کنیم. متد get_range() پیش از محاسبه‌ی مسافت همچنان ظرفیّت باتری را در نظر می‌گیرد امّا مسافت قابل پیمایش یک نوع خاصّ خودرو را که با آن در ارتباط است گزارش می‌دهد. به جای این کار، از سوی دیگر، می‌توانیم ارتباط را بین متد get_range() و باتری برقرار کنیم امّا پارامتری مانند car_model را به آن بدهیم. در نتیجه متد get_range() مسافت را بر اساس ظرفیّت باتری و مدل خودرو گزارش خواهد کرد.

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

خودآزمایی

۹-۶. دکه‌ی بستنی‌فروشی: دکه‌ی بستنی‌فروشی را نوعی رستوران در نظر بگیرید. کلاسی به نام IceCreamStand تعریف کنید که از کلاس Restaurant که در تمرین ۹-۱ یا تمرین ۹-۴ ارث می‌برد. هر دو کلاس کار می‌کنند؛ موردی را انتخاب کنید که بیشتر دوست دارید. مشخّصه‌ای به نام flavors اضافه کنید که لیستی از طعم‌های بستنی را ذخیره کند. متدی بنویسید که این طعم‌ها را نمایش دهد. نمونه‌ای از IceCreamStand بسازید و این متد را فراخوانی کنید.

۹-۷. مدیر: مدیر سیستم نوع خاصّی کاربر است. کلاسی به نام Admin بنویسید که از کلاس User (تمرین ۹-۳ یا تمرین ۹-۵) ارث ببرد. مشخّصه‌ای به نام privileges (امتیازات) اضافه کنید که لیستی از رشته‌ها مانند "can add post"، "can delete post"، "can ban user" و … باشد. متدی به نام show_privileges() بنویسید که مجموعه‌ی امتیازات مدیر را فهرست کند. نمونه‌ای از Admin بسازید و متد خود را فرابخوانید.

۹-۸. امتیازات: کلاس جداگانه‌ای به نام Privileges بسازید. این کلاس باید یک مشخّصه داشته باشد، privileges، که لیستی از رشته‌های شرح داده‌شده در تمرین ۹-۷ در آن ذخیره شود. متد show_privileges() را به این کلاس منتقل کنید. نمونه‌ای از Privileges را به عنوان یک مشخّصه در کلاس Admin تعریف کنید. نمونه‌ای جدید از Admin بسازید و از متد خود برای نمایش امتیازات مدیر بهره بگیرید.

۹-۹. ارتقای باتری: از نسخه‌ی نهایی electric_car.py در این بخش استفاده کنید. متدی به نام upgrade_battery() به کلاس Battery اضافه کنید. این متد باید ظرفّیت باتری را بررسی کند و آن را برابر با ۱۰۰ بگذارد (البته اگر قبلاً این مقدار را نداشته است.) خودرویی الکتریکی با ظرفیّت باتری پیش‌فرض بسازید، یک بار get_range() را فراخوانی کنید. پس از ارتقای باتری به کمک متد خود، بار دیگر get_range() را صدا کنید. باید افزایشی در مسافت قابل پیمایش خودرو مشاهده کنید.

4 - وارد کردن کلاس‌ها

بیایید ماژولی بسازیم که تنها شامل کلاس Car باشد. اینجا به یک مشکل ظریف در نام‌گذاری برمی‌خوریم: ما قبلاً در این فصل فایلی به نام car.py داشته‌ایم امّا این ماژول هم باید car.py نام‌گذاری شود زیرا دربردارنده‌ی کدی بیانگر خودروست. این مشکل نامگذاری را این‌گونه حل می‌کنیم: کلاس Car را در ماژولی به نام car.py ذخیره می‌کنیم، سپس آن را به جای فایل car.py قبلی می‌گذاریم. از این به بعد هر برنامه‌ای که از این ماژول استفاده می‌کند به نام فایل ویژه‌تری نیاز دارد، مثلاً my_car.py. در اینجا، در car.py تنها باید کد کلاس Car را ببینیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
"""A class that can be used to represent a car."""

class Car:
    """A simple attempt to represent a car."""

    def __init__(self, make, model, year):
        """Initialize attributes to describe a car."""
        self.make = make
        self.model = model
        self.year = year
        self.odometer_reading = 0

    def get_descriptive_name(self):
        """Return a neatly formatted descriptive name."""
        long_name = f"{self.year} {self.manufacturer} {self.model}"
        return long_name.title()

    def read_odometer(self):
        """Print a statement showing the car's mileage."""
        print(f"This car has {self.odometer_reading} miles on it.")

    def update_odometer(self, mileage):
        """
        Set the odometer reading to the given value.
        Reject the change if it attempts to roll the odometer back.
        """
        if mileage >= self.odometer_reading:
            self.odometer_reading = mileage
        else:
            print("You can't roll back an odometer!")

    def increment_odometer(self, miles):
        """Add the given amount to the odometer reading."""
        self.odometer_reading += miles

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

اکنون فایلی جداگانه به نام my_car.py می‌سازیم. این فایل کلاس Car را وارد کرده و نمونه‌ای از آن را می‌سازد:

1
2
3
4
5
6
7
8
9
from car import Car

my_new_car = Car('audi', 'a4', 2019)

print(my_new_car.get_descriptive_name())

my_new_car.odometer_reading = 23

my_new_car.read_odometer()

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

1
2
3
2019 Audi A4

This car has 23 miles on it.

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

ذخیره کردن چند کلاس در یک ماژول

شما می‌توانید هر تعداد کلاس که نیاز دارید در یک ماژول ذخیره کنید، با این حال کلاس‌های هر ماژول باید به نحوی به یکدیگر مربوط باشند. کلاس‌های Battery و ElectricCar هر دو نمایانگر خودروها هستند بنابراین آن‌ها را به ماژول car.py اضافه می‌کنیم.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
"""A set of classes used to represent gas and electric cars."""

class Car:
    --snip--

class Battery:
    """A simple attempt to model a battery for an electric car."""

    def __init__(self, battery_size=70):
        """Initialize the battery's attributes."""
        self.battery_size = battery_size

    def describe_battery(self):
        """Print a statement describing the battery size."""
        print(f"This car has a {self.battery_size}-kWh battery.")

    def get_range(self):
        """Print a statement about the range this battery provides."""
        if self.battery_size == 75:
            range = 260
        elif self.battery_size == 100:
            range = 315

        print(f"This car can go about {range} miles on a full charge.")

class ElectricCar(Car):
    """Models aspects of a car, specific to electric vehicles."""

    def __init__(self, make, model, year):
        """
        Initialize attributes of the parent class.
        Then initialize attributes specific to an electric car.
        """
        super().__init__(make, model, year)
        self.battery = Battery()

اکنون می‌توانیم فایل جدیدی به نام my_electric_car.py بسازیم، کلاس ElectricCar را وارد کنیم و یک خودروی الکتریکی بسازیم:

1
2
3
4
5
6
7
8
9
from car import ElectricCar

my_tesla = ElectricCar('tesla', 'model s', 2019)

print(my_tesla.get_descriptive_name())

my_tesla.battery.describe_battery()

my_tesla.battery.get_range()

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

1
2
3
4
5
2019 Tesla Model S

This car has a 75-kWh battery.

This car can go about 260 miles on a full charge.

وارد کردن چند کلاس از یک ماژول

شما می‌توانید هر تعداد کلاسی که نیاز داشتید در یک فایل برنامه وارد کنید. اگر بخواهیم در یک فایل یک خودروی معمولی و یک خودروی الکتریکی بسازیم، باید هر دو کلاس Car و ElectricCar را وارد کنیم:

1
2
3
4
5
6
7
8
9
from car import Car, ElectricCar

my_beetle = Car('volkswagen', 'beetle', 2019)

print(my_beetle.get_descriptive_name())

my_tesla = ElectricCar('tesla', 'roadster', 2019)

print(my_tesla.get_descriptive_name())

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

در این مثال یک خودروی معمولی Volkswagen Beetle و یک خودروی الکتریکی Tesla Roadster می‌سازیم:

1
2
3
2019 Volkswagen Beetle

2019 Tesla Roadster

وارد کردن کلّ یک ماژول

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

وارد کلّ ماژول car و ساختن یک خودروی معمولی و الکتریکی به این شکل است:

1
2
3
4
5
6
7
8
9
import car

my_beetle = car.Car('volkswagen', 'beetle', 2019)

print(my_beetle.get_descriptive_name())

my_tesla = car.ElectricCar('tesla', 'roadster', 2019)

print(my_tesla.get_descriptive_name())

کلّ ماژول car را وارد می‌کنیم. پس از آن از طریق دستور module_name.ClassName به کلاس‌های آن دسترسی پیدا می‌کنیم. دوباره یک Volkswagen Beetle و یک Tesla Roadster می‌سازیم.

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

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

1
from module_name import *

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

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

وارد کردن یک ماژول در ماژول دیگر

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

برای مثال کلاس Car را در یک ماژول و کلاس‌های ElectricCar و Battery را در ماژولی جدا ذخیره می‌کنیم. ماژول جدیدی به نام electric_car.py می‌سازیم و آن را جایگزینِ فایل electric_car.py که پیش از این ساخته بودیم می‌کنیم و تنها دو کلاس Battery و ElectricCar را درون آن کپی می‌کنیم:

1
2
3
4
5
6
7
8
9
"""A set of classes that can be used to represent electric cars."""

from car import Car

class Battery:
    --snip--

class ElectricCar(Car):
    --snip--

کلاس ElectricCar نیاز به دسترسی به کلاس والد خود Car دارد، بنابراین Car را مستقیماً در ماژول وارد می‌کنیم. اگر این خط را فراموش کنیم، هنگام وارد کردن ماژول electric_car اعلام خطا می‌کند. همچنین باید ماژول Car را به‌روزرسانی کنیم تا تنها شامل کلاس Car باشد:

1
2
3
4
"""A class that can be used to represent a car."""

class Car:
    --snip--

اکنون می‌توانیم از هر ماژول جداگانه کلاس‌هایی را وارد کنیم و هر نوع خودرو که خواستیم بسازیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from car import Car

from electric_car import ElectricCar

my_beetle = Car('volkswagen', 'beetle', 2019)

print(my_beetle.get_descriptive_name())

my_tesla = ElectricCar('tesla', 'roadster', 2019)

print(my_tesla.get_descriptive_name())

Car را از ماژول خود و ElectricCar را از ماژول مربوطه‌اش وارد می‌کنیم. سپس یک خودروی معمولی و یک خودروی الکتریکی می‌سازیم. هر دو نوع خودرو به درستی ساخته شده‌اند:

1
2
3
2019 Volkswagen Beetle

2019 Tesla Roadster

استفاده از نام‌های مستعار

چنان‌که در فصل ۸ دیدید هنگام استفاده از ماژول‌ها نام‌های مستعار می‌توانند به مرتب‌سازی کد پروژه‌های شما کمک کنند. می‌توانید از نام‌های مستعار هنگام وارد کردن کلاس‌ها هم استفاده کنید.

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

1
from electric_car import ElectricCar as EC

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

1
my_tesla = EC('tesla', 'roadster', 2019)

یافتن یک جریانِ کاری برای خود

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

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

خودآزمایی

۹-۱۰. رستوران‌های واردشده: از آخرین کلاس Restaurant خود استفاده کنید و آن را در یک ماژول ذخیره کنید. فایلی جدا بسازید که Restaurant را وارد می‌کند. نمونه‌ای از این کلاس بسازید و از متدهای آن استفاده کنید تا نشان دهید که عبارت import به درستی کار می‌کند.

۹-۱۱. کلاس مدیر واردشده: از کار خود در تمرین ۹-۸ شروع کنید. کلاس‌های User، Privileges و Admin را در یک ماژول ذخیره کنید. فایلی جدا بسازید، نمونه‌ای از Admin ایجاد کنید و show_privileges() را فراخوانی کنید تا معلوم شود همه چیز درست کار می‌کند.

۹-۱۲. چند ماژول: کلاس User را در یک ماژول و دو کلاس Privileges و Admin را در ماژولی دیگر ذخیره کنید. در فایلی جدا نمونه‌ای از Admin بسازید و show_privileges() را صدا کنید تا نشان دهید که همه چیز همچنان درست کار می‌کند.

5 - کتابخانه‌ی استاندارد پایتون و شکل‌دهی به کلاس‌ها

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

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

اینجا نحوه‌ی تولید عددی تصادفی بین ۱ و ۶ را می‌بینیم:

1
2
3
from random import randint

randint(1, 6)
1
3

تابع سودمند دیگر choice() است. این تابع یک لیست یا چندتایی را گرفته و یکی از اعضای آن را به صورت تصادفی برمی‌گرداند:

1
2
3
4
5
6
7
from random import choice

players = ['charles', 'martina', 'michael', 'florence', 'eli']

first_up = choice(players)

first_up
1
'florence'

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

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

خودآزمایی

۹-۱۳. تاس: کلاسی به نام Die بسازید که مشخّصه‌ای به نام sides (وجه‌ها) دارد و مقدار اوّلیّه‌ی آن ۶ است. متدی به نام roll_die() بنویسید که عددی تصادفی بین ۱ و تعداد وجه‌های تاس چاپ می‌کند. تاسی شش‌وجهی بسازید و آن را ۱۰ بار بیندازید.

یک تاس ده‌وجهی و بیست‌وجهی بسازید و هر کدام را ۱۰ بار بیندازید.

۹-۱۴. بخت‌آزمایی: لیست یا چندتایی‌ای از ۱۰ عدد و ۵ حرف بسازید. چهار عدد یا حرف را به طور تصادفی از لیست انتخاب کنید و پیامی چاپ کنید که شماره‌ی بلیت هرکس به این چهار حرف یا عدد بخورد برنده‌ی جایزه‌ای شده است.

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

۹-۱۶. ماژول هفته‌ی پایتون: یکی از منابع عالی برای کاویدن کتابخانه‌ی استاندارد پایتون سایتی به نام Python Module of the Week است. به نشانی https://pymotw.com/ بروید و به فهرست آن نگاهی بیندازید. ماژولی را پیدا کنید که به نظرتان جالب می‌آید و درباره‌ی آن بخوانید، شاید بهتر باشد از ماژول random شروع کنید.

شکل‌دادن به کلاس‌ها

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

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

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

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

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

خلاصه

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

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

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