Модуль 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 инкапсуляция основана на соглашениях, а не на строгих ограничениях
Поздравляем!
Вы прошли модуль "ООП в Python". Теперь вы знаете, как создавать классы, использовать наследование, полиморфизм и инкапсуляцию для создания объектно-ориентированных программ!