Python & AI Tutorials Logo
Программирование Python

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)). Подкласс автоматически наследует все методы и атрибуты родительского класса.

Когда вы создаёте подкласс, вы указываете родительский класс в круглых скобках после имени класса.

python
# Родительский класс
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 от своего родителя.

Animal

+name

+init(name)

+speak()

Dog

32.1.2) Почему наследование важно

Наследование решает распространённую проблему программирования: дублирование кода. Представьте, что вы строите систему для управления разными типами сотрудников:

python
# Без наследования — много дублирования
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() дублируются. С наследованием мы можем устранить это дублирование:

python
# С наследованием — общий код в родительском классе
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) Добавление новых методов в подклассы

Подклассы могут добавлять собственные методы, которых нет у родителя:

python
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__ подкласса:

python
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) Несколько уровней наследования

Классы могут наследоваться от классов, которые сами наследуются от других классов, создавая иерархию наследования:

python
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)

LivingThing

+name

+is_alive()

Animal

+species

+move()

Dog

+breed

+bark()

Dog наследуется от Animal, который наследуется от LivingThing. Это означает, что Dog имеет доступ к методам и атрибутам обоих родительских классов.

32.2) Переопределение методов в подклассах

32.2.1) Что означает переопределение методов

Иногда подклассу нужно изменить то, как работает унаследованный метод. Переопределение методов(method overriding) означает определение метода в подклассе с тем же именем, что и метод в родительском классе. Когда вы вызываете этот метод у экземпляра подкласса, Python использует версию подкласса вместо версии родителя.

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) Зачем переопределять методы?

Переопределение методов позволяет создавать специализированные версии общего поведения. Рассмотрим иерархию фигур:

python
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__ родителя, чтобы гарантировать выполнение инициализации родительского класса:

python
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() возвращает временный объект, который позволяет вызывать методы родительского класса, не называя родителя явно.

python
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__ родителя:

python
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() для расширения метода родителя, а не для полного замещения:

python
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():

python
class Vehicle:
    def __init__(self, brand):
        self.brand = brand
 
class Car(Vehicle):
    def __init__(self, brand, model):
        super().__init__(brand)  # Предпочтительно
        self.model = model

Используйте прямые вызовы родителя, когда вам нужно вызвать конкретного родителя в сценарии множественного наследования (рассмотрим позже) или когда вы хотите явно указать, какого родителя вы вызываете:

python
class Car(Vehicle):
    def __init__(self, brand, model):
        Vehicle.__init__(self, brand)  # Явно, но менее гибко
        self.model = model

При одиночном наследовании (один родитель) super() почти всегда — лучший выбор.

32.3.5) super() с другими методами

Вы можете использовать super() с любым методом, не только с __init__:

python
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 ERROR

32.4) Полиморфизм: работа с совместимыми классами

32.4.1) Что означает полиморфизм

Полиморфизм(polymorphism) (от греческого: «многие формы») — это возможность одинаково обращаться с объектами разных классов, если они предоставляют одни и те же методы.

В Python, если у нескольких классов есть методы с одинаковым именем, вы можете вызывать эти методы, не зная точный класс объекта.

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) Полиморфизм с наследованием

Полиморфизм особенно мощен вместе с наследованием, когда подклассы переопределяют методы родителя:

python
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 важнее то, что объект умеет делать (его методы), а не то, чем он является (его класс). Если у объекта есть нужные методы, вы можете использовать его независимо от иерархии классов.

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) Практический пример: система плагинов

Полиморфизм позволяет создавать гибкие, расширяемые системы. Вот простая система плагинов для обработки данных:

python
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: DLROWOLLEH

DataPipeline не нужно знать, какие именно процессоры он содержит — он просто вызывает process() у каждого. Вы можете легко добавлять новые типы процессоров, не меняя код конвейера.

32.5) Проверка типов и отношений между классами (isinstance, issubclass)

32.5.1) Проверка типов экземпляров с помощью isinstance()

Иногда нужно проверить, является ли объект экземпляром определённого класса. Функция isinstance() делает именно это:

python
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() проверяет всю цепочку наследования:

python
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

Объект tesla является экземпляром ElectricCar, но также является экземпляром Car и Vehicle из-за наследования.

32.5.3) Проверка нескольких типов одновременно

Вы можете проверить, является ли объект экземпляром любого из нескольких классов, передав кортеж:

python
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() проверяет, является ли один класс подклассом другого:

python
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) Практические случаи использования проверки типов

Проверка типов полезна, когда вам нужно разное поведение для разных типов:

python
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

Однако во многих случаях полиморфизм (когда каждый класс реализует общий метод) лучше, чем проверка типов:

python
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) (общие методы) проверке типов

По мере того как вы будете создавать более крупные программы, эти объектно-ориентированные техники помогут вам писать код, который проще понимать, сопровождать и расширять.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai