Към съдържанието
wannacode

Наследяване

Проблем

Две копия на един и същ код

Програмата за заплати има клас Employee с име, заплата и метод за повишение. После трябваше и мениджър, който има и бонус. Най-бързо беше да копираш Employee в нов клас Manager и да добавиш бонуса.

Месец по-късно някой въвежда повишение -10% вместо 10%. Затова в Employee добавяш проверка. В Manager я забравяш (методът е raise_salary, защото raise е запазена дума в Python):

# в Employee
def raise_salary(self, percent):
if percent > 0:
self._salary += self._salary * percent // 100
# в Manager: копие отпреди проверката
def raise_salary(self, percent):
self._salary += self._salary * percent // 100
ivan = Employee("Иван", 2000)
maria = Manager("Мария", 3000, 500)
ivan.raise_salary(-10)
maria.raise_salary(-10)
print(ivan)
print(maria)
Изход
Иван: 2000 €
Мария: 2700 €

Заплатата на Иван е защитена, а на Мария падна с 300 €. Двата класа са еднакви на 90%, но всяка поправка трябва да се прави два пъти.

Идеята

Общото е в родителя

Мениджърът е служител, само с нещо повече. Кажи го на езика: class Manager(Employee):. В Manager пишеш само новото, бонуса:

class Manager(Employee):
def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self._bonus = bonus
@property
def bonus(self):
return self._bonus
maria = Manager("Мария", 3000, 500)
maria.raise_salary(-10)
maria.raise_salary(10)
print(maria)
print(f"Бонус: {maria.bonus} €")
Изход
Мария: 3300 €
Бонус: 500 €

Manager вече има всичко на Employee: име, заплата, raise_salary() с проверката, __str__(). Затова повишението с -10% не мина. Проверката е написана веднъж и важи и за двата класа.

Това се казва наследяване. Employee е родителят (базов клас), а Manager е наследникът. Наследникът взима всичко на родителя и добавя своето.

Проверката дали наследяването има смисъл е изречението „X е Y“. „Мениджърът е служител“ е вярно. „Фактурата е служител“ не е, затова фактурата не наследява Employee, дори да има сума и име.

Пробвай

Какво получава стажантът?

Intern няма нищо свое: тялото е само pass.

Какво ще отпечата програмата?

class Intern(Employee):
pass
ana = Intern("Ана", 1200)
ana.raise_salary(5)
print(ana)
Отговор

Идеята

Първо родителят

Manager има свой __init__, защото има и бонус. Но името и заплатата ги подрежда __init__ на Employee. Кой го вика?

Python вика само един __init__: този на класа на обекта. Родителският трябва да извикаш сам:

def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self._bonus = bonus

super() е „родителят на този клас“, а super().__init__(name, salary) вика неговия __init__ с тези аргументи. Той подрежда _name и _salary, а после Manager подрежда _bonus. Python позволява този ред навсякъде в __init__, но го пиши първи: така частта на родителя е готова, преди наследникът да я ползва.

Ако го забравиш, Python не казва нищо, докато полето не потрябва. print(maria) спира с AttributeError: 'Manager' object has no attribute '_name': полето просто не е създадено.

Обектът е един. В него има и частта на родителя, и частта на наследника, а всяка част се подрежда от своя конструктор.

def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self._bonus = bonus
maria = Manager("Мария", 3000, 500)
print(f"{maria}, бонус {maria.bonus} €")
Стъпка по стъпка, с думи
  1. Manager(...) създава един обект. Засега е празен: всяко поле се появява, когато __init__ му даде стойност.
  2. Започва __init__ на Manager с трите аргумента.
  3. Първо родителят: super().__init__(name, salary) вика __init__ на Employee. Python не го вика сам.
  4. __init__ на Employee подрежда своята част: _name и _salary.
  5. Чак после тялото на Manager подрежда бонуса.
  6. Обектът е един, а всяка част е подредена от своя конструктор.
По-точно, за любопитните
  • __init__ се наследява като всеки друг метод. Затова Intern без свой __init__ приема името и заплатата на Employee. Свой __init__ пишеш, когато наследникът има нещо ново, и тогава той замества този на родителя.
  • В по-стар код ще видиш super(Manager, self).__init__(...) или Employee.__init__(self, ...). Правят същото, super() без нищо в скобите е днешният запис.

Пробвай

Кой конструктор е пръв?

Тук наследникът не вика родителя изрично:

Какво ще отпечата програмата?

class Account:
def __init__(self):
print("Сметката е открита")
class SavingsAccount(Account):
def __init__(self):
print("Лихва: 3%")
savings = SavingsAccount()
Отговор

Пробвай

Намери грешката

Мария трябва да има заплата 3000 € и бонус 500 €. Програмата обаче печата:

Изход
Мария: 500 €

На кой ред е грешката?

Избери реда с грешката.

Идеята

Долната черта: за класа и наследниците

В края на годината бонусът на мениджъра влиза в заплатата му. Методът е в Manager, а заплатата е _salary в Employee. Property-то salary само чете, а тук трябва да я промениш.

В Java и C# за това има трето ниво на достъп, protected: виждат го класът и наследниците му, а останалите не. Python няма нито private, нито protected. Долната черта е договорка точно за това: „вътрешно, за класа и наследниците му“. Затова Manager пише направо в self._salary, а Employee не се променя:

class Employee:
def __init__(self, name, salary):
self._name = name
self._salary = salary
# ... останалото е както преди
class Manager(Employee):
# ... _bonus и __init__
def merge_bonus(self):
self._salary += self._bonus
self._bonus = 0
maria = Manager("Мария", 3000, 500)
maria.merge_bonus()
print(maria)
print(f"Бонус: {maria.bonus} €")
Изход
Мария: 3500 €
Бонус: 0 €

Договорката не е защита: maria._salary = 0 в програмата също минава, без грешка. Долната черта казва на хората „не пипай отвън“, а наследникът е „свой“. Затова наследникът я ползва само когато наистина трябва да промени полето, а не за да заобиколи проверката в raise_salary().

По-точно, за любопитните
  • Две долни черти, self.__salary, са най-близо до private. Python тихо преименува полето на _Employee__salary. В Manager self.__salary вече е _Manager__salary, а такова поле няма: AttributeError: 'Manager' object has no attribute '_Manager__salary'. Това пази от случайни сблъсъци на имена, не от хора: maria._Employee__salary пак се стига.

Пробвай

Кое липсва?

Стажантът е служител. Какво трябва да пише на празното място?

class Intern():
    pass

Идеята

Родители и общ прародител

В Java, C# и JavaScript класът има точно един родител. Python позволява и няколко: class WorkingStudent(Employee, Student): получава методите и на двата. Това се казва множествено наследяване. Тук ще ползваме по един родител. Няколко ще ни трябват в урока за интерфейсите.

Веригата може да е дълга. Intern наследява Employee. А кого наследява Employee, щом не пише родител в скобите? Всеки клас без родител наследява object. Това е общият родител на всички класове. Затова и празен клас има методи:

class Ticket:
pass
a = Ticket()
b = Ticket()
print(a == a)
print(a == b)
Изход
True
False

== вика метода __eq__, а той идва от object. Засега той казва само дали двете променливи сочат един и същ обект.

Оттам е и __str__. Ако не го напишеш, print(ticket) печата нещо като <__main__.Ticket object at 0x7f3a2c1d5e50>: името на класа и адрес, който е различен при всяко пускане. В модул „Класове“ писа свой __str__. Сега знаеш, че така заменяш версията на object. Повече за това е в следващия урок.

По-точно, за любопитните
  • В Python няма клас, който не може да се наследява. typing.final го обещава, но проверява само инструмент като mypy, не самият Python.
  • При няколко родителя Python търси метода по ред: първо в класа, после в родителите отляво надясно, накрая в object. Редът се вижда в WorkingStudent.__mro__.

Упражнение

Наследници

Реши задачите в редактора си. Родителските класове са от модул „Класове“. Ако не са ти под ръка, напиши ги наново: те са кратки.

1. Семейна кола. SimpleCar има скорост и метод accelerate(), който я вдига с 10. Напиши FamilyCar, който наследява SimpleCar, започва с 4 места и има метод add_seat(). Създай семейна кола, ускори я и добави място:

Изход
Скорост: 10 км/ч
Места: 5

2. Студент. Person има име, възраст и introduce(). Напиши Student, който наследява Person и добавя среден успех grade (дробно число). Използвай конструктора на родителя.

Изход
Аз съм Елена, на 19 години.
Успех: 5.5

3. Брейк. DanceMove има име, продължителност в секунди и perform(). Напиши BreakDanceMove със сложност от 1 до 10 и метод show_complexity().

Изход
Изпълнявам moonwalk за 5 секунди
Изпълнявам windmill за 12 секунди
windmill: сложност 8 от 10

4. Електрическа кола. Car има марка, гориво в литри и изминати километри. refuel(liters) долива, а drive(distance) харчи 1 литър на 10 км и добавя километрите (ако горивото не стига, казва го и не тръгва). Напиши ElectricCar:

  • без гориво, с батерия, която започва от 100%;
  • с ефективност: колко км изминава с 1% батерия (за „Тесла“ 5);
  • charge() зарежда батерията до 100%;
  • drive_electric(distance) харчи батерия вместо гориво и също добавя километрите.
Наследникът също променя километрите. Как трябва да се казва полето в Car? Подсказка: картата за долната черта.
Изход
Шкода: 150 км, гориво 35 л
Тесла: 200 км, батерия 60%
Батерията не стига за 400 км
Тесла: 600 км, батерия 20%

Още задачи

  • Работник. Worker наследява Person, добавя длъжност и метод work():
Изход
Аз съм Петър, на 34 години.
Работя като счетоводител
  • Стажант с ментор. Добави в Intern поле с името на ментора и го подай в конструктора, заедно с името и заплатата за родителя. Сега Intern вече има нужда от свой __init__.

Тест

Провери се

1. class Manager(Employee). Кое е вярно?
2. Полето _name е в Employee. Може ли Manager да напише self._name = "Мария" в свой метод?
3. Какво прави super().__init__(name, salary) в __init__ на Manager?
4. Колко родителя може да има един клас в Python?
5. Manager има свой __init__, който не вика super().__init__(). Какво става?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·