Python

Модуль 5: ООП в Python

Объектно-ориентированное программирование (ООП) — это парадигма программирования, которая использует объекты и классы для организации кода. Python — полноценный объектно-ориентированный язык, который предоставляет все необходимые инструменты для разработки в этом стиле.

В этом модуле вы изучите:

  • Классы и объекты — основные строительные блоки ООП
  • Наследование — механизм повторного использования кода
  • Полиморфизм — возможность использовать объекты разных классов через общий интерфейс
  • Инкапсуляцию — скрытие внутренних деталей реализации

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

5.1 Классы и объекты

Классы и объекты — фундаментальные концепции объектно-ориентированного программирования. Класс — это шаблон или чертеж, который определяет свойства (атрибуты) и поведение (методы) объектов. Объект — это конкретный экземпляр класса.

Ключевые понятия:

  • Класс — шаблон для создания объектов
  • Объект — экземпляр класса
  • Атрибуты — переменные, хранящие данные объекта
  • Методы — функции, определяющие поведение объекта
  • Конструктор — специальный метод для инициализации объекта

Создание класса

В Python класс определяется с помощью ключевого слова class:

class Автомобиль:
    """Класс для представления автомобиля."""
    
    # Атрибуты класса (общие для всех экземпляров)
    количество_колес = 4
    
    # Конструктор
    def __init__(self, марка, модель, год, цвет):
        # Атрибуты экземпляра (уникальные для каждого объекта)
        self.марка = марка
        self.модель = модель
        self.год = год
        self.цвет = цвет
        self.пробег = 0  # Начальное значение
    
    # Методы
    def информация(self):
        """Возвращает информацию об автомобиле."""
        return f"{self.марка} {self.модель}, {self.год}, {self.цвет}, пробег: {self.пробег} км"
    
    def ехать(self, расстояние):
        """Увеличивает пробег на указанное расстояние."""
        self.пробег += расстояние
        return f"Проехали {расстояние} км. Текущий пробег: {self.пробег} км"

Создание объектов (экземпляров класса)

После определения класса можно создавать его экземпляры:

# Создаем объекты класса Автомобиль
моя_машина = Автомобиль("Toyota", "Corolla", 2020, "белый")
другая_машина = Автомобиль("Honda", "Civic", 2019, "синий")

# Получаем доступ к атрибутам
print(моя_машина.марка)  # Toyota
print(другая_машина.цвет)  # синий

# Вызываем методы
print(моя_машина.информация())  # Toyota Corolla, 2020, белый, пробег: 0 км
print(моя_машина.ехать(100))  # Проехали 100 км. Текущий пробег: 100 км
print(моя_машина.ехать(50))  # Проехали 50 км. Текущий пробег: 150 км

# Доступ к атрибутам класса
print(Автомобиль.количество_колес)  # 4
print(моя_машина.количество_колес)  # 4 (доступ через экземпляр)

Метод __init__ (конструктор)

Метод __init__ — это специальный метод, который автоматически вызывается при создании нового экземпляра класса. Он используется для инициализации атрибутов объекта.

class Человек:
    def __init__(self, имя, возраст):
        self.имя = имя
        self.возраст = возраст
        print(f"Создан новый объект Человек: {имя}, {возраст} лет")

# При создании объекта автоматически вызывается __init__
человек1 = Человек("Иван", 30)  # Выведет: Создан новый объект Человек: Иван, 30 лет

О параметре self

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

Атрибуты класса и экземпляра

В Python существует два типа атрибутов:

  • Атрибуты класса — общие для всех экземпляров
  • Атрибуты экземпляра — уникальные для каждого объекта
class Студент:
    # Атрибут класса
    учебное_заведение = "Университет"
    
    def __init__(self, имя, курс):
        # Атрибуты экземпляра
        self.имя = имя
        self.курс = курс

# Создаем студентов
студент1 = Студент("Анна", 2)
студент2 = Студент("Петр", 3)

# Доступ к атрибутам экземпляра
print(студент1.имя)  # Анна
print(студент2.курс)  # 3

# Доступ к атрибутам класса
print(студент1.учебное_заведение)  # Университет
print(студент2.учебное_заведение)  # Университет

# Изменение атрибута класса
Студент.учебное_заведение = "Академия"
print(студент1.учебное_заведение)  # Академия
print(студент2.учебное_заведение)  # Академия

# Изменение атрибута экземпляра не влияет на другие экземпляры
студент1.курс = 4
print(студент1.курс)  # 4
print(студент2.курс)  # 3 (не изменился)

Методы экземпляра, класса и статические методы

В Python существует три типа методов:

1. Методы экземпляра

Это обычные методы, которые работают с конкретным экземпляром класса. Они принимают self в качестве первого параметра.

2. Методы класса

Методы класса работают с классом, а не с его экземплярами. Они определяются с помощью декоратора @classmethod и принимают cls (ссылку на класс) в качестве первого параметра.

3. Статические методы

Статические методы не работают ни с классом, ни с его экземплярами. Они определяются с помощью декоратора @staticmethod и не принимают специальных первых параметров.

class Математика:
    # Атрибут класса
    описание = "Класс для математических операций"
    
    def __init__(self, значение):
        # Атрибут экземпляра
        self.значение = значение
    
    # Метод экземпляра
    def удвоить(self):
        return self.значение * 2
    
    # Метод класса
    @classmethod
    def изменить_описание(cls, новое_описание):
        cls.описание = новое_описание
        return cls.описание
    
    # Статический метод
    @staticmethod
    def сложить(a, b):
        return a + b

# Использование методов экземпляра
мат = Математика(5)
print(мат.удвоить())  # 10

# Использование методов класса
Математика.изменить_описание("Новое описание")
print(Математика.описание)  # Новое описание
print(мат.описание)  # Новое описание

# Использование статических методов
print(Математика.сложить(10, 20))  # 30
print(мат.сложить(10, 20))  # 30 (можно вызвать и через экземпляр)

Магические методы (dunder методы)

Python предоставляет множество специальных методов, которые начинаются и заканчиваются двойным подчеркиванием (double underscore, или "dunder"). Эти методы позволяют определить, как объекты вашего класса будут взаимодействовать со встроенными функциями и операторами Python.

class Точка:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    # Строковое представление для пользователя
    def __str__(self):
        return f"Точка({self.x}, {self.y})"
    
    # Строковое представление для отладки
    def __repr__(self):
        return f"Точка(x={self.x}, y={self.y})"
    
    # Сложение точек
    def __add__(self, other):
        return Точка(self.x + other.x, self.y + other.y)
    
    # Сравнение точек
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    
    # Длина вектора от начала координат
    def __abs__(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5

# Создаем точки
точка1 = Точка(3, 4)
точка2 = Точка(1, 2)

# Используем магические методы
print(точка1)  # Вызывает __str__: Точка(3, 4)
print(repr(точка1))  # Вызывает __repr__: Точка(x=3, y=4)

# Сложение точек
точка3 = точка1 + точка2  # Вызывает __add__
print(точка3)  # Точка(4, 6)

# Сравнение точек
print(точка1 == точка2)  # Вызывает __eq__: False
print(точка1 == Точка(3, 4))  # True

# Вычисление длины вектора
print(abs(точка1))  # Вызывает __abs__: 5.0

Часто используемые магические методы:

  • __init__(self, ...) — конструктор
  • __str__(self) — строковое представление для пользователя (str())
  • __repr__(self) — строковое представление для отладки (repr())
  • __len__(self) — длина объекта (len())
  • __getitem__(self, key) — доступ по индексу/ключу (obj[key])
  • __setitem__(self, key, value) — установка значения по индексу/ключу (obj[key] = value)
  • __add__(self, other) — сложение (obj1 + obj2)
  • __eq__(self, other) — сравнение на равенство (obj1 == obj2)
  • __lt__(self, other) — сравнение "меньше" (obj1 < obj2)
  • __call__(self, ...) — вызов объекта как функции (obj())

Свойства (properties)

Свойства позволяют определить методы, которые будут вызываться при доступе к атрибуту, его изменении или удалении. Это позволяет контролировать доступ к атрибутам и добавлять дополнительную логику.

class Температура:
    def __init__(self, цельсий=0):
        self._цельсий = цельсий
    
    # Геттер - вызывается при доступе к свойству
    @property
    def цельсий(self):
        return self._цельсий
    
    # Сеттер - вызывается при изменении свойства
    @цельсий.setter
    def цельсий(self, значение):
        if значение < -273.15:
            raise ValueError("Температура не может быть ниже абсолютного нуля!")
        self._цельсий = значение
    
    # Другое свойство, вычисляемое на основе цельсия
    @property
    def фаренгейт(self):
        return self._цельсий * 9/5 + 32
    
    @фаренгейт.setter
    def фаренгейт(self, значение):
        self.цельсий = (значение - 32) * 5/9

# Использование свойств
температура = Температура(25)
print(температура.цельсий)  # 25
print(температура.фаренгейт)  # 77.0

# Изменение через сеттер
температура.цельсий = 30
print(температура.цельсий)  # 30
print(температура.фаренгейт)  # 86.0

температура.фаренгейт = 68
print(температура.цельсий)  # 20.0

# Проверка валидации
try:
    температура.цельсий = -300  # Вызовет ошибку
except ValueError as e:
    print(e)  # Температура не может быть ниже абсолютного нуля!

Практический пример: Создание класса Банковский счет

class БанковскийСчет:
    """Класс для представления банковского счета."""
    
    # Атрибут класса - процентная ставка для всех счетов
    процентная_ставка = 0.05
    
    def __init__(self, владелец, начальный_баланс=0):
        self.владелец = владелец
        self._баланс = начальный_баланс  # Защищенный атрибут
        self.история_операций = []
        
    @property
    def баланс(self):
        """Свойство для доступа к балансу счета."""
        return self._баланс
    
    def внести(self, сумма):
        """Вносит деньги на счет."""
        if сумма <= 0:
            raise ValueError("Сумма должна быть положительной")
        
        self._баланс += сумма
        self.история_операций.append(f"Внесено: {сумма}")
        return f"Внесено {сумма}. Новый баланс: {self._баланс}"
    
    def снять(self, сумма):
        """Снимает деньги со счета."""
        if сумма <= 0:
            raise ValueError("Сумма должна быть положительной")
        
        if сумма > self._баланс:
            raise ValueError("Недостаточно средств")
        
        self._баланс -= сумма
        self.история_операций.append(f"Снято: {сумма}")
        return f"Снято {сумма}. Новый баланс: {self._баланс}"
    
    def начислить_проценты(self):
        """Начисляет проценты на баланс счета."""
        проценты = self._баланс * self.процентная_ставка
        self._баланс += проценты
        self.история_операций.append(f"Начислены проценты: {проценты:.2f}")
        return f"Начислены проценты: {проценты:.2f}. Новый баланс: {self._баланс:.2f}"
    
    def показать_историю(self):
        """Возвращает историю операций по счету."""
        return "\n".join(self.история_операций)
    
    def __str__(self):
        return f"Счет владельца {self.владелец}, баланс: {self._баланс}"
    
    @classmethod
    def изменить_ставку(cls, новая_ставка):
        """Изменяет процентную ставку для всех счетов."""
        if новая_ставка < 0:
            raise ValueError("Ставка не может быть отрицательной")
        cls.процентная_ставка = новая_ставка
        return f"Новая процентная ставка: {новая_ставка}"

# Использование класса БанковскийСчет
счет_анны = БанковскийСчет("Анна Иванова", 1000)
счет_петра = БанковскийСчет("Петр Сидоров")

print(счет_анны)  # Счет владельца Анна Иванова, баланс: 1000

# Операции со счетом
print(счет_анны.внести(500))  # Внесено 500. Новый баланс: 1500
print(счет_анны.снять(200))  # Снято 200. Новый баланс: 1300
print(счет_анны.начислить_проценты())  # Начислены проценты: 65.00. Новый баланс: 1365.00

# Изменение процентной ставки для всех счетов
БанковскийСчет.изменить_ставку(0.06)
print(БанковскийСчет.процентная_ставка)  # 0.06
print(счет_анны.процентная_ставка)  # 0.06
print(счет_петра.процентная_ставка)  # 0.06

# История операций
print("\nИстория операций:")
print(счет_анны.показать_историю())

Советы по работе с классами и объектами

  • Следуйте соглашению: имена классов пишутся в CamelCase (каждое слово с большой буквы)
  • Всегда добавляйте docstring к классам и методам
  • Используйте атрибуты класса для данных, общих для всех экземпляров
  • Используйте свойства (properties) для контроля доступа к атрибутам
  • Создавайте классы с единственной ответственностью (принцип единственной ответственности)
  • Помните, что в Python все атрибуты публичны по умолчанию

5.2 Наследование

Наследование — один из ключевых принципов объектно-ориентированного программирования, который позволяет создавать новые классы на основе существующих. Дочерний класс (подкласс) наследует атрибуты и методы родительского класса (суперкласса), а также может добавлять новые или переопределять существующие.

Ключевые понятия:

  • Родительский класс (суперкласс) — класс, от которого наследуются другие классы
  • Дочерний класс (подкласс) — класс, который наследуется от другого класса
  • Переопределение методов — изменение поведения унаследованных методов
  • Множественное наследование — наследование от нескольких классов одновременно
  • Иерархия классов — древовидная структура наследования

Базовое наследование

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

class Животное:
    """Базовый класс для всех животных."""
    
    def __init__(self, имя, возраст):
        self.имя = имя
        self.возраст = возраст
    
    def издать_звук(self):
        return "Какой-то звук животного"
    
    def информация(self):
        return f"{self.имя}, возраст: {self.возраст} лет"

# Дочерний класс Собака наследуется от класса Животное
class Собака(Животное):
    """Класс для представления собаки."""
    
    def __init__(self, имя, возраст, порода):
        # Вызываем конструктор родительского класса
        super().__init__(имя, возраст)
        # Добавляем новый атрибут
        self.порода = порода
    
    # Переопределяем метод родительского класса
    def издать_звук(self):
        return "Гав!"
    
    # Добавляем новый метод
    def вилять_хвостом(self):
        return f"{self.имя} виляет хвостом"

# Создаем экземпляры
животное = Животное("Безымянное животное", 5)
собака = Собака("Рекс", 3, "Овчарка")

# Используем методы
print(животное.информация())  # Безымянное животное, возраст: 5 лет
print(животное.издать_звук())  # Какой-то звук животного

print(собака.информация())  # Рекс, возраст: 3 лет (унаследовано от Животное)
print(собака.издать_звук())  # Гав! (переопределено)
print(собака.вилять_хвостом())  # Рекс виляет хвостом (новый метод)
print(f"Порода: {собака.порода}")  # Порода: Овчарка (новый атрибут)

Функция super()

Функция super() используется для вызова методов родительского класса. Это особенно полезно при переопределении методов, когда вы хотите расширить функциональность родительского метода, а не полностью заменить его.

class Сотрудник:
    def __init__(self, имя, зарплата):
        self.имя = имя
        self.зарплата = зарплата
    
    def информация(self):
        return f"Сотрудник: {self.имя}, зарплата: {self.зарплата} руб."
    
    def повысить_зарплату(self, сумма):
        self.зарплата += сумма
        return f"Зарплата повышена на {сумма} руб. Новая зарплата: {self.зарплата} руб."

class Менеджер(Сотрудник):
    def __init__(self, имя, зарплата, отдел):
        # Вызываем конструктор родительского класса
        super().__init__(имя, зарплата)
        self.отдел = отдел
    
    # Расширяем метод информация, добавляя информацию об отделе
    def информация(self):
        # Получаем базовую информацию из родительского класса
        базовая_информация = super().информация()
        # Добавляем информацию об отделе
        return f"{базовая_информация}, отдел: {self.отдел}"
    
    # Добавляем новый метод
    def управлять_отделом(self):
        return f"{self.имя} управляет отделом {self.отдел}"

# Создаем экземпляры
сотрудник = Сотрудник("Иван Петров", 50000)
менеджер = Менеджер("Анна Сидорова", 80000, "Маркетинг")

# Используем методы
print(сотрудник.информация())  # Сотрудник: Иван Петров, зарплата: 50000 руб.
print(менеджер.информация())  # Сотрудник: Анна Сидорова, зарплата: 80000 руб., отдел: Маркетинг

# Вызываем унаследованный метод
print(менеджер.повысить_зарплату(10000))  # Зарплата повышена на 10000 руб. Новая зарплата: 90000 руб.

# Вызываем новый метод
print(менеджер.управлять_отделом())  # Анна Сидорова управляет отделом Маркетинг

Проверка наследования

Python предоставляет несколько способов проверить отношения наследования между классами и объектами:

# Проверяем, является ли объект экземпляром класса
print(isinstance(собака, Собака))  # True
print(isinstance(собака, Животное))  # True (т.к. Собака наследуется от Животное)
print(isinstance(животное, Собака))  # False

# Проверяем, является ли класс подклассом другого класса
print(issubclass(Собака, Животное))  # True
print(issubclass(Животное, Собака))  # False

# Получаем список базовых классов
print(Собака.__bases__)  # (,)

Множественное наследование

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

class Работник:
    def __init__(self, имя, зарплата):
        self.имя = имя
        self.зарплата = зарплата
    
    def работать(self):
        return f"{self.имя} работает"

class Студент:
    def __init__(self, имя, университет):
        self.имя = имя
        self.университет = университет
    
    def учиться(self):
        return f"{self.имя} учится в {self.университет}"

# Множественное наследование
class СтудентРаботник(Работник, Студент):
    def __init__(self, имя, зарплата, университет):
        # Явно вызываем конструкторы обоих родительских классов
        Работник.__init__(self, имя, зарплата)
        Студент.__init__(self, имя, университет)
    
    def информация(self):
        return f"{self.имя} работает за {self.зарплата} руб. и учится в {self.университет}"

# Создаем экземпляр
студент_работник = СтудентРаботник("Алексей", 30000, "МГУ")

# Используем методы из обоих родительских классов
print(студент_работник.работать())  # Алексей работает
print(студент_работник.учиться())  # Алексей учится в МГУ
print(студент_работник.информация())  # Алексей работает за 30000 руб. и учится в МГУ

О порядке разрешения методов (MRO)

При множественном наследовании может возникнуть ситуация, когда метод с одинаковым именем определен в нескольких родительских классах. Python использует порядок разрешения методов (Method Resolution Order, MRO) для определения, какой метод будет вызван.

MRO определяет порядок, в котором Python ищет методы в иерархии классов. Посмотреть MRO для класса можно с помощью атрибута __mro__ или метода mro():

print(СтудентРаботник.__mro__)
# (, , , )

В этом примере, если метод определен и в Работник, и в Студент, будет использован метод из класса Работник, так как он стоит первым в MRO.

Абстрактные классы

Абстрактный класс — это класс, который не предназначен для создания экземпляров, а служит только в качестве базового класса для других классов. В Python для создания абстрактных классов используется модуль abc (Abstract Base Classes).

from abc import ABC, abstractmethod

class ТранспортноеСредство(ABC):
    def __init__(self, название):
        self.название = название
    
    @abstractmethod
    def двигаться(self):
        """Этот метод должен быть реализован во всех подклассах."""
        pass
    
    @abstractmethod
    def остановиться(self):
        """Этот метод должен быть реализован во всех подклассах."""
        pass
    
    def информация(self):
        return f"Транспортное средство: {self.название}"

class Автомобиль(ТранспортноеСредство):
    def __init__(self, название, марка):
        super().__init__(название)
        self.марка = марка
    
    # Реализация абстрактного метода
    def двигаться(self):
        return f"{self.название} едет по дороге"
    
    # Реализация абстрактного метода
    def остановиться(self):
        return f"{self.название} остановился"

class Самолет(ТранспортноеСредство):
    def __init__(self, название, авиакомпания):
        super().__init__(название)
        self.авиакомпания = авиакомпания
    
    # Реализация абстрактного метода
    def двигаться(self):
        return f"{self.название} летит по воздуху"
    
    # Реализация абстрактного метода
    def остановиться(self):
        return f"{self.название} приземлился"
    
    # Дополнительный метод
    def взлететь(self):
        return f"{self.название} взлетает"

# Попытка создать экземпляр абстрактного класса вызовет ошибку
try:
    транспорт = ТранспортноеСредство("Транспорт")
except TypeError as e:
    print(f"Ошибка: {e}")  # Ошибка: Can't instantiate abstract class ТранспортноеСредство with abstract methods двигаться, остановиться

# Создаем экземпляры конкретных классов
автомобиль = Автомобиль("Моя машина", "Toyota")
самолет = Самолет("Рейс 123", "Аэрофлот")

# Используем методы
print(автомобиль.двигаться())  # Моя машина едет по дороге
print(самолет.двигаться())  # Рейс 123 летит по воздуху
print(самолет.взлететь())  # Рейс 123 взлетает

Наследование встроенных типов

В Python можно наследоваться от встроенных типов, таких как list, dict, str и т.д., чтобы расширить их функциональность:

class РасширенныйСписок(list):
    """Расширенный список с дополнительными методами."""
    
    def сумма(self):
        """Возвращает сумму всех элементов списка."""
        return sum(self)
    
    def среднее(self):
        """Возвращает среднее значение элементов списка."""
        if not self:
            return 0
        return self.сумма() / len(self)
    
    def только_положительные(self):
        """Возвращает новый список только с положительными элементами."""
        return РасширенныйСписок([x for x in self if x > 0])

# Создаем экземпляр
мой_список = РасширенныйСписок([1, -2, 3, -4, 5])

# Используем методы из базового класса list
мой_список.append(6)
мой_список.extend([7, -8])
print(мой_список)  # [1, -2, 3, -4, 5, 6, 7, -8]
print(len(мой_список))  # 8

# Используем новые методы
print(мой_список.сумма())  # 8
print(мой_список.среднее())  # 1.0
print(мой_список.только_положительные())  # [1, 3, 5, 6, 7]

Практический пример: Иерархия классов для интернет-магазина

class Товар:
    """Базовый класс для всех товаров в магазине."""
    
    def __init__(self, название, цена, артикул):
        self.название = название
        self.цена = цена
        self.артикул = артикул
        self.в_наличии = True
    
    def информация(self):
        статус = "В наличии" if self.в_наличии else "Нет в наличии"
        return f"{self.название} (арт. {self.артикул}), цена: {self.цена} руб., {статус}"
    
    def изменить_цену(self, новая_цена):
        self.цена = новая_цена
        return f"Цена на {self.название} изменена на {новая_цена} руб."
    
    def продать(self):
        if self.в_наличии:
            self.в_наличии = False
            return f"{self.название} продан"
        return f"{self.название} нет в наличии"

class Электроника(Товар):
    """Класс для электронных товаров."""
    
    def __init__(self, название, цена, артикул, гарантия):
        super().__init__(название, цена, артикул)
        self.гарантия = гарантия  # в месяцах
    
    def информация(self):
        базовая_информация = super().информация()
        return f"{базовая_информация}, гарантия: {self.гарантия} мес."
    
    def продлить_гарантию(self, месяцы):
        self.гарантия += месяцы
        return f"Гарантия на {self.название} продлена на {месяцы} мес. Новая гарантия: {self.гарантия} мес."

class Смартфон(Электроника):
    """Класс для смартфонов."""
    
    def __init__(self, название, цена, артикул, гарантия, операционная_система, память):
        super().__init__(название, цена, артикул, гарантия)
        self.операционная_система = операционная_система
        self.память = память  # в ГБ
    
    def информация(self):
        базовая_информация = super().информация()
        return f"{базовая_информация}, ОС: {self.операционная_система}, память: {self.память} ГБ"

class Книга(Товар):
    """Класс для книг."""
    
    def __init__(self, название, цена, артикул, автор, жанр, страницы):
        super().__init__(название, цена, артикул)
        self.автор = автор
        self.жанр = жанр
        self.страницы = страницы
    
    def информация(self):
        базовая_информация = super().информация()
        return f"{базовая_информация}, автор: {self.автор}, жанр: {self.жанр}, {self.страницы} стр."

# Создаем экземпляры
телефон = Смартфон("iPhone 13", 79990, "SM001", 12, "iOS", 128)
книга = Книга("Война и мир", 950, "BK001", "Л.Н. Толстой", "Роман", 1225)

# Используем методы
print(телефон.информация())
# iPhone 13 (арт. SM001), цена: 79990 руб., В наличии, гарантия: 12 мес., ОС: iOS, память: 128 ГБ

print(книга.информация())
# Война и мир (арт. BK001), цена: 950 руб., В наличии, автор: Л.Н. Толстой, жанр: Роман, 1225 стр.

print(телефон.продлить_гарантию(6))
# Гарантия на iPhone 13 продлена на 6 мес. Новая гарантия: 18 мес.

print(книга.продать())
# Война и мир продан

print(книга.информация())
# Война и мир (арт. BK001), цена: 950 руб., Нет в наличии, автор: Л.Н. Толстой, жанр: Роман, 1225 стр.

Советы по использованию наследования

  • Используйте наследование, когда между классами существует отношение "является" (is-a): Собака является Животным
  • Избегайте глубоких иерархий наследования (более 2-3 уровней)
  • Предпочитайте композицию наследованию, когда это возможно (отношение "имеет" (has-a))
  • При множественном наследовании будьте осторожны с проблемой ромбовидного наследования
  • Используйте super() для вызова методов родительского класса
  • Создавайте абстрактные базовые классы для определения общих интерфейсов

5.3 Полиморфизм

Полиморфизм — один из основных принципов объектно-ориентированного программирования, который позволяет использовать объекты разных классов через общий интерфейс. Термин "полиморфизм" происходит от греческих слов "поли" (много) и "морф" (форма), что означает "много форм".

Ключевые понятия:

  • Полиморфизм — способность объектов разных классов реагировать на одинаковые методы по-разному
  • Интерфейс — набор методов, которые должны быть реализованы классом
  • Утиная типизация — "если нечто выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, и есть утка"
  • Перегрузка операторов — определение поведения операторов для пользовательских классов

Полиморфизм подтипов

Полиморфизм подтипов основан на наследовании и позволяет использовать объекты дочерних классов там, где ожидаются объекты родительского класса. Это возможно, потому что дочерние классы наследуют интерфейс родительского класса.

class Фигура:
    """Базовый класс для геометрических фигур."""
    
    def __init__(self, название):
        self.название = название
    
    def площадь(self):
        """Вычисляет площадь фигуры."""
        raise NotImplementedError("Метод должен быть переопределен в дочернем классе")
    
    def периметр(self):
        """Вычисляет периметр фигуры."""
        raise NotImplementedError("Метод должен быть переопределен в дочернем классе")
    
    def описание(self):
        """Возвращает описание фигуры."""
        return f"Это {self.название}"

class Прямоугольник(Фигура):
    """Класс для прямоугольников."""
    
    def __init__(self, ширина, высота):
        super().__init__("прямоугольник")
        self.ширина = ширина
        self.высота = высота
    
    def площадь(self):
        return self.ширина * self.высота
    
    def периметр(self):
        return 2 * (self.ширина + self.высота)
    
    def описание(self):
        базовое_описание = super().описание()
        return f"{базовое_описание} с шириной {self.ширина} и высотой {self.высота}"

class Круг(Фигура):
    """Класс для кругов."""
    
    def __init__(self, радиус):
        super().__init__("круг")
        self.радиус = радиус
    
    def площадь(self):
        import math
        return math.pi * self.радиус ** 2
    
    def периметр(self):
        import math
        return 2 * math.pi * self.радиус
    
    def описание(self):
        базовое_описание = super().описание()
        return f"{базовое_описание} с радиусом {self.радиус}"

# Функция, которая работает с любой фигурой
def вывести_информацию(фигура):
    """Выводит информацию о фигуре."""
    print(фигура.описание())
    print(f"Площадь: {фигура.площадь():.2f}")
    print(f"Периметр: {фигура.периметр():.2f}")
    print()

# Создаем разные фигуры
прямоугольник = Прямоугольник(5, 3)
круг = Круг(4)

# Используем полиморфизм - одна функция работает с разными типами
вывести_информацию(прямоугольник)
# Это прямоугольник с шириной 5 и высотой 3
# Площадь: 15.00
# Периметр: 16.00

вывести_информацию(круг)
# Это круг с радиусом 4
# Площадь: 50.27
# Периметр: 25.13

Утиная типизация

Python использует "утиную типизацию" (duck typing), что означает, что тип объекта определяется его поведением, а не явным наследованием. Если объект имеет методы и свойства, которые ожидаются в определенном контексте, то он может быть использован в этом контексте, независимо от его фактического класса.

class Утка:
    def крякать(self):
        return "Кря-кря!"
    
    def плавать(self):
        return "Утка плавает"

class РобоУтка:
    def крякать(self):
        return "Робо-кря!"
    
    def плавать(self):
        return "РобоУтка плавает"

# Здесь нет общего базового класса, но оба класса имеют одинаковые методы

def утиные_действия(утка):
    """Функция работает с любым объектом, который имеет методы крякать и плавать."""
    print(утка.крякать())
    print(утка.плавать())
    print()

# Создаем объекты
утка = Утка()
робоутка = РобоУтка()

# Используем утиную типизацию
утиные_действия(утка)
# Кря-кря!
# Утка плавает

утиные_действия(робоутка)
# Робо-кря!
# РобоУтка плавает

Преимущества утиной типизации

Утиная типизация делает код более гибким и менее зависимым от иерархии классов. Вместо того чтобы требовать, чтобы объекты были определенного типа, мы просто ожидаем, что они будут поддерживать определенные операции. Это позволяет использовать в коде объекты, которые не были изначально спроектированы для работы вместе, но имеют совместимые интерфейсы.

Перегрузка операторов

Перегрузка операторов — это форма полиморфизма, которая позволяет определить, как операторы Python (+, -, *, / и т.д.) будут работать с объектами пользовательских классов. Для этого используются специальные методы (магические методы).

class Вектор:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    # Перегрузка оператора сложения (+)
    def __add__(self, other):
        return Вектор(self.x + other.x, self.y + other.y)
    
    # Перегрузка оператора вычитания (-)
    def __sub__(self, other):
        return Вектор(self.x - other.x, self.y - other.y)
    
    # Перегрузка оператора умножения на скаляр (*)
    def __mul__(self, скаляр):
        return Вектор(self.x * скаляр, self.y * скаляр)
    
    # Перегрузка оператора сравнения (==)
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    
    # Строковое представление
    def __str__(self):
        return f"Вектор({self.x}, {self.y})"

# Создаем векторы
v1 = Вектор(3, 4)
v2 = Вектор(1, 2)

# Используем перегруженные операторы
v3 = v1 + v2  # Вызывает __add__
print(v3)  # Вектор(4, 6)

v4 = v1 - v2  # Вызывает __sub__
print(v4)  # Вектор(2, 2)

v5 = v1 * 2  # Вызывает __mul__
print(v5)  # Вектор(6, 8)

print(v1 == v2)  # False, вызывает __eq__
print(v1 == Вектор(3, 4))  # True

Полиморфизм и встроенные функции

Многие встроенные функции Python, такие как len(), str(), iter(), работают с разными типами данных благодаря полиморфизму. Каждый тип данных реализует соответствующие магические методы, которые вызываются этими функциями.

class КолодаКарт:
    def __init__(self):
        self.карты = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
    
    # Реализация для len()
    def __len__(self):
        return len(self.карты)
    
    # Реализация для str()
    def __str__(self):
        return f"Колода из {len(self)} карт"
    
    # Реализация для iter()
    def __iter__(self):
        return iter(self.карты)

# Создаем колоду
колода = КолодаКарт()

# Используем встроенные функции
print(len(колода))  # 13, вызывает __len__
print(str(колода))  # "Колода из 13 карт", вызывает __str__

# Итерация по объекту
for карта in колода:  # вызывает __iter__
    print(карта, end=' ')
# 2 3 4 5 6 7 8 9 10 J Q K A

Абстрактные базовые классы (ABC)

Абстрактные базовые классы (ABC) предоставляют способ формально определить интерфейсы в Python. Они гарантируют, что дочерние классы реализуют определенные методы, что делает полиморфизм более надежным.

from abc import ABC, abstractmethod

class Животное(ABC):
    @abstractmethod
    def голос(self):
        """Каждое животное должно иметь метод голос."""
        pass
    
    @abstractmethod
    def движение(self):
        """Каждое животное должно иметь метод движение."""
        pass
    
    def дышать(self):
        """Этот метод уже реализован и может быть унаследован."""
        return "Животное дышит"

class Собака(Животное):
    def голос(self):
        return "Гав!"
    
    def движение(self):
        return "Собака бежит"

class Птица(Животное):
    def голос(self):
        return "Чирик!"
    
    def движение(self):
        return "Птица летит"
    
    # Переопределяем метод дышать
    def дышать(self):
        базовое_дыхание = super().дышать()
        return f"{базовое_дыхание} через воздушные мешки"

# Создаем объекты
собака = Собака()
птица = Птица()

# Используем полиморфизм
животные = [собака, птица]
for животное in животные:
    print(f"Голос: {животное.голос()}")
    print(f"Движение: {животное.движение()}")
    print(f"Дыхание: {животное.дышать()}")
    print()

# Голос: Гав!
# Движение: Собака бежит
# Дыхание: Животное дышит
#
# Голос: Чирик!
# Движение: Птица летит
# Дыхание: Животное дышит через воздушные мешки

Практический пример: Обработка различных типов данных

class ОбработчикДанных:
    """Класс для демонстрации полиморфизма при обработке различных типов данных."""
    
    @staticmethod
    def обработать(данные):
        """Обрабатывает данные в зависимости от их типа."""
        if hasattr(данные, "items"):  # Словарь или похожий на словарь объект
            return ОбработчикДанных._обработать_словарь(данные)
        elif hasattr(данные, "__iter__") and not isinstance(данные, str):  # Итерируемый объект, но не строка
            return ОбработчикДанных._обработать_список(данные)
        elif isinstance(данные, str):  # Строка
            return ОбработчикДанных._обработать_строку(данные)
        elif isinstance(данные, (int, float)):  # Число
            return ОбработчикДанных._обработать_число(данные)
        else:
            return f"Неизвестный тип данных: {type(данные).__name__}"
    
    @staticmethod
    def _обработать_словарь(словарь):
        """Обрабатывает словарь."""
        результат = {}
        for ключ, значение in словарь.items():
            результат[ключ.upper() if isinstance(ключ, str) else ключ] = значение
        return результат
    
    @staticmethod
    def _обработать_список(список):
        """Обрабатывает список или другой итерируемый объект."""
        return [x * 2 if isinstance(x, (int, float)) else x for x in список]
    
    @staticmethod
    def _обработать_строку(строка):
        """Обрабатывает строку."""
        return строка.upper()
    
    @staticmethod
    def _обработать_число(число):
        """Обрабатывает число."""
        return число * 2

# Используем полиморфизм для обработки различных типов данных
данные = [
    {"имя": "Иван", "возраст": 30},
    [1, 2, 3, 4],
    "привет, мир!",
    42,
    3.14
]

for элемент in данные:
    результат = ОбработчикДанных.обработать(элемент)
    print(f"Тип: {type(элемент).__name__}, Результат: {результат}")

# Тип: dict, Результат: {'ИМЯ': 'Иван', 'ВОЗРАСТ': 30}
# Тип: list, Результат: [2, 4, 6, 8]
# Тип: str, Результат: ПРИВЕТ, МИР!
# Тип: int, Результат: 84
# Тип: float, Результат: 6.28

Советы по использованию полиморфизма

  • Определяйте общие интерфейсы для классов, которые должны работать взаимозаменяемо
  • Используйте абстрактные базовые классы для формального определения интерфейсов
  • Помните о "утиной типизации" — объекты должны поддерживать ожидаемые методы
  • Перегружайте операторы только тогда, когда это делает код более понятным
  • Избегайте проверок типов (isinstance) там, где можно использовать полиморфизм
  • Стремитесь к тому, чтобы функции работали с разными типами данных без изменения их кода

5.4 Инкапсуляция

Инкапсуляция — один из фундаментальных принципов объектно-ориентированного программирования, который заключается в скрытии внутренних деталей реализации объекта и предоставлении внешнего интерфейса для взаимодействия с ним. Инкапсуляция позволяет защитить данные от случайного изменения и обеспечивает целостность объекта.

Ключевые понятия:

  • Инкапсуляция — скрытие внутренних деталей и данных объекта от внешнего мира
  • Контроль доступа — ограничение доступа к атрибутам и методам объекта
  • Геттеры и сеттеры — методы для контролируемого доступа к атрибутам
  • Свойства (properties) — механизм Python для реализации геттеров и сеттеров

Уровни доступа в Python

В отличие от многих других объектно-ориентированных языков, Python не имеет строгих механизмов для обеспечения инкапсуляции. Вместо этого он следует принципу "мы все взрослые здесь" и использует соглашения об именовании для указания уровня доступа к атрибутам и методам:

  • Публичные атрибуты и методы — обычные имена без подчеркиваний (например, name, age)
  • Защищенные атрибуты и методы — имена с одним подчеркиванием в начале (например, _name, _age)
  • Приватные атрибуты и методы — имена с двумя подчеркиваниями в начале (например, __name, __age)
class Пользователь:
    def __init__(self, имя, возраст, пароль):
        self.имя = имя  # Публичный атрибут
        self._возраст = возраст  # Защищенный атрибут
        self.__пароль = пароль  # Приватный атрибут
    
    def публичный_метод(self):
        """Публичный метод, доступный извне класса."""
        return f"Привет, {self.имя}!"
    
    def _защищенный_метод(self):
        """Защищенный метод, не рекомендуется вызывать извне класса."""
        return f"Возраст: {self._возраст}"
    
    def __приватный_метод(self):
        """Приватный метод, предназначен только для внутреннего использования."""
        return f"Пароль: {self.__пароль}"
    
    def получить_информацию(self):
        """Публичный метод, который использует приватный метод."""
        return f"{self.публичный_метод()} {self._защищенный_метод()}"

# Создаем объект
пользователь = Пользователь("Анна", 25, "секретный_пароль")

# Доступ к публичным атрибутам и методам
print(пользователь.имя)  # Анна
print(пользователь.публичный_метод())  # Привет, Анна!

# Доступ к защищенным атрибутам и методам (возможен, но не рекомендуется)
print(пользователь._возраст)  # 25
print(пользователь._защищенный_метод())  # Возраст: 25

# Попытка доступа к приватным атрибутам и методам
try:
    print(пользователь.__пароль)  # Вызовет AttributeError
except AttributeError as e:
    print(f"Ошибка: {e}")

try:
    print(пользователь.__приватный_метод())  # Вызовет AttributeError
except AttributeError as e:
    print(f"Ошибка: {e}")

# Использование публичного интерфейса
print(пользователь.получить_информацию())  # Привет, Анна! Возраст: 25

О приватных атрибутах в Python

В Python приватные атрибуты (с двумя подчеркиваниями) на самом деле не являются строго приватными. Они реализуются с помощью механизма, называемого "name mangling" (искажение имен). Имя атрибута __attr в классе MyClass преобразуется в _MyClass__attr. Это означает, что к приватным атрибутам все еще можно получить доступ извне, но для этого нужно знать, как работает механизм искажения имен:

# Доступ к приватному атрибуту через искаженное имя
print(пользователь._Пользователь__пароль)  # секретный_пароль

Геттеры и сеттеры

Геттеры и сеттеры — это методы, которые используются для контролируемого доступа к атрибутам объекта. Они позволяют добавить логику проверки и обработки данных при чтении и записи атрибутов.

class Человек:
    def __init__(self, имя, возраст):
        self.__имя = имя
        self.__возраст = возраст
    
    # Геттер для имени
    def get_имя(self):
        return self.__имя
    
    # Сеттер для имени
    def set_имя(self, имя):
        if not имя:
            raise ValueError("Имя не может быть пустым")
        self.__имя = имя
    
    # Геттер для возраста
    def get_возраст(self):
        return self.__возраст
    
    # Сеттер для возраста
    def set_возраст(self, возраст):
        if возраст < 0 or возраст > 120:
            raise ValueError("Возраст должен быть от 0 до 120")
        self.__возраст = возраст

# Создаем объект
человек = Человек("Иван", 30)

# Используем геттеры и сеттеры
print(человек.get_имя())  # Иван
print(человек.get_возраст())  # 30

человек.set_имя("Петр")
человек.set_возраст(35)

print(человек.get_имя())  # Петр
print(человек.get_возраст())  # 35

# Проверка валидации
try:
    человек.set_возраст(150)  # Вызовет ошибку
except ValueError as e:
    print(f"Ошибка: {e}")  # Ошибка: Возраст должен быть от 0 до 120

Свойства (properties)

Python предоставляет механизм свойств (properties), который позволяет использовать методы для доступа к атрибутам, но при этом сохранить синтаксис прямого доступа. Это делает код более чистым и удобным для использования.

class Человек:
    def __init__(self, имя, возраст):
        self.__имя = имя
        self.__возраст = возраст
    
    # Свойство для имени
    @property
    def имя(self):
        """Геттер для имени."""
        return self.__имя
    
    @имя.setter
    def имя(self, значение):
        """Сеттер для имени."""
        if not значение:
            raise ValueError("Имя не может быть пустым")
        self.__имя = значение
    
    # Свойство для возраста
    @property
    def возраст(self):
        """Геттер для возраста."""
        return self.__возраст
    
    @возраст.setter
    def возраст(self, значение):
        """Сеттер для возраста."""
        if значение < 0 or значение > 120:
            raise ValueError("Возраст должен быть от 0 до 120")
        self.__возраст = значение
    
    # Свойство только для чтения
    @property
    def совершеннолетний(self):
        """Свойство только для чтения."""
        return self.__возраст >= 18

# Создаем объект
человек = Человек("Иван", 30)

# Используем свойства как обычные атрибуты
print(человек.имя)  # Иван
print(человек.возраст)  # 30
print(человек.совершеннолетний)  # True

# Изменяем значения через свойства
человек.имя = "Петр"
человек.возраст = 16

print(человек.имя)  # Петр
print(человек.возраст)  # 16
print(человек.совершеннолетний)  # False

# Проверка валидации
try:
    человек.возраст = 150  # Вызовет ошибку
except ValueError as e:
    print(f"Ошибка: {e}")  # Ошибка: Возраст должен быть от 0 до 120

# Попытка изменить свойство только для чтения
try:
    человек.совершеннолетний = True  # Вызовет AttributeError
except AttributeError as e:
    print(f"Ошибка: {e}")  # Ошибка: can't set attribute

Создание свойств с помощью функции property

Помимо декораторов, свойства можно создавать с помощью встроенной функции property. Это альтернативный способ, который может быть полезен в некоторых случаях.

class Прямоугольник:
    def __init__(self, ширина, высота):
        self.__ширина = ширина
        self.__высота = высота
    
    def get_ширина(self):
        return self.__ширина
    
    def set_ширина(self, значение):
        if значение <= 0:
            raise ValueError("Ширина должна быть положительной")
        self.__ширина = значение
    
    def get_высота(self):
        return self.__высота
    
    def set_высота(self, значение):
        if значение <= 0:
            raise ValueError("Высота должна быть положительной")
        self.__высота = значение
    
    def get_площадь(self):
        return self.__ширина * self.__высота
    
    # Создание свойств с помощью функции property
    ширина = property(get_ширина, set_ширина)
    высота = property(get_высота, set_высота)
    площадь = property(get_площадь)  # Свойство только для чтения

# Создаем объект
прямоугольник = Прямоугольник(5, 3)

# Используем свойства
print(прямоугольник.ширина)  # 5
print(прямоугольник.высота)  # 3
print(прямоугольник.площадь)  # 15

# Изменяем значения
прямоугольник.ширина = 10
прямоугольник.высота = 6

print(прямоугольник.ширина)  # 10
print(прямоугольник.высота)  # 6
print(прямоугольник.площадь)  # 60

Дескрипторы

Дескрипторы — это более продвинутый механизм для контроля доступа к атрибутам. Они позволяют определить, как атрибуты будут вести себя при доступе, изменении и удалении. Дескрипторы реализуются с помощью специальных методов __get__, __set__ и __delete__.

class ПоложительноеЧисло:
    """Дескриптор для положительных чисел."""
    
    def __init__(self, имя):
        self.имя = имя
        self.приватное_имя = f"__{имя}"
    
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return getattr(instance, self.приватное_имя, 0)
    
    def __set__(self, instance, value):
        if value <= 0:
            raise ValueError(f"{self.имя} должно быть положительным")
        setattr(instance, self.приватное_имя, value)

class Товар:
    """Класс, использующий дескрипторы."""
    
    цена = ПоложительноеЧисло("цена")
    количество = ПоложительноеЧисло("количество")
    
    def __init__(self, название, цена, количество):
        self.название = название
        self.цена = цена
        self.количество = количество
    
    @property
    def стоимость(self):
        """Вычисляет общую стоимость товара."""
        return self.цена * self.количество

# Создаем объект
товар = Товар("Ноутбук", 50000, 3)

# Используем атрибуты с дескрипторами
print(товар.название)  # Ноутбук
print(товар.цена)  # 50000
print(товар.количество)  # 3
print(товар.стоимость)  # 150000

# Изменяем значения
товар.цена = 45000
товар.количество = 5

print(товар.цена)  # 45000
print(товар.количество)  # 5
print(товар.стоимость)  # 225000

# Проверка валидации
try:
    товар.цена = -1000  # Вызовет ошибку
except ValueError as e:
    print(f"Ошибка: {e}")  # Ошибка: цена должно быть положительным

Практический пример: Банковский счет с инкапсуляцией

class БанковскийСчет:
    """Класс для представления банковского счета с инкапсуляцией."""
    
    def __init__(self, владелец, начальный_баланс=0):
        self.__владелец = владелец
        self.__баланс = начальный_баланс
        self.__активен = True
        self.__история_транзакций = []
    
    @property
    def владелец(self):
        """Свойство только для чтения."""
        return self.__владелец
    
    @property
    def баланс(self):
        """Свойство только для чтения."""
        return self.__баланс
    
    @property
    def активен(self):
        """Свойство только для чтения."""
        return self.__активен
    
    def внести(self, сумма):
        """Вносит деньги на счет."""
        if not self.__активен:
            raise ValueError("Счет закрыт")
        
        if сумма <= 0:
            raise ValueError("Сумма должна быть положительной")
        
        self.__баланс += сумма
        self.__записать_транзакцию("внесение", сумма)
        return f"Внесено {сумма} руб. Новый баланс: {self.__баланс} руб."
    
    def снять(self, сумма):
        """Снимает деньги со счета."""
        if not self.__активен:
            raise ValueError("Счет закрыт")
        
        if сумма <= 0:
            raise ValueError("Сумма должна быть положительной")
        
        if сумма > self.__баланс:
            raise ValueError("Недостаточно средств на счете")
        
        self.__баланс -= сумма
        self.__записать_транзакцию("снятие", сумма)
        return f"Снято {сумма} руб. Новый баланс: {self.__баланс} руб."
    
    def закрыть(self):
        """Закрывает счет."""
        if not self.__активен:
            raise ValueError("Счет уже закрыт")
        
        self.__активен = False
        self.__записать_транзакцию("закрытие", 0)
        return f"Счет закрыт. Остаток: {self.__баланс} руб."
    
    def получить_историю(self):
        """Возвращает историю транзакций."""
        return self.__история_транзакций.copy()
    
    def __записать_транзакцию(self, тип, сумма):
        """Приватный метод для записи транзакции."""
        import datetime
        дата = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        транзакция = {
            "дата": дата,
            "тип": тип,
            "сумма": сумма,
            "баланс": self.__баланс
        }
        self.__история_транзакций.append(транзакция)
    
    def __str__(self):
        статус = "активен" if self.__активен else "закрыт"
        return f"Счет {self.__владелец}, баланс: {self.__баланс} руб., статус: {статус}"

# Создаем счет
счет = БанковскийСчет("Иван Иванов", 1000)

# Используем публичный интерфейс
print(счет)  # Счет Иван Иванов, баланс: 1000 руб., статус: активен
print(счет.владелец)  # Иван Иванов
print(счет.баланс)  # 1000
print(счет.активен)  # True

# Выполняем операции
print(счет.внести(500))  # Внесено 500 руб. Новый баланс: 1500 руб.
print(счет.снять(200))  # Снято 200 руб. Новый баланс: 1300 руб.

# Проверка валидации
try:
    счет.снять(2000)  # Вызовет ошибку
except ValueError as e:
    print(f"Ошибка: {e}")  # Ошибка: Недостаточно средств на счете

# Получаем историю транзакций
история = счет.получить_историю()
for транзакция in история:
    print(f"{транзакция['дата']} - {транзакция['тип']} - {транзакция['сумма']} руб. - Баланс: {транзакция['баланс']} руб.")

# Закрываем счет
print(счет.закрыть())  # Счет закрыт. Остаток: 1300 руб.
print(счет.активен)  # False

# Попытка выполнить операцию с закрытым счетом
try:
    счет.внести(100)  # Вызовет ошибку
except ValueError as e:
    print(f"Ошибка: {e}")  # Ошибка: Счет закрыт

Советы по использованию инкапсуляции в Python

  • Используйте соглашения об именовании для указания уровня доступа к атрибутам и методам
  • Предпочитайте свойства (properties) вместо явных геттеров и сеттеров
  • Скрывайте внутренние детали реализации, предоставляя четкий публичный интерфейс
  • Используйте приватные атрибуты для данных, которые не должны изменяться извне
  • Добавляйте валидацию данных в сеттеры свойств
  • Помните, что в Python инкапсуляция основана на соглашениях, а не на строгих ограничениях

Настройки

Цветовая схема

Тема