Полиморфизъм
Проблем
Заплата с if по вид
Във фирмата има служители, мениджъри с бонус и стажанти на половин работен ден. Някой е направил един клас Employee с поле „роля“, а заплатата се смята така:
def monthly_pay(e): if e.role == "мениджър": return e.salary + e.bonus elif e.role == "стажант": return e.salary // 2 return e.salaryРаботи. Но после идва нов вид служител, на хонорар. Трябва нов elif тук, и в функцията за данъците, и в функцията за фиша, и навсякъде, където някой е питал за ролята. А всеки служител носи поле за бонус, макар че само мениджърите имат бонус.
Кодът за мениджъра е разпилян из цялата програма, вместо да е в мениджъра.
Идеята
Всеки клас казва сам
С наследяването от миналия урок Manager и Intern са отделни класове. Нека всеки сам каже каква е месечната му заплата. Родителят има обща версия, а наследникът пише своя версия на същия метод:
class Employee: # ... __init__, name и salary
def monthly_pay(self): return self._salary
class Manager(Employee): # ... _bonus и __init__
def monthly_pay(self): return self.salary + self._bonusivan = Employee("Иван", 2000)maria = Manager("Мария", 3000, 500)print(ivan.monthly_pay())print(maria.monthly_pay())20003500Това се казва презаписване на метод (override). Наследникът пише метод със същото име. За обектите от наследника неговата версия заменя тази на родителя.
Не трябва нищо повече: няма ключова дума и няма проверка. Всеки метод може да бъде презаписан. Python гледа само името, не и параметрите. Това е удобно, но значи, че грешка в името или в параметрите не спира никой. След малко ще видиш какво става тогава.
По-точно, за любопитните
- От Python 3.12 има
@overrideот модулаtyping. Слагаш го над презаписания метод. Самият Python не го проверява: програмата работи по същия начин и с него, и без него. Проверява го инструмент като mypy, който чете кода, преди да го пуснеш. - Метод, който наследниците не бива да сменят, също няма как да се забрани.
@finalотtypingе само бележка за такива инструменти.
Пробвай
Какво се смени?
Стажантът презаписва само метода за месечната заплата:
Какво ще отпечата програмата?
class Intern(Employee): def monthly_pay(self): return self.salary // 2
ana = Intern("Ана", 1600)print(ana.monthly_pay())print(ana.salary)monthly_pay() на стажанта връща половината заплата. Самата заплата не се променя, а salary е наследен от Employee без промяна.
Идеята
Един списък за всички
Мениджърът е служител. Затова служители, мениджъри и стажанти могат да са в един списък и да се обработят в един цикъл:
staff = [ Employee("Иван", 2000), Manager("Мария", 3000, 500), Intern("Ана", 1600),]for e in staff: print(f"{e.name}: {e.monthly_pay()} €")Иван: 2000 €Мария: 3500 €Ана: 800 €Цикълът вика един и същ метод, e.monthly_pay(), а резултатът е различен. Изпълнява се версията на обекта, а не на типа на променливата. Това се казва полиморфизъм: на гръцки „много форми“. Едно извикване, а всеки обект отговаря по своя начин.
Сега новият вид служител е нов клас с негов monthly_pay(). Цикълът и останалата програма не се пипат. Няма if по роля, няма поле за бонус в служителите без бонус.
Променливата e няма тип, затова през нея можеш да напишеш каквото искаш, и e.bonus. Ако обектът е мениджър, работи. Ако е обикновен служител, програмата спира на този ред: AttributeError: 'Employee' object has no attribute 'bonus'. Python не проверява предварително. Как се питат обектите какво могат, ще видим в урока „Какъв е всъщност?“.
Виж го
Кой метод се изпълнява
staff = [ Employee("Иван", 2000), Manager("Мария", 3000, 500), Intern("Ана", 1600),]for e in staff: print(f"{e.name}: {e.monthly_pay()} €")Стъпка по стъпка, с думи
- В списъка има обекти от три класа. Всяко място пази стрелка.
- e сочи Иван, обикновен Employee: изпълнява се monthly_pay на Employee.
- e сочи Мария, Manager: изпълнява се нейният метод, заплата плюс бонус.
- e сочи Ана, Intern: стажантът взима половината от заплатата.
- Един и същ ред, три различни метода. Решава обектът, към който сочи e.
Списъкът пази три стрелки. Всяка сочи обект от различен клас. При всеки кръг e сочи следващия обект, а извикването отива в метода на този обект: първо в Employee, после в Manager, после в Intern.
Изборът става, докато програмата работи. На всеки кръг Python търси monthly_pay първо в класа на обекта, после в родителя му. Никой не проверява предварително, че всеки обект има такъв метод. Ако някой няма, програмата спира на неговия кръг.
Пробвай
Намери грешката
Програмата е същата, но стажантът получава цялата заплата:
Иван: 2000 €Мария: 3500 €Ана: 1600 €Ето класа Intern. На кой ред е грешката?
Избери реда с грешката.
monthlypay, без долна черта. Това е нов метод, а не презаписване на monthly_pay. Затова се вика версията на родителя. Python не казва нищо: за него това е просто още един метод. @override от typing над метода не би спрял програмата, но инструмент като mypy би го хванал.Идеята
Допълни родителя, не го преписвай
Понякога наследникът не иска да замени метода изцяло, а да добави нещо към него. Мениджърът се описва като служител, плюс бонуса. Версията на родителя се вика с super().__str__():
def __str__(self): return f"{super().__str__()}, бонус {self._bonus} €"print(Employee("Иван", 2000))print(Manager("Мария", 3000, 500))Иван (2000 €)Мария (3000 €), бонус 500 €Ако Employee смени как се описва, мениджърът ще го последва сам.
Това е и __str__ от модул „Класове“. Там вече презаписваше метод: версията на object.
По-точно, за любопитните
super().method()може да се извика от всеки метод на наследника, не само от презаписания.
- Python не сверява параметрите. Ако в
Managerнапишешdef monthly_pay(self, extra):, това пак е презаписване, а цикълът, който викаe.monthly_pay(), спира на мениджъра:TypeError: Manager.monthly_pay() missing 1 required positional argument: 'extra'. Затова презаписаният метод приема същите параметри като на родителя.
Пробвай
Кое липсва?
Стажантът се описва като служител и думата „стажант“. Какво трябва да пише на празното място?
class Intern(Employee):
def __str__(self):
return f"{.__str__()}, стажант"super().__str__() дава „Ана (1600 €)“, а наследникът добавя „, стажант“. С self.__str__() вместо него методът щеше да вика сам себе си, без край, докато Python не спре програмата с RecursionError.
Упражнение
Всеки по своему
Реши задачите в редактора си. Във всяка задача има списък с обекти от няколко класа и един цикъл.
1. Приют. Клас Animal с име и метод make_sound(), който печата „някакъв звук“. Dog и Cat го презаписват. Сложи по едно от трите в списък:
Гост: някакъв звукРекс: Бау!Мици: Мяу, мяу!2. Студентът се представя. Student от миналия урок вече има успех. Презапиши introduce(), така че да казва и успеха. Използвай super().introduce(), не преписвай реда на Person.
Аз съм Георги, на 45 години.Аз съм Елена, на 19 години.Среден успех: 5.53. Купон. Клас Performance със заглавие и метод start(). DanceMove, BreakDanceMove (наследява DanceMove, добавя сложност) и Song го презаписват. Клас Party пази списък с изпълнения и има метод start_party(), който стартира всичко поред и накрая казва, че купонът е започнал.
Танцуваме moonwalkТанцуваме windmill сложност 8 от 10Пеем 'Bohemian Rhapsody'Започва томболаКупонът започна!Още задачи
- Хонорар. Добави към служителите
Contractorс часове и ставка на час. Месечната му заплата е часове × ставка. Сметни фонда на заплатите за Иван, Мария, Ана и Петър (60 часа по 25 €):
Фонд работна заплата: 7800 €- Фиш. Добави на
Employeeметодpayslip(), който връща текст „име: заплата €“ с месечната заплата. Напиши го само вEmployee. Защо работи вярно и за мениджъра, и за стажанта?
Тест