وراثت و چندریختی
دو ستون باقیمانده شیءگرایی. وراثت یعنی استفاده دوباره از یک کلاس به جای کپی کردنش، و چندریختی یعنی اینکه اشیاء مختلف به یک پیام واحد، هر کدام به شیوه خودشان جواب دهند.
۱. اولین وراثت
فرض کنید کلاس 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
شرح مثال
- نام کلاس والد داخل پرانتز جلوی نام کلاس فرزند نوشته میشود.
- فرزند همه چیز والد را میگیرد: متدها و صفتها.
- رابطه یکطرفه است. والد هیچ خبری از فرزند ندارد.
- اصطلاحها: کلاس والد را پایه یا superclass و کلاس فرزند را مشتق یا subclass هم میگویند.
۲. بازنویسی متد
# 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
شرح مثال
- کلاس باید از
ABCارث ببرد و متدهای اجباری با@abstractmethodعلامت بخورند. - از خود کلاس انتزاعی نمیشود شیء ساخت. تلاش برای آن
TypeErrorمیدهد. - فرزندی که متد انتزاعی را پیاده نکند هم قابل ساخت نیست. پس خطا زودتر و در جای درست پیدا میشود.
- کلاس انتزاعی میتواند متدهای عادی هم داشته باشد، مثل
send_allکه همه فرزندان رایگان میگیرند. - این ساختار در فصل سی و سوم، هنگام یادگیری الگوی استراتژی، دوباره به کار میآید.
۱۱. نمونه کامل: اشکال هندسی
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 | اجبار فرزندان به پیادهسازی |
| وراثت یا ترکیب | «است» یا «دارد» |
تمرینهای فصل بیست و سه
- کلاس
Animalو سپسDogوCatوBirdرا بنویسید کهspeak()را بازنویسی کنند. - کلاس
PersonوStudent(Person)را بنویسید، طوری که دانشجوstudent_idهم داشته باشد. - کلاسهای
VehicleوCarوMotorcycleرا با متد مشترکdescribe()بنویسید. - کلاس انتزاعی
Employeeبا متد انتزاعیsalary()و دو فرزندHourlyEmployeeوMonthlyEmployeeبنویسید. - اشیائی از چند کلاس را در یک لیست بگذارید و در یک حلقه، یک متد را روی همه صدا بزنید.
- ترتیب MRO کلاسی با سه سطح وراثت را نشان دهید.
- یک کلاس میکسین بنویسید که فقط متد
to_json()اضافه کند و آن را با دو کلاس بیربط به هم استفاده کنید. - مثالی پیدا کنید که وراثت در آن اشتباه به کار رفته و با ترکیب بازنویسیاش کنید.