Четыре принципа ООП
Объектно-ориентированное программирование строится на четырёх принципах. Они помогают проектировать код так, чтобы его было легче расширять и поддерживать.
| Принцип | Краткое описание |
|---|---|
| Наследование | Один класс может унаследовать свойства и поведение другого. Позволяет избегать дублирования кода и переиспользовать логику. |
| Инкапсуляция | Объект скрывает внутреннее устройство и предоставляет только нужный интерфейс. Помогает защищать данные и контролировать доступ к ним. |
| Полиморфизм | Одинаковый интерфейс может вести себя по-разному в зависимости от типа объекта. Позволяет использовать одинаковый код для разных классов. |
| Абстракция | Выделение общих характеристик и поведения, чтобы задать интерфейс для будущих реализаций в наследниках. |
Знание принципов упрощает понимание механизмов наследования, переопределения и взаимодействия объектов, а также помогает писать гибкий и расширяемый код.
Наследование
Наследование — это механизм, который позволяет одному классу перенять свойства и поведение другого. Класс, от которого наследуются, называется родительским, базовым или суперклассом. Класс, который наследует, называется дочерним, производным или подклассом.
class Parent:
# родительский класс
...
class Child(Parent):
# дочерний класс, наследует всё от Parent
...
Зачем нужно наследование:
- Избежать дублирования кода, выделив общее поведение в один базовый класс.
- Удобно расширять существующую логику без переписывания старых классов.
- Работать с разными объектами одинаково, если они наследуются от общего предка.
class Employee:
def __init__(self, name: str) -> None:
self.name = name
def work(self) -> None:
print(f"{self.name} is working...")
class Programmer(Employee):
pass
class Manager(Employee):
pass
programmer = Programmer("Alice")
manager = Manager("Bob")
programmer.work()
manager.work()
Что происходит: Employee хранит имя сотрудника и умеет работать. Programmer и Manager наследуют поле name и метод work(), поэтому могут использовать их без повторного определения.
Полиморфизм
Полиморфизм (от греч. «много форм») — это возможность использовать один и тот же интерфейс для разных типов объектов. Разные классы могут реализовывать один и тот же метод по-своему, а вызывающий код работает с ними одинаково.
Зачем нужен полиморфизм:
- Позволяет писать универсальный код, не заботясь о типе объекта.
- Упрощает расширение программы — можно добавлять новые типы, не меняя старую логику.
- Делает код гибким и читаемым.
Переопределение методов
Когда дочерний класс заново определяет метод, унаследованный от родителя, это называется overriding. При вызове такого метода Python использует реализацию из дочернего класса.
class Employee:
def __init__(self, name: str) -> None:
self.name = name
def work(self) -> None:
print(f"{self.name} is working...")
class Programmer(Employee):
def work(self) -> None:
print(f"{self.name} writing code...")
class Manager(Employee):
def work(self) -> None:
print(f"{self.name} managing team...")
staff = [Programmer("Alice"), Manager("Bob"), Programmer("Bill")]
for person in staff:
person.work()
Что происходит: у всех сотрудников есть метод work(), но он реализован по-разному. В списке staff хранятся объекты разных классов, и Python автоматически вызывает нужную версию метода в зависимости от типа объекта. Это и есть полиморфизм.
Перегрузка методов в Python
Перегрузка (overloading) — это когда один и тот же метод ведёт себя по-разному в зависимости от переданных аргументов. В Python нет классической перегрузки по количеству аргументов, как в Java или C++.
class Math:
def add(self, a: int, b: int) -> int:
return a + b
def add(self, a: int, b: int, c: int) -> int:
return a + b + c
m = Math()
print(m.add(1, 2, 3)) # Работает
print(m.add(1, 2)) # TypeError!
Вторая версия add() просто заменяет первую — Python запоминает только последнее определение метода.
Функция super()
При наследовании часто возникает ситуация, когда родитель уже задаёт общие поля в __init__(), а в дочернем классе нужно добавить новые. Чтобы не дублировать код, используется функция super() — она вызывает ближайший метод родителя.
Неправильный способ: дублирование кода
class Employee:
def __init__(self, name: str) -> None:
self.name = name
class Programmer(Employee):
def __init__(self, name: str, language: str) -> None:
self.name = name # повторяем то, что делает родитель
self.language = language
Такой код нарушает принцип DRY (Do not Repeat Yourself): если поведение Employee.__init__ изменится, Programmer об этом не узнает.
Альтернативный неправильный способ: явный вызов родителя
class Programmer(Employee):
def __init__(self, name: str, language: str) -> None:
Employee.__init__(self, name) # привязано к имени класса
self.language = language
Этот способ работает, но считается плохой практикой: он жёстко привязан к имени родительского класса, может некорректно работать при изменении иерархии и не учитывает порядок разрешения методов.
Правильный способ: super()
class Programmer(Employee):
def __init__(self, name: str, language: str) -> None:
super().__init__(name)
self.language = language
class Manager(Employee):
def __init__(self, name: str, department: str) -> None:
super().__init__(name)
self.department = department
p = Programmer("Alice", "Python")
print(p.name)
print(p.language)
super() в цепочке наследования
Когда уровней наследования несколько, super() позволяет вызывать инициализацию каждого уровня без жёсткой привязки к имени родительского класса.
class Person:
def __init__(self, name: str) -> None:
self.name = name
print(f"Init Person: {self.name}")
class Employee(Person):
def work(self) -> None:
print(f"{self.name} is working...")
class Manager(Employee):
def __init__(self, name: str, department: str) -> None:
super().__init__(name)
self.department = department
print(f"Init Manager: {self.name} manages {self.department}")
m = Manager("Alice", "Development")
super() в обычных методах
super() полезна не только в __init__(), но и в любом методе, где нужно расширить поведение родителя, а не полностью его заменить.
class Employee:
def work(self) -> None:
print("Employee is doing general tasks.")
class Programmer(Employee):
def work(self) -> None:
super().work()
print("Programmer is writing code.")
class Manager(Employee):
def work(self) -> None:
super().work()
print("Manager is holding a meeting.")
staff = [Programmer(), Manager()]
for person in staff:
person.work()
print()
super() автоматически находит метод в ближайшем родителе, избегает дублирования и корректно работает при изменении иерархии классов.
Наследование от object
В Python все классы неявно наследуются от встроенного класса object, даже если родитель не указан. object — это корневой класс всей иерархии классов в Python. Он предоставляет базовые методы, такие как __str__(), __init__() и другие.
class Book:
pass
# Python воспринимает это как:
# class Book(object):
# pass
b = Book()
print(b.__str__()) # Вызов метода __str__ напрямую
print(b) # print() тоже вызывает __str__
Метод __str__() присутствует в классе без собственной реализации, потому что он унаследован от object. Все классы получают единое поведение по умолчанию и становятся совместимыми со встроенными механизмами Python.
Функции isinstance и issubclass
Python предоставляет удобные встроенные функции для проверки типов и иерархии классов, что особенно полезно при работе с наследованием и полиморфизмом.
isinstance
isinstance(obj, some_class) проверяет, является ли объект экземпляром указанного класса или его наследника. Если да — возвращает True, иначе — False. Вторым аргументом можно передать кортеж классов.
class Employee:
pass
class Programmer(Employee):
pass
class Manager(Employee):
pass
e = Employee()
p = Programmer()
m = Manager()
print(isinstance(p, Programmer)) # True — экземпляр класса
print(isinstance(p, Employee)) # True — экземпляр наследника
print(isinstance(p, Manager)) # False — другая ветка иерархии
print(isinstance(p, object)) # True — все классы наследуют от object
print(isinstance("hello", (str, int))) # True — строка принадлежит к одному из классов
issubclass
issubclass(class_a, class_b) проверяет, является ли class_a подклассом class_b напрямую или через цепочку наследования. Вторым аргументом тоже можно передать кортеж.
class BackendDeveloper(Programmer):
pass
print(issubclass(Programmer, Employee)) # True — прямой потомок
print(issubclass(BackendDeveloper, Programmer)) # True — прямой потомок
print(issubclass(BackendDeveloper, Employee)) # True — потомок через цепочку
print(issubclass(Manager, Programmer)) # False — разные ветки
print(issubclass(Employee, object)) # True — все классы наследуют от object
isinstance принимает объект, а issubclass — класс. Передача объекта в issubclass вызовет TypeError.
super(), isinstance() и issubclass() см. Built-in Functions. Описание объектной модели — в разделе Data model.