← Назад к вопросам

Как связаны принцип Барбары Лисков и полиморфизм?

3.0 Senior🔥 201 комментариев
#Python Core#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как связаны принцип Барбары Лисков и полиморфизм?

Принцип подстановки Лисков (Liskov Substitution Principle, LSP) — это фундаментальная основа правильного полиморфизма. Давайте разберёмся, как они связаны.

1. Определение LSP

Liskov Substitution Principle (LSP): "Функции, которые используют ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом."

Другими словами: если S — подтип T, то объекты типа S должны без проблем подставляться вместо объектов типа T.

2. LSP как основа полиморфизма

Полиморфизм работает ТОЛЬКО если выполняется LSP:

# Базовый класс
class Animal:
    def make_sound(self) -> str:
        return 'Some sound'

# Правильный подтип (соблюдает LSP)
class Dog(Animal):
    def make_sound(self) -> str:
        return 'Woof!'

class Cat(Animal):
    def make_sound(self) -> str:
        return 'Meow!'

# Полиморфная функция
def animal_sounds(animals: list[Animal]) -> None:
    for animal in animals:
        print(animal.make_sound())

# Это работает потому что Dog и Cat соблюдают LSP
animals = [Dog(), Cat(), Dog()]
animal_sounds(animals)  # Meow!, Woof!, Woof!
# Функция работает с любыми подтипами Animal

3. Нарушение LSP — проблемы с полиморфизмом

Пример нарушения LSP:

class Bird:
    def fly(self) -> str:
        return 'Flying...'

# ПЛОХО: Penguin нарушает LSP
class Penguin(Bird):
    def fly(self) -> str:
        raise NotImplementedError('Penguins cannot fly!')

# Полиморфная функция
def make_bird_fly(bird: Bird):
    return bird.fly()  # Ожидаем, что любая Bird может летать

# Падает!
bird = Penguin()
make_bird_fly(bird)  # NotImplementedError!
# LSP нарушена: Penguin не может заменить Bird

Правильное решение:

# Правильно: разделяем иерархию
class Bird:
    def move(self) -> str:
        pass

class FlyingBird(Bird):
    def fly(self) -> str:
        return 'Flying...'

class NonFlyingBird(Bird):
    def walk(self) -> str:
        return 'Walking...'

class Penguin(NonFlyingBird):
    def walk(self) -> str:
        return 'Waddling...'

class Sparrow(FlyingBird):
    def fly(self) -> str:
        return 'Flying high!'

# Теперь LSP соблюдается
def make_flying_bird_fly(bird: FlyingBird):
    return bird.fly()

make_flying_bird_fly(Sparrow())  # OK
make_flying_bird_fly(Penguin())  # Ошибка типа (правильно!)

4. LSP и контракты методов

LSP требует соблюдения контрактов — предусловий и постусловий:

class Rectangle:
    def __init__(self, width: float, height: float):
        self._width = width
        self._height = height
    
    def set_width(self, width: float) -> None:
        self._width = width
    
    def set_height(self, height: float) -> None:
        self._height = height
    
    def area(self) -> float:
        return self._width * self._height

# ПЛОХО: нарушает контракт Rectangle
class Square(Rectangle):
    def set_width(self, width: float) -> None:
        # Квадрат: ширина == высота
        self._width = width
        self._height = width  # Нарушение!
    
    def set_height(self, height: float) -> None:
        self._width = height
        self._height = height

# Проблема
def calculate_area(rect: Rectangle) -> float:
    rect.set_width(5)
    rect.set_height(4)
    # Ожидаем площадь 5 * 4 = 20
    return rect.area()

print(calculate_area(Rectangle(0, 0)))  # 20 ✓
print(calculate_area(Square(0, 0)))     # 16 ✗ (5*4 but 4*4)
# LSP нарушена!

# Правильное решение
class Shape:
    def area(self) -> float:
        pass

class Rectangle(Shape):
    def __init__(self, width: float, height: float):
        self._width = width
        self._height = height
    
    def area(self) -> float:
        return self._width * self._height

class Square(Shape):
    def __init__(self, side: float):
        self._side = side
    
    def area(self) -> float:
        return self._side * self._side

5. LSP и исключения

Потомок не должен выбрасывать неожиданные исключения:

class PaymentProcessor:
    def process(self, amount: float) -> bool:
        '''Обрабатывает платёж, возвращает успех'''
        pass

# ПЛОХО: выбрасывает неожиданное исключение
class CreditCardProcessor(PaymentProcessor):
    def process(self, amount: float) -> bool:
        if amount > 1000:
            raise ValueError('Amount too large')  # Нарушение!
        return True

