برنامه‌نویسی پایتون فهرست مطالب
فصل ۲۳

وراثت و چندریختی

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

در این فصل می‌خوانید
  1. اولین وراثت
  2. بازنویسی متد
  3. تابع super
  4. فرزندی که صفت اضافه می‌کند
  5. isinstance و issubclass
  6. وراثت چندسطحی و MRO
  7. وراثت چندگانه
  8. چندریختی
  9. تایپ اردکی
  10. کلاس انتزاعی
  11. نمونه کامل: اشکال هندسی
  12. وراثت یا ترکیب

۱. اولین وراثت

فرض کنید کلاس Dog و Cat دارید و هر دو باید بتوانند بدوند. به جای نوشتن دو بار همان کد، یک کلاس والد می‌سازیم و هر دو از آن ارث می‌برند.

# A child class receives everything the parent has, and may add more.
class Animal:
    def run(self):
        print('Running...')


class Dog(Animal):          # Dog inherits from Animal
    def speak(self):
        print('Bark')


if __name__ == '__main__':
    dog = Dog()
    dog.speak()             # its own method
    dog.run()               # inherited from Animal

    # The parent knows nothing about the child:
    animal = Animal()
    animal.run()
    # animal.speak()   ->  AttributeError

اولین وراثت: نمونه 01_first_inheritance.py

شرح مثال

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

# When the child defines a method that already exists in the parent,
# the child version wins. This is called OVERRIDING.
class Animal:
    def speak(self):
        print('Some generic sound')

    def describe(self):
        print('I am an animal and I say:')
        self.speak()          # calls the CHILD version when self is a child


class Dog(Animal):
    def speak(self):
        print('Bark')


class Cat(Animal):
    def speak(self):
        print('Meow')


if __name__ == '__main__':
    Animal().describe()
    Dog().describe()
    Cat().describe()

بازنویسی: نمونه 02_override.py

I am an animal and I say:
Some generic sound
I am an animal and I say:
Bark
I am an animal and I say:
Meow
نکته کلیدی این مثال

متد describe فقط در Animal نوشته شده و داخلش self.speak() را صدا می‌زند. ولی وقتی روی یک سگ صدا زده شود، نسخه سگ اجرا می‌شود. چون self همان شیء واقعی است و پایتون همیشه از کلاس واقعی شیء شروع به گشتن می‌کند. این پایه چندریختی است.

۳. تابع super

# super() gives access to the parent class.
# It is used above all to run the parent __init__.
class Human:
    def __init__(self, name: str = 'Adam'):
        self.name = name

    def greet(self):
        print(f'Hello {self.name}!')


class Student(Human):
    def __init__(self, name: str = 'Adam'):
        super().__init__(name)      # let Human set self.name
        # extra work of the child would go here

    def learn(self):
        print('I try to learn!')

تابع super: نمونه 03_super_init.py

super() یعنی «کلاس والد من». پرکاربردترین استفاده‌اش صدا زدن سازنده والد است. اگر یادتان برود، صفت‌هایی که والد می‌ساخت اصلاً ساخته نمی‌شوند و بعداً AttributeError می‌گیرید.

۴. فرزندی که صفت اضافه می‌کند

# The usual pattern: the child calls super().__init__() and then adds its own
# attributes.
class Person:
    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

    def describe(self) -> str:
        return f'{self.name}, {self.age} years old'


class Employee(Person):
    def __init__(self, name: str, age: int, salary: float) -> None:
        super().__init__(name, age)     # name and age handled by Person
        self.salary = salary            # only Employee has this

    def describe(self) -> str:
        # Reusing the parent version instead of repeating it.
        return f'{super().describe()}, salary {self.salary:,.0f}'


class Manager(Employee):
    def __init__(self, name: str, age: int, salary: float, team: list[str]) -> None:
        super().__init__(name, age, salary)
        self.team = team

    def describe(self) -> str:
        return f'{super().describe()}, manages {len(self.team)} people'


if __name__ == '__main__':
    print(Person('Ali', 30).describe())
    print(Employee('Sara', 28, 30_000_000).describe())
    print(Manager('Reza', 45, 60_000_000, ['Ali', 'Sara']).describe())

افزودن صفت: نمونه 04_child_adds_attributes.py

Ali, 30 years old
Sara, 28 years old, salary 30,000,000
Reza, 45 years old, salary 60,000,000, manages 2 people

الگوی استاندارد را ببینید: super().__init__(...) اول، بعد صفت‌های خودمان. و در describe هم به جای تکرار متن والد، super().describe() صدا زده شده و چیزی به آن اضافه شده است. سه سطح وراثت اینجا زنجیره‌ای کار می‌کنند.

۵. isinstance و issubclass

dog = Dog()

# isinstance() also answers True for the parents.
print(isinstance(dog, Dog))       # True
print(isinstance(dog, Animal))    # True
print(isinstance(dog, Cat))       # False

# type() is exact, isinstance() is not - prefer isinstance().
print(type(dog) is Dog)           # True
print(type(dog) is Animal)        # False

# issubclass() works on the classes themselves.
print(issubclass(Dog, Animal))    # True
print(issubclass(Animal, Dog))    # False

# Everything in Python inherits from object.
print(issubclass(Animal, object)) # True

بررسی نوع: نمونه 05_isinstance.py

همیشه isinstance را به type(x) is Y ترجیح دهید، چون وراثت را در نظر می‌گیرد. جمله آخر مهم است: در پایتون همه چیز در نهایت از object ارث می‌برد، حتی عدد و رشته.

۶. وراثت چندسطحی و MRO

# Three levels of inheritance. Every 'do' calls the one above it with super().
class A:
    def do(self):
        print('A')


class B(A):
    def do(self):
        print('B')
        super().do()


class C(B):
    def do(self):
        print('C')
        super().do()


if __name__ == '__main__':
    c = C()
    c.do()          # prints C, then B, then A

    # The MRO (Method Resolution Order) is the exact list of classes Python
    # searches, in order.
    print(C.__mro__)

MRO: نمونه 06_multilevel_mro.py

C
B
A
(<class 'C'>, <class 'B'>, <class 'A'>, <class 'object'>)

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

۷. وراثت چندگانه

# A class may have several parents. super() then follows the MRO,
# not simply "the first parent".
class Logger:
    def __init__(self):
        print('Logger init')
        super().__init__()          # keep the chain going!

    def log(self, message='...'):
        print('Logging:', message)


class Database:
    def __init__(self):
        print('Database init')
        super().__init__()


class App(Logger, Database):
    def __init__(self):
        print('App init')
        super().__init__()          # starts the chain: Logger, then Database


if __name__ == '__main__':
    app = App()
    app.log('the application started')
    print(App.__mro__)

وراثت چندگانه: نمونه 07_multiple_inheritance.py

با احتیاط

وراثت چندگانه قدرتمند است ولی به‌سرعت گیج‌کننده می‌شود. اگر همه کلاس‌ها super().__init__() را صدا نزنند، زنجیره قطع می‌شود و بعضی سازنده‌ها اجرا نمی‌شوند. در عمل، یا از ترکیب استفاده کنید یا از میکسین: کلاسی مثل Logger که فقط رفتار اضافه می‌کند و هیچ داده‌ای ندارد.

۸. چندریختی

# Polymorphism: the same call gives a different result depending on the object.
class Shape:
    def area(self) -> float:
        raise NotImplementedError('every shape must implement area()')

    def name(self) -> str:
        return type(self).__name__


class Rectangle(Shape):
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height


class Circle(Shape):
    def __init__(self, radius: float) -> None:
        self.radius = radius

    def area(self) -> float:
        return 3.14159 * self.radius ** 2


class Square(Rectangle):
    def __init__(self, side: float) -> None:
        super().__init__(side, side)


if __name__ == '__main__':
    shapes: list[Shape] = [Rectangle(3, 4), Circle(5), Square(2)]

    # One single loop handles every kind of shape.
    for shape in shapes:
        print(f'{shape.name():<10} area = {shape.area():.2f}')

    print('Total area:', sum(shape.area() for shape in shapes))

چندریختی: نمونه 08_polymorphism.py

Rectangle  area = 12.00
Circle     area = 78.54
Square     area = 4.00
Total area: 94.53975

چرا این مهم است

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

۹. تایپ اردکی

# Duck typing: "If it walks like a duck and quacks like a duck, it is a duck."
# Python does not care about the class; it only cares that the method exists.
class Dog:
    def speak(self):
        print('Bark')


class Cat:
    def speak(self):
        print('Meow')


class Robot:
    'Not an animal at all - but it can speak, so it fits.'
    def speak(self):
        print('Beep boop')


def talk(thing):
    thing.speak()          # no inheritance, no interface, no check


if __name__ == '__main__':
    for thing in (Dog(), Cat(), Robot()):
        talk(thing)

    # If the method is missing we get an AttributeError at that moment.
    # hasattr() lets us check first.
    class Stone:
        pass

    stone = Stone()
    if hasattr(stone, 'speak'):
        talk(stone)
    else:
        print('A stone cannot speak.')

تایپ اردکی: نمونه 09_duck_typing.py

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

۱۰. کلاس انتزاعی

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

# An abstract base class defines WHAT the children must provide,
# without providing it itself. It cannot be instantiated.
from abc import ABC, abstractmethod


class Notifier(ABC):
    def __init__(self, target: str) -> None:
        self.target = target

    @abstractmethod
    def send(self, message: str) -> None:
        'Every real notifier has to implement this.'

    def send_all(self, messages: list[str]) -> None:
        'A normal method that the children inherit for free.'
        for message in messages:
            self.send(message)


class EmailNotifier(Notifier):
    def send(self, message: str) -> None:
        print(f'Email to {self.target}: {message}')


class SmsNotifier(Notifier):
    def send(self, message: str) -> None:
        print(f'SMS to {self.target}: {message}')


if __name__ == '__main__':
    notifiers: list[Notifier] = [
        EmailNotifier('ali@kadoosedu.ir'),
        SmsNotifier('09121234567'),
    ]

    for notifier in notifiers:
        notifier.send_all(['Welcome!', 'Your class starts at 17:00'])

    # Creating the abstract class itself is refused:
    try:
        Notifier('nobody')
    except TypeError as e:
        print('Refused:', e)

کلاس انتزاعی: نمونه 10_abstract_base_class.py

شرح مثال

۱۱. نمونه کامل: اشکال هندسی

import math
from abc import ABC, abstractmethod


class Shape(ABC):
    def __init__(self, name: str) -> None:
        self.name = name

    @abstractmethod
    def area(self) -> float:
        'Return the area of the shape.'

    @abstractmethod
    def perimeter(self) -> float:
        'Return the perimeter of the shape.'

    def describe(self) -> str:
        'Shared by every shape - written only once.'
        return (f'{self.name:<10} area={self.area():>8.2f} '
                f'perimeter={self.perimeter():>8.2f}')


class Rectangle(Shape):
    def __init__(self, width: float, height: float) -> None:
        super().__init__('Rectangle')
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height

    def perimeter(self) -> float:
        return 2 * (self.width + self.height)


class Square(Rectangle):
    def __init__(self, side: float) -> None:
        super().__init__(side, side)
        self.name = 'Square'


class Circle(Shape):
    def __init__(self, radius: float) -> None:
        super().__init__('Circle')
        self.radius = radius

    def area(self) -> float:
        return math.pi * self.radius ** 2

    def perimeter(self) -> float:
        return 2 * math.pi * self.radius

اشکال هندسی: نمونه 11_shapes.py

دقت کنید که Square از Rectangle ارث می‌برد، نه از Shape. مربع واقعاً یک نوع مستطیل است، پس area و perimeter را رایگان می‌گیرد و فقط سازنده‌اش را ساده می‌کند.

۱۲. وراثت یا ترکیب

'''Inheritance says "IS A", composition says "HAS A".'''


# --- Wrong: a car is NOT an engine -------------------------------------------
class Engine:
    def start(self):
        print('Engine started')


class BadCar(Engine):
    'A car that inherits from Engine also inherits everything an engine can do,'
    'which makes no sense and breaks as soon as Engine changes.'


# --- Right: a car HAS an engine ----------------------------------------------
class GoodCar:
    def __init__(self, brand: str, engine: Engine) -> None:
        self.brand = brand
        self.engine = engine

    def start(self) -> None:
        print(f'{self.brand}:', end=' ')
        self.engine.start()


# --- Right use of inheritance: an electric car IS A car ----------------------
class ElectricCar(GoodCar):
    def __init__(self, brand: str, battery_kwh: float) -> None:
        super().__init__(brand, Engine())
        self.battery_kwh = battery_kwh

    def start(self) -> None:
        print(f'{self.brand} starts silently ({self.battery_kwh} kWh).')

مقایسه: نمونه 12_inheritance_vs_composition.py

رابطهابزارمثال
«یک نوع از» استوراثتدانشجو یک نوع شخص است
«دارد»ترکیبماشین موتور دارد
آزمون ساده

جمله را به فارسی بگویید: «آیا X واقعاً یک نوع Y است؟» اگر جواب بله است، وراثت. اگر جمله عجیب شد، مثل «ماشین یک نوع موتور است»، ترکیب. انتخاب غلط بین این دو، رایج‌ترین اشتباه شیءگرایی در دنیاست.

فایل‌های تمرینی این فصل

پوشه S23-oop-inheritance-and-polymorphism شامل نمونه‌های 01_first_inheritance.py تا 12_inheritance_vs_composition.py.

خلاصه فصل

ابزاروظیفه آن
class Child(Parent):فرزند همه چیز والد را می‌گیرد
بازنویسیفرزند نسخه خودش از یک متد را می‌نویسد
super()دسترسی به والد، مخصوصاً برای سازنده
isinstance(x, C)بررسی نوع، با در نظر گرفتن وراثت
issubclass(A, B)بررسی رابطه بین دو کلاس
C.__mro__ترتیب دقیق جست‌وجوی متد
وراثت چندگانهممکن ولی خطرناک. میکسین یا ترکیب بهتر است
چندریختییک حلقه، چند کلاس، رفتار متفاوت
تایپ اردکیمهم نیست چه کلاسی است، مهم است متد را دارد
ABC و @abstractmethodاجبار فرزندان به پیاده‌سازی
وراثت یا ترکیب«است» یا «دارد»

تمرین‌های فصل بیست و سه

  1. کلاس Animal و سپس Dog و Cat و Bird را بنویسید که speak() را بازنویسی کنند.
  2. کلاس Person و Student(Person) را بنویسید، طوری که دانشجو student_id هم داشته باشد.
  3. کلاس‌های Vehicle و Car و Motorcycle را با متد مشترک describe() بنویسید.
  4. کلاس انتزاعی Employee با متد انتزاعی salary() و دو فرزند HourlyEmployee و MonthlyEmployee بنویسید.
  5. اشیائی از چند کلاس را در یک لیست بگذارید و در یک حلقه، یک متد را روی همه صدا بزنید.
  6. ترتیب MRO کلاسی با سه سطح وراثت را نشان دهید.
  7. یک کلاس میکسین بنویسید که فقط متد to_json() اضافه کند و آن را با دو کلاس بی‌ربط به هم استفاده کنید.
  8. مثالی پیدا کنید که وراثت در آن اشتباه به کار رفته و با ترکیب بازنویسی‌اش کنید.
فصل قبل: کپسوله‌سازی و dataclass فصل بعد: متدهای جادویی