32. Расширение классов с помощью наследования и полиморфизма
В главе 30 мы узнали, как создавать собственные классы для моделирования концепций реального мира. Мы построили классы вроде BankAccount и Student, которые объединяли данные и поведение. Но что делать, если вам нужно создать новый класс, похожий на существующий, но с некоторыми различиями или дополнениями?
Наследование(inheritance) — это механизм Python для создания новых классов на основе существующих. Вместо копирования и вставки кода вы можете создать подкласс(subclass), который автоматически получает все атрибуты и методы от родительского класса(parent class) (его также называют базовым классом(base class) или суперклассом(superclass)), а затем добавить или изменить то, что вам нужно.
В этой главе рассматривается, как наследование позволяет строить иерархии связанных классов, как настраивать унаследованное поведение и как полиморфизм(polymorphism) позволяет использовать разные классы взаимозаменяемо, когда у них общие интерфейсы.
32.1) Создание подклассов на основе существующих классов
32.1.1) Базовый синтаксис наследования
Наследование позволяет создать новый класс (называемый подклассом(subclass) или дочерним классом(child class)) на основе существующего класса (называемого родительским классом(parent class) или базовым классом(base class)). Подкласс автоматически наследует все методы и атрибуты родительского класса.
Когда вы создаёте подкласс, вы указываете родительский класс в круглых скобках после имени класса.
# Родительский класс
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound"
# Подкласс, наследующий от Animal
class Dog(Animal):
pass # Пока дополнительный код не нужен
# Создаём экземпляр Dog
buddy = Dog("Buddy")
print(buddy.speak()) # Output: Buddy makes a sound
print(buddy.name) # Output: BuddyХотя у Dog нет собственного кода (только pass), он наследует всё от Animal. Класс Dog автоматически имеет метод __init__ и метод speak от своего родителя.
32.1.2) Почему наследование важно
Наследование решает распространённую проблему программирования: дублирование кода. Представьте, что вы строите систему для управления разными типами сотрудников:
# Без наследования — много дублирования
class FullTimeEmployee:
def __init__(self, name, employee_id, salary):
self.name = name
self.employee_id = employee_id
self.salary = salary
def get_info(self):
return f"{self.name} (ID: {self.employee_id})"
class PartTimeEmployee:
def __init__(self, name, employee_id, hourly_rate):
self.name = name
self.employee_id = employee_id
self.hourly_rate = hourly_rate
def get_info(self):
return f"{self.name} (ID: {self.employee_id})"Обратите внимание, что name, employee_id и get_info() дублируются. С наследованием мы можем устранить это дублирование:
# С наследованием — общий код в родительском классе
class Employee:
def __init__(self, name, employee_id):
self.name = name
self.employee_id = employee_id
def get_info(self):
return f"{self.name} (ID: {self.employee_id})"
class FullTimeEmployee(Employee):
def __init__(self, name, employee_id, salary):
Employee.__init__(self, name, employee_id) # Вызов __init__ родителя
# Note: We'll learn a better way to do this with super() in Section 32.3
self.salary = salary
class PartTimeEmployee(Employee):
def __init__(self, name, employee_id, hourly_rate):
Employee.__init__(self, name, employee_id) # Вызов __init__ родителя
self.hourly_rate = hourly_rate
# Оба подкласса наследуют get_info()
alice = FullTimeEmployee("Alice", "E001", 75000)
bob = PartTimeEmployee("Bob", "E002", 25)
print(alice.get_info()) # Output: Alice (ID: E001)
print(bob.get_info()) # Output: Bob (ID: E002)Теперь общие атрибуты и методы находятся в Employee, а каждый подкласс определяет только то, что делает его уникальным.
32.1.3) Добавление новых методов в подклассы
Подклассы могут добавлять собственные методы, которых нет у родителя:
class Vehicle:
def __init__(self, brand, model):
self.brand = brand
self.model = model
def get_description(self):
return f"{self.brand} {self.model}"
class Car(Vehicle):
def __init__(self, brand, model, num_doors):
Vehicle.__init__(self, brand, model)
self.num_doors = num_doors
def honk(self): # Новый метод, специфичный для Car
return "Beep beep!"
class Motorcycle(Vehicle):
def __init__(self, brand, model, has_sidecar):
Vehicle.__init__(self, brand, model)
self.has_sidecar = has_sidecar
def rev_engine(self): # Новый метод, специфичный для Motorcycle
return "Vroom vroom!"
# У каждого подкласса есть методы родителя плюс собственные
my_car = Car("Toyota", "Camry", 4)
print(my_car.get_description()) # Output: Toyota Camry
print(my_car.honk()) # Output: Beep beep!
my_bike = Motorcycle("Harley", "Sportster", False)
print(my_bike.get_description()) # Output: Harley Sportster
print(my_bike.rev_engine()) # Output: Vroom vroom!Класс Car имеет и get_description() (унаследованный), и honk() (свой). Класс Motorcycle имеет get_description() (унаследованный) и rev_engine() (свой).
32.1.4) Добавление новых атрибутов в подклассы
Подклассы также могут добавлять собственные атрибуты экземпляра. Обычно это делают в методе __init__ подкласса:
class BankAccount:
def __init__(self, account_number, balance):
self.account_number = account_number
self.balance = balance
def deposit(self, amount):
self.balance += amount
return self.balance
class SavingsAccount(BankAccount):
def __init__(self, account_number, balance, interest_rate):
BankAccount.__init__(self, account_number, balance)
self.interest_rate = interest_rate # Новый атрибут
def apply_interest(self): # Новый метод, использующий новый атрибут
interest = self.balance * self.interest_rate
self.balance += interest
return interest
# SavingsAccount имеет все атрибуты BankAccount плюс interest_rate
savings = SavingsAccount("SA001", 1000, 0.03)
savings.deposit(500) # Унаследованный метод
print(f"Balance: ${savings.balance}") # Output: Balance: $1500
interest_earned = savings.apply_interest()
print(f"Interest earned: ${interest_earned:.2f}") # Output: Interest earned: $45.00
print(f"New balance: ${savings.balance:.2f}") # Output: New balance: $1545.00У SavingsAccount есть account_number и balance из BankAccount, а также собственный атрибут interest_rate.
32.1.5) Несколько уровней наследования
Классы могут наследоваться от классов, которые сами наследуются от других классов, создавая иерархию наследования:
class LivingThing:
def __init__(self, name):
self.name = name
def is_alive(self):
return True
class Animal(LivingThing):
def __init__(self, name, species):
LivingThing.__init__(self, name)
self.species = species
def move(self):
return f"{self.name} is moving"
class Dog(Animal):
def __init__(self, name, breed):
Animal.__init__(self, name, "Dog")
self.breed = breed
def bark(self):
return f"{self.name} says: Woof!"
# Dog наследуется от Animal, который наследуется от LivingThing
max_dog = Dog("Max", "Golden Retriever")
# Методы со всех трёх уровней работают
print(max_dog.is_alive()) # Output: True (from LivingThing)
print(max_dog.move()) # Output: Max is moving (from Animal)
print(max_dog.bark()) # Output: Max says: Woof! (from Dog)
# Атрибуты со всех трёх уровней существуют
print(max_dog.name) # Output: Max (from LivingThing)
print(max_dog.species) # Output: Dog (from Animal)
print(max_dog.breed) # Output: Golden Retriever (from Dog)Dog наследуется от Animal, который наследуется от LivingThing. Это означает, что Dog имеет доступ к методам и атрибутам обоих родительских классов.
32.2) Переопределение методов в подклассах
32.2.1) Что означает переопределение методов
Иногда подклассу нужно изменить то, как работает унаследованный метод. Переопределение методов(method overriding) означает определение метода в подклассе с тем же именем, что и метод в родительском классе. Когда вы вызываете этот метод у экземпляра подкласса, Python использует версию подкласса вместо версии родителя.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound"
class Dog(Animal):
def speak(self): # Переопределяем метод speak родителя
return f"{self.name} says: Woof!"
class Cat(Animal):
def speak(self): # Переопределяем с другим поведением
return f"{self.name} says: Meow!"
# У каждого класса своя версия speak()
generic_animal = Animal("Generic")
print(generic_animal.speak()) # Output: Generic makes a sound
buddy = Dog("Buddy")
print(buddy.speak()) # Output: Buddy says: Woof!
whiskers = Cat("Whiskers")
print(whiskers.speak()) # Output: Whiskers says: Meow!Когда вы вызываете buddy.speak(), Python сначала ищет метод speak в классе Dog, поскольку buddy — это экземпляр Dog. Поскольку Dog определяет собственный метод speak, Python использует эту версию. Если бы у Dog не было метода speak, Python затем искал бы его в родительском классе Animal и использовал бы версию оттуда.
Этот порядок поиска — начиная с класса экземпляра и затем переходя к родительскому классу — и есть то, как работает переопределение методов и как подклассы настраивают унаследованное поведение.
32.2.2) Зачем переопределять методы?
Переопределение методов позволяет создавать специализированные версии общего поведения. Рассмотрим иерархию фигур:
class Shape:
def __init__(self, name):
self.name = name
def area(self):
return 0 # Реализация по умолчанию
def describe(self):
return f"{self.name} with area {self.area()}"
class Rectangle(Shape):
def __init__(self, width, height):
Shape.__init__(self, "Rectangle")
self.width = width
self.height = height
def area(self): # Переопределяем вычислением, специфичным для прямоугольника
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
Shape.__init__(self, "Circle")
self.radius = radius
def area(self): # Переопределяем вычислением, специфичным для круга
return 3.14159 * self.radius ** 2
# Каждая фигура рассчитывает площадь по-своему
rect = Rectangle(5, 3)
print(rect.describe()) # Output: Rectangle with area 15
circle = Circle(4)
print(circle.describe()) # Output: Circle with area 50.26544Метод describe() наследуется обоими подклассами и работает корректно, потому что каждый подкласс предоставляет собственную реализацию area().
Когда вы вызываете rect.describe(), выполняется унаследованный метод describe(), но self ссылается на экземпляр Rectangle. Поэтому, когда describe() вызывает self.area(), Python сначала ищет area() в классе Rectangle и находит переопределённую версию.
32.2.3) Переопределение __init__ и вызов инициализации родителя
Когда вы переопределяете __init__, обычно нужно вызвать __init__ родителя, чтобы гарантировать выполнение инициализации родительского класса:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def introduce(self):
return f"I'm {self.name}, {self.age} years old"
class Student(Person):
def __init__(self, name, age, student_id, major):
# Вызов __init__ родителя, чтобы задать name и age
Person.__init__(self, name, age)
# Затем задаём атрибуты, специфичные для Student
self.student_id = student_id
self.major = major
alice = Student("Alice", 20, "S12345", "Computer Science")
print(alice.name) # Output: Alice
print(alice.student_id) # Output: S12345Обратите внимание, что Student.__init__ сначала вызывает Person.__init__(self, name, age), чтобы инициализировать атрибуты родительского класса, а затем добавляет собственные атрибуты.
32.3) Использование super() для доступа к поведению родителя
32.3.1) Что делает super()
В предыдущем разделе мы вызывали методы родителя явно: ParentClass.method(self, ...). Python предоставляет более чистый способ: функция super(). super() возвращает временный объект, который позволяет вызывать методы родительского класса, не называя родителя явно.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound"
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # Чище, чем Animal.__init__(self, name)
self.breed = breed
def speak(self):
parent_sound = super().speak() # Вызываем speak() родителя
return f"{parent_sound} - specifically, Woof!"
buddy = Dog("Buddy", "Labrador")
print(buddy.speak())
# Output: Buddy makes a sound - specifically, Woof!Использование super() имеет несколько преимуществ:
- Вам не нужно явно указывать родительский класс
- Это корректно работает с множественным наследованием (рассмотрим позже)
- Это упрощает сопровождение кода, если вы меняете родительский класс
32.3.2) Использование super() в __init__
Самое распространённое применение super() — вызов __init__ родителя:
class Employee:
def __init__(self, name, employee_id):
self.name = name
self.employee_id = employee_id
self.is_active = True
def deactivate(self):
self.is_active = False
class Manager(Employee):
def __init__(self, name, employee_id, department):
super().__init__(name, employee_id) # Инициализируем атрибуты родителя
self.department = department
self.team = [] # Атрибут, специфичный для Manager
def add_team_member(self, employee):
self.team.append(employee)
# Manager получает все атрибуты Employee плюс свои
sarah = Manager("Sarah", "M001", "Engineering")
print(sarah.name) # Output: Sarah
print(sarah.is_active) # Output: True
print(sarah.department) # Output: EngineeringВызывая super().__init__(name, employee_id), класс Manager гарантирует, что выполняется вся логика инициализации из Employee, включая установку is_active в True.
32.3.3) Расширение родительских методов с помощью super()
Вы можете использовать super() для расширения метода родителя, а не для полного замещения:
class BankAccount:
def __init__(self, account_number, balance):
self.account_number = account_number
self.balance = balance
self.transaction_count = 0
def deposit(self, amount):
self.balance += amount
self.transaction_count += 1
return self.balance
class CheckingAccount(BankAccount):
def __init__(self, account_number, balance, overdraft_limit):
super().__init__(account_number, balance)
self.overdraft_limit = overdraft_limit
def deposit(self, amount):
was_overdrawn = self.balance < 0
# Вызываем deposit родителя, чтобы обработать базовую логику
new_balance = super().deposit(amount)
# Добавляем поведение, специфичное для checking
if was_overdrawn and new_balance >= 0:
print("Account is no longer overdrawn")
return new_balance
checking = CheckingAccount("C001", -50, 100)
checking.deposit(75)
# Output: Account is no longer overdrawn
print(f"Balance: ${checking.balance}") # Output: Balance: $25
print(f"Transactions: {checking.transaction_count}") # Output: Transactions: 1Метод CheckingAccount.deposit() вызывает super().deposit(amount), чтобы выполнить базовую логику пополнения (обновление баланса и счётчика транзакций), а затем добавляет собственную проверку статуса овердрафта.
32.3.4) Когда использовать super() вместо прямого вызова родителя
В большинстве случаев используйте super():
class Vehicle:
def __init__(self, brand):
self.brand = brand
class Car(Vehicle):
def __init__(self, brand, model):
super().__init__(brand) # Предпочтительно
self.model = modelИспользуйте прямые вызовы родителя, когда вам нужно вызвать конкретного родителя в сценарии множественного наследования (рассмотрим позже) или когда вы хотите явно указать, какого родителя вы вызываете:
class Car(Vehicle):
def __init__(self, brand, model):
Vehicle.__init__(self, brand) # Явно, но менее гибко
self.model = modelПри одиночном наследовании (один родитель) super() почти всегда — лучший выбор.
32.3.5) super() с другими методами
Вы можете использовать super() с любым методом, не только с __init__:
class TextProcessor:
def process(self, text):
# Базовая обработка: убрать пробелы по краям
return text.strip()
class UppercaseProcessor(TextProcessor):
def process(self, text):
# Сначала выполняем обработку родителя
processed = super().process(text)
# Затем добавляем преобразование в верхний регистр
return processed.upper()
class PrefixProcessor(UppercaseProcessor):
def __init__(self, prefix):
self.prefix = prefix
def process(self, text):
# Сначала выполняем обработку родителя (которая вызывает своего родителя тоже)
processed = super().process(text)
# Затем добавляем префикс
return f"{self.prefix}: {processed}"
processor = PrefixProcessor("ALERT")
result = processor.process(" system error ")
print(result) # Output: ALERT: SYSTEM ERROR32.4) Полиморфизм: работа с совместимыми классами
32.4.1) Что означает полиморфизм
Полиморфизм(polymorphism) (от греческого: «многие формы») — это возможность одинаково обращаться с объектами разных классов, если они предоставляют одни и те же методы.
В Python, если у нескольких классов есть методы с одинаковым именем, вы можете вызывать эти методы, не зная точный класс объекта.
class Dog:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} says: Woof!"
class Cat:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} says: Meow!"
class Bird:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} says: Tweet!"
# Функция, которая работает с любым объектом, у которого есть метод speak()
def make_animal_speak(animal):
print(animal.speak())
# Работает с разными классами
buddy = Dog("Buddy")
whiskers = Cat("Whiskers")
tweety = Bird("Tweety")
make_animal_speak(buddy) # Output: Buddy says: Woof!
make_animal_speak(whiskers) # Output: Whiskers says: Meow!
make_animal_speak(tweety) # Output: Tweety says: Tweet!Функции make_animal_speak() неважно, какой класс у параметра animal — ей нужно лишь, чтобы у объекта был метод speak(). Это и есть полиморфизм в действии.
32.4.2) Полиморфизм с наследованием
Полиморфизм особенно мощен вместе с наследованием, когда подклассы переопределяют методы родителя:
class PaymentMethod:
def process_payment(self, amount):
return f"Processing ${amount:.2f}"
class CreditCard(PaymentMethod):
def __init__(self, card_number):
self.card_number = card_number
def process_payment(self, amount):
return f"Charging ${amount:.2f} to credit card ending in {self.card_number[-4:]}"
class PayPal(PaymentMethod):
def __init__(self, email):
self.email = email
def process_payment(self, amount):
return f"Sending ${amount:.2f} via PayPal to {self.email}"
class BankTransfer(PaymentMethod):
def __init__(self, account_number):
self.account_number = account_number
def process_payment(self, amount):
return f"Transferring ${amount:.2f} to account {self.account_number}"
# Функция, которая работает с любым PaymentMethod
def complete_purchase(payment_method, amount):
print(payment_method.process_payment(amount))
print("Purchase complete!")
# Все способы оплаты работают с одной и той же функцией
credit = CreditCard("1234567890123456")
paypal = PayPal("user@example.com")
bank = BankTransfer("9876543210")
complete_purchase(credit, 99.99)
# Output: Charging $99.99 to credit card ending in 3456
# Output: Purchase complete!
complete_purchase(paypal, 49.50)
# Output: Sending $49.50 via PayPal to user@example.com
# Output: Purchase complete!
complete_purchase(bank, 199.00)
# Output: Transferring $199.00 to account 9876543210
# Output: Purchase complete!Функция complete_purchase() работает с любым подклассом PaymentMethod. Каждый подкласс предоставляет собственную реализацию process_payment(), но функции не нужно знать, с каким конкретно классом она работает.
32.4.3) Duck typing: «Если это ходит как утка…»
Полиморфизм в Python не требует, чтобы классы были связаны наследованием. Это называется утиная типизация(duck typing): «Если это ходит как утка и крякает как утка, значит, это утка». Другими словами, Python важнее то, что объект умеет делать (его методы), а не то, чем он является (его класс). Если у объекта есть нужные методы, вы можете использовать его независимо от иерархии классов.
class FileWriter:
def __init__(self, filename):
self.filename = filename
def write(self, data):
print(f"Writing to {self.filename}: {data}")
class DatabaseWriter:
def __init__(self, table_name):
self.table_name = table_name
def write(self, data):
print(f"Inserting into {self.table_name}: {data}")
class ConsoleWriter:
def write(self, data):
print(f"Console output: {data}")
# Функция, которая работает с любым объектом, у которого есть метод write()
def save_data(writer, data):
writer.write(data)
# Все три класса работают, хотя они никак не связаны
file_writer = FileWriter("data.txt")
db_writer = DatabaseWriter("users")
console_writer = ConsoleWriter()
save_data(file_writer, "User data")
# Output: Writing to data.txt: User data
save_data(db_writer, "User data")
# Output: Inserting into users: User data
save_data(console_writer, "User data")
# Output: Console output: User dataНи один из этих классов не наследуется от общего родителя, но все они работают с save_data(), потому что у всех есть метод write(). Это утиная типизация — функции неважен класс, ей важен только интерфейс (доступные методы).
32.4.4) Практический пример: система плагинов
Полиморфизм позволяет создавать гибкие, расширяемые системы. Вот простая система плагинов для обработки данных:
class DataProcessor:
def process(self, data):
return data # Базовая реализация ничего не делает
class UppercaseProcessor(DataProcessor):
def process(self, data):
return data.upper()
class ReverseProcessor(DataProcessor):
def process(self, data):
return data[::-1]
class RemoveSpacesProcessor(DataProcessor):
def process(self, data):
return data.replace(" ", "")
class DataPipeline:
def __init__(self):
self.processors = []
def add_processor(self, processor):
self.processors.append(processor)
def run(self, data):
result = data
for processor in self.processors:
result = processor.process(result) # Полиморфный вызов
return result
# Собираем конвейер обработки
pipeline = DataPipeline()
pipeline.add_processor(UppercaseProcessor())
pipeline.add_processor(RemoveSpacesProcessor())
pipeline.add_processor(ReverseProcessor())
# Обрабатываем данные через конвейер
input_data = "Hello World"
output = pipeline.run(input_data)
print(f"Input: {input_data}") # Output: Input: Hello World
print(f"Output: {output}") # Output: Output: DLROWOLLEHDataPipeline не нужно знать, какие именно процессоры он содержит — он просто вызывает process() у каждого. Вы можете легко добавлять новые типы процессоров, не меняя код конвейера.
32.5) Проверка типов и отношений между классами (isinstance, issubclass)
32.5.1) Проверка типов экземпляров с помощью isinstance()
Иногда нужно проверить, является ли объект экземпляром определённого класса. Функция isinstance() делает именно это:
class Animal:
pass
class Dog(Animal):
pass
class Cat(Animal):
pass
buddy = Dog()
whiskers = Cat()
# Проверяем, является ли объект экземпляром класса
print(isinstance(buddy, Dog)) # Output: True
print(isinstance(buddy, Animal)) # Output: True (Dog inherits from Animal)
print(isinstance(buddy, Cat)) # Output: False
print(isinstance(whiskers, Cat)) # Output: True
print(isinstance(whiskers, Animal)) # Output: True
print(isinstance(whiskers, Dog)) # Output: FalseОбратите внимание, что isinstance(buddy, Animal) возвращает True, хотя buddy — экземпляр Dog. Это потому, что Dog наследуется от Animal, и поэтому экземпляр Dog также считается экземпляром Animal.
32.5.2) Почему isinstance() учитывает наследование
Функция isinstance() проверяет всю цепочку наследования:
class Vehicle:
pass
class Car(Vehicle):
pass
class ElectricCar(Car):
pass
tesla = ElectricCar()
# Проверяем все уровни наследования
print(isinstance(tesla, ElectricCar)) # Output: True
print(isinstance(tesla, Car)) # Output: True
print(isinstance(tesla, Vehicle)) # Output: True
print(isinstance(tesla, str)) # Output: FalseОбъект tesla является экземпляром ElectricCar, но также является экземпляром Car и Vehicle из-за наследования.
32.5.3) Проверка нескольких типов одновременно
Вы можете проверить, является ли объект экземпляром любого из нескольких классов, передав кортеж:
class Dog:
pass
class Cat:
pass
class Bird:
pass
def is_pet(animal):
return isinstance(animal, (Dog, Cat, Bird))
buddy = Dog()
whiskers = Cat()
tweety = Bird()
rock = "just a rock"
print(is_pet(buddy)) # Output: True
print(is_pet(whiskers)) # Output: True
print(is_pet(tweety)) # Output: True
print(is_pet(rock)) # Output: FalseЭто более кратко, чем писать isinstance(animal, Dog) or isinstance(animal, Cat) or isinstance(animal, Bird).
32.5.4) Проверка отношений между классами с помощью issubclass()
Функция issubclass() проверяет, является ли один класс подклассом другого:
class Animal:
pass
class Dog(Animal):
pass
class Cat(Animal):
pass
class Poodle(Dog):
pass
# Проверяем отношения между классами
print(issubclass(Dog, Animal)) # Output: True
print(issubclass(Cat, Animal)) # Output: True
print(issubclass(Poodle, Dog)) # Output: True
print(issubclass(Poodle, Animal)) # Output: True (indirect inheritance)
print(issubclass(Dog, Cat)) # Output: False
# Класс считается подклассом самого себя
print(issubclass(Dog, Dog)) # Output: TrueОбратите внимание, что issubclass() работает с классами, а не с экземплярами. Используйте isinstance() для экземпляров и issubclass() для классов.
32.5.5) Практические случаи использования проверки типов
Проверка типов полезна, когда вам нужно разное поведение для разных типов:
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
class Manager(Employee):
def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self.bonus = bonus
class Contractor:
def __init__(self, name, hourly_rate, hours):
self.name = name
self.hourly_rate = hourly_rate
self.hours = hours
def calculate_payment(worker):
# При использовании isinstance() с наследованием проверяйте подклассы раньше родительских классов
if isinstance(worker, Manager): # Сначала проверяем Manager
return worker.salary + worker.bonus
elif isinstance(worker, Employee): # Затем проверяем Employee (родительский класс)
return worker.salary
elif isinstance(worker, Contractor):
return worker.hourly_rate * worker.hours
else:
return 0
alice = Employee("Alice", 50000)
bob = Manager("Bob", 70000, 10000)
charlie = Contractor("Charlie", 50, 160)
print(f"Alice's payment: ${calculate_payment(alice)}") # Output: Alice's payment: $50000
print(f"Bob's payment: ${calculate_payment(bob)}") # Output: Bob's payment: $80000
print(f"Charlie's payment: ${calculate_payment(charlie)}")# Output: Charlie's payment: $8000Однако во многих случаях полиморфизм (когда каждый класс реализует общий метод) лучше, чем проверка типов:
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def calculate_payment(self):
return self.salary
class Manager(Employee):
def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self.bonus = bonus
def calculate_payment(self):
return self.salary + self.bonus
class Contractor:
def __init__(self, name, hourly_rate, hours):
self.name = name
self.hourly_rate = hourly_rate
self.hours = hours
def calculate_payment(self):
return self.hourly_rate * self.hours
# Проверка типов не нужна — полиморфизм справляется
workers = [
Employee("Alice", 50000),
Manager("Bob", 70000, 10000),
Contractor("Charlie", 50, 160)
]
for worker in workers:
payment = worker.calculate_payment() # Полиморфный вызов
print(f"{worker.name}'s payment: ${payment}")Output:
Alice's payment: $50000
Bob's payment: $80000
Charlie's payment: $8000Этот полиморфный подход более гибкий и его проще расширять новыми типами работников. Вам не нужно изменять вызывающий код, когда вы добавляете новый класс работника — просто убедитесь, что у него есть метод calculate_payment().
Наследование(inheritance) и полиморфизм(polymorphism) — мощные инструменты для организации кода и создания гибких, расширяемых систем. Создавая подклассы, вы можете повторно использовать существующий код, добавляя или изменяя поведение. Переопределяя методы, вы можете настраивать работу подклассов. А используя полиморфизм, вы можете писать код, который работает со многими разными классами через общий интерфейс.
Ключ в том, чтобы использовать эти возможности осмысленно:
- Используйте наследование(inheritance), когда действительно есть отношение «является» (
DogявляетсяAnimal) - Переопределяйте методы, чтобы специализировать поведение, а не чтобы полностью менять то, что делает класс
- Используйте
super()для расширения поведения родителя, а не для его полной замены - По возможности предпочитайте полиморфизм(polymorphism) (общие методы) проверке типов
По мере того как вы будете создавать более крупные программы, эти объектно-ориентированные техники помогут вам писать код, который проще понимать, сопровождать и расширять.