ارث‌بری

همیشه نیاز نیست نوشتنِ یک کلاس 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() را صدا کنید. باید افزایشی در مسافت قابل پیمایش خودرو مشاهده کنید.