# Клиентский код ломается
def pay(processor: PaymentProcessor, amount: float):
    try:
        result = processor.process(amount)
        return result
    except Exception as e:
        print(f'Payment failed: {e}')
        # Ожидали другие ошибки, не ValueError!

pay(CreditCardProcessor(), 1500)  # Неожиданное исключение

# Правильно
class CreditCardProcessor(PaymentProcessor):
    def process(self, amount: float) -> bool:
        if amount > 1000:
            return False  # Возвращаем False, не выбрасываем
        return True

6. LSP и return types (ковариантность)

Потомок может возвращать более специализированный тип (covariance):

class Animal:
    pass

class Dog(Animal):
    def bark(self):
        print('Woof!')

# Правильно: Dog — подтип Animal
class AnimalShelter:
    def get_animal(self) -> Animal:
        return Animal()

class DogShelter(AnimalShelter):
    def get_animal(self) -> Dog:  # Более специализированный тип
        return Dog()

# LSP соблюдается: Dog IS-A Animal
def adopt_pet(shelter: AnimalShelter):
    pet = shelter.get_animal()
    if isinstance(pet, Dog):
        pet.bark()

adopt_pet(DogShelter())  # OK

7. LSP и parameter types (контравариантность)

Потомок может принимать более общие типы (contravariance):

class EventHandler:
    def handle(self, event: 'ClickEvent') -> None:
        pass

class ClickEvent:
    pass

class CustomClickEvent(ClickEvent):
    pass

# ПРАВИЛЬНО: принимаем более общий тип
class UniversalEventHandler(EventHandler):
    def handle(self, event: 'Event') -> None:  # Event — более общий
        pass

class Event:
    pass

# LSP соблюдается
def process_event(handler: EventHandler, event: ClickEvent):
    handler.handle(event)  # Работает с обоими

process_event(EventHandler(), ClickEvent())
process_event(UniversalEventHandler(), ClickEvent())  # OK

8. LSP и Design by Contract

Укрепляйте инварианты через контракты:

class Account:
    def __init__(self, balance: float = 0):
        self.balance = balance
    
    def withdraw(self, amount: float) -> bool:
        """Снять деньги. Постусловие: баланс >= 0"""
        if amount <= self.balance:
            self.balance -= amount
            return True
        return False  # Не может быть отрицательный баланс

# Потомок должен соблюдать контракт
class OverdraftAccount(Account):
    def __init__(self, balance: float = 0, limit: float = 100):
        super().__init__(balance)
        self.overdraft_limit = limit
    
    def withdraw(self, amount: float) -> bool:
        """Снять деньги с овердрафтом. Постусловие: баланс >= -limit"""
        if amount <= self.balance + self.overdraft_limit:
            self.balance -= amount
            return True
        return False  # Усилили контракт, не ослабили

# Правильно: OverdraftAccount можно использовать везде где Account
def transfer(from_account: Account, to_account: Account, amount: float):
    if from_account.withdraw(amount):
        to_account.balance += amount

transfer(OverdraftAccount(100, 50), Account(50), 120)  # OK

9. LSP и реальный пример: Django ORM

from django.db import models

# Базовый класс QuerySet
class QuerySet:
    def filter(self, **kwargs):
        return self  # Возвращает QuerySet для chaining
    
    def all(self):
        return self
    
    def count(self):
        return 0

# Потомки соблюдают тот же контракт (LSP)
class Model:
    objects: QuerySet  # Manager, подтип QuerySet

class User(Model):
    name = models.CharField()
    email = models.CharField()

# Полиморфный код работает
def get_users(queryset: QuerySet):
    return queryset.filter(active=True).count()

get_users(User.objects)  # Работает благодаря LSP

10. Признаки нарушения LSP

# Антипаттерны

# 1. Type checking в клиентском коде
def process(obj: BaseClass):
    if isinstance(obj, SpecificClass):
        # Нарушение LSP!
        specific_method()
    else:
        normal_method()

# 2. Проверка перед вызовом
def use_animal(animal: Animal):
    if hasattr(animal, 'fly'):  # Нарушение!
        animal.fly()

# 3. Ловля специфичных исключений
def safe_operation(processor: Processor):
    try:
        processor.process()
    except SpecificException:  # Нарушение!
        handle_specific_case()

Вывод

Связь LSP и полиморфизма:

  1. Полиморфизм работает только если LSP соблюдена
  2. LSP гарантирует предсказуемое поведение потомков
  3. Иерархия классов должна быть логичной (не Rectangle vs Square)
  4. Контракты методов должны усиливаться, не ослабляться
  5. Клиентский код НЕ должен знать о конкретных типах

Это делает код гибким, расширяемым и безопасным для расширения.

Как связаны принцип Барбары Лисков и полиморфизм? | PrepBro