Атрибуты, методы и принципы ООП (наследование, инкапсуляция, полиморфизм) - Python с нуля до Junior: Путь к первой работе в IT - Qpel.AI

Атрибуты, методы и принципы ООП (наследование, инкапсуляция, полиморфизм)

В прошлом уроке вы научились создавать классы и объекты, а также инициализировать их атрибуты. Теперь давайте копнём глубже: разберём, как объекты взаимодействуют и как принципы объектно-ориентированного программирования (ООП) делают код понятным, гибким и лёгким в поддержке.

Атрибуты и методы: подробности

Вы уже знаете: атрибуты — это данные объекта (его характеристики), а методы — это функции, которые определяют его поведение.

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

Важно различать два типа атрибутов:

  • Атрибуты экземпляра (Instance Attributes): Принадлежат конкретному объекту. Вы определяете их внутри метода __init__, и они уникальны для каждого созданного объекта.

    class Car:
        def __init__(self, brand, model):
            self.brand = brand  # Атрибут экземпляра
            self.model = model  # Атрибут экземпляра
    
    car1 = Car("Toyota", "Camry")
    car2 = Car("Lada", "Granta")
    
    print(car1.brand) # Toyota
    print(car2.brand) # Lada
    
  • Атрибуты класса (Class Attributes): Принадлежат самому классу, а не его экземплярам. Они общие для всех объектов этого класса и определяются внутри тела класса, но вне методов.

    class Car:
        # Атрибут класса
        # Общий для всех машин, произведенных этим "заводом"
        wheels = 4
    
        def __init__(self, brand, model):
            self.brand = brand
            self.model = model
    
    car1 = Car("Toyota", "Camry")
    car2 = Car("Lada", "Granta")
    
    print(car1.wheels) # 4
    print(car2.wheels) # 4
    print(Car.wheels)  # 4 (доступ к атрибуту класса через сам класс)
    
    # Можно изменить атрибут класса, и это повлияет на все экземпляры
    Car.wheels = 6
    print(car1.wheels) # 6
    

    Совет: Атрибуты класса часто используют для констант или значений по умолчанию, которые одинаковы для всех объектов.

Методы экземпляра и специальные методы

  • Методы экземпляра (Instance Methods): Это обычные методы, которые вы уже видели. Они принимают self как первый аргумент и работают с атрибутами конкретного экземпляра.

    class Dog:
        def __init__(self, name, breed):
            self.name = name
            self.breed = breed
    
        def bark(self): # Метод экземпляра
            return f"{self.name} says Woof!"
    
    my_dog = Dog("Buddy", "Golden Retriever")
    print(my_dog.bark()) # Buddy says Woof!
    
  • Специальные методы (Special Methods): Их ещё называют "магическими" методами или "dunder-методами" (от double underscore – двойное подчёркивание). Их имена начинаются и заканчиваются двойным подчёркиванием (например, __init__, __str__). Эти методы позволяют классам взаимодействовать со встроенными функциями Python и операторами. Подробнее о них поговорим на следующей странице.

Принципы ООП: фундамент чистого кода

ООП базируется на четырёх основных принципах. Они помогают создавать масштабируемые и поддерживаемые системы:

  1. Наследование (Inheritance)
  2. Инкапсуляция (Encapsulation)
  3. Полиморфизм (Polymorphism)
  4. Абстракция (Abstraction) (часто рассматривается как концепция, тесно связанная с другими принципами, а не отдельный принцип реализации)

Давайте рассмотрим первые три — их чаще всего используют на практике.

1. Наследование: "Это как..."

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

Это принцип "это как..." или "является разновидностью". Например, "Собака является разновидностью Животного".

class Animal: # Родительский класс
    def __init__(self, name):
        self.name = name

    def speak(self):
        raise NotImplementedError("Этот метод должен быть переопределен в дочернем классе")

class Dog(Animal): # Дочерний класс, наследует от Animal
    def speak(self): # Переопределение метода speak
        return f"{self.name} лает: Гав!"

class Cat(Animal): # Дочерний класс, наследует от Animal
    def speak(self): # Переопределение метода speak
        return f"{self.name} мяукает: Мяу!"

my_dog = Dog("Шарик")
my_cat = Cat("Мурка")

print(my_dog.speak()) # Шарик лает: Гав!
print(my_cat.speak()) # Мурка мяукает: Мяу!

Зачем это нужно? Наследование помогает повторно использовать код (DRY - Don't Repeat Yourself). Если у нескольких классов есть общая функциональность, вынесите её в родительский класс.

2. Инкапсуляция: "Скрытие деталей"

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

В Python нет строгих механизмов приватности, как в некоторых других языках (например, private). Вместо этого используют соглашения:

  • Один нижний пробел _attribute: Указывает, что атрибут или метод считается "защищённым" (protected) и предназначен для использования внутри класса или его подклассов. Это соглашение, а не строгий запрет.
  • Два нижних пробела __attribute: Вызывает "искажение имени" (name mangling), делая доступ к атрибуту извне класса сложнее (но не невозможным). Это используют для предотвращения конфликтов имён при наследовании.
class BankAccount:
    def __init__(self, balance):
        self.__balance = balance # "Приватный" атрибут с искажением имени

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            print(f"Внесено {amount}. Новый баланс: {self.__balance}")
        else:
            print("Сумма должна быть положительной.")

    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            print(f"Снято {amount}. Новый баланс: {self.__balance}")
        else:
            print("Недостаточно средств или неверная сумма.")

    def get_balance(self): # Метод для получения баланса
        return self.__balance

account = BankAccount(1000)
account.deposit(500)
account.withdraw(200)
print(f"Текущий баланс: {account.get_balance()}")

# Попытка прямого доступа (не рекомендуется, но возможна через искаженное имя)
# print(account.__balance) # Вызовет AttributeError
# print(account._BankAccount__balance) # Работает, но так делать не стоит!

Зачем это нужно? Инкапсуляция защищает данные от некорректного изменения извне и упрощает поддержку кода. Изменения во внутренней реализации класса не влияют на внешний код, использующий этот класс.

3. Полиморфизм: "Множество форм"

Полиморфизм (от греч. poly — много, morph — форма) означает, что объекты разных классов могут по-разному отвечать на один и тот же вызов метода, в зависимости от их типа.

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

class Dog:
    def speak(self):
        return "Гав!"

class Cat:
    def speak(self):
        return "Мяу!"

class Duck:
    def speak(self):
        return "Кря!"

def make_sound(animal):
    # Здесь make_sound не знает, какой именно объект ему передали,
    # но он знает, что у него есть метод speak().
    # И каждый объект реализует speak() по-своему.
    return animal.speak()

dog = Dog()
cat = Cat()
duck = Duck()

animals = [dog, cat, duck]

for animal in animals:
    print(make_sound(animal))
# Вывод:
# Гав!
# Мяу!
# Кря!

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

Практическое задание

Давайте закрепим знания.

Задание: Создайте систему для управления сотрудниками компании, используя принципы ООП.

  1. Базовый класс Employee (Сотрудник):

    • Атрибуты экземпляра: name (имя), employee_id (ID сотрудника).
    • Метод display_info(), который выводит имя и ID сотрудника.
    • Атрибут класса company_name (название компании), например, "ООО 'Рога и Копыта'".
  2. Дочерний класс Developer (Разработчик):

    • Наследуется от Employee.
    • Дополнительный атрибут экземпляра: programming_language (язык программирования).
    • Переопределите метод display_info(), чтобы он также выводил язык программирования.
    • Добавьте новый метод write_code(), который выводит сообщение вроде: "Имя пишет код на язык программирования."
  3. Дочерний класс Manager (Менеджер):

    • Наследуется от Employee.
    • Дополнительный атрибут экземпляра: department (отдел).
    • Переопределите метод display_info(), чтобы он также выводил отдел.
    • Добавьте новый метод manage_team(), который выводит сообщение вроде: "Имя управляет командой в отделе отдел."
  4. Инкапсуляция (опционально, для продвинутых):

    • Попробуйте сделать employee_id "защищённым" атрибутом (_employee_id) и создайте метод get_employee_id() для его получения.
  5. Полиморфизм:

    • Создайте список из нескольких объектов Developer и Manager.
    • Пройдитесь по этому списку в цикле и вызовите для каждого объекта метод display_info(). Убедитесь, что каждый объект выводит информацию в соответствии со своей ролью.

Пример структуры кода для начала:

class Employee:
    company_name = "ООО 'Рога и Копыта'" # Атрибут класса

    def __init__(self, name, employee_id):
        self.name = name
        self._employee_id = employee_id # Защищенный атрибут

    def display_info(self):
        print(f"Имя: {self.name}, ID: {self._employee_id}, Компания: {Employee.company_name}")

    def get_employee_id(self):
        return self._employee_id

class Developer(Employee):
    def __init__(self, name, employee_id, programming_language):
        super().__init__(name, employee_id) # Вызов конструктора родителя
        self.programming_language = programming_language

    def display_info(self):
        # Дополняем информацию родителя
        super().display_info()
        print(f"Язык программирования: {self.programming_language}")

    def write_code(self):
        print(f"{self.name} пишет код на {self.programming_language}.")

# Добавьте класс Manager по аналогии

# Создание объектов и демонстрация полиморфизма
employees = []
employees.append(Developer("Иван Петров", "DEV001", "Python"))
employees.append(Manager("Мария Сидорова", "MGR002", "Разработки"))

for emp in employees:
    emp.display_info()
    # Здесь можно добавить проверку типа, если нужно вызвать специфичные методы
    # if isinstance(emp, Developer):
    #     emp.write_code()

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

На следующей странице мы углубимся в "магические" методы Python, которые позволяют нам делать наши объекты ещё мощнее и интуитивно понятнее, взаимодействуя со стандартными операциями языка.