Понимание особенностей классов в Python

PythonBeginner
Практиковаться сейчас

Введение

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

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

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

Изучение инкапсуляции с помощью базовых классов

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

В Python мы используем соглашение об именовании, чтобы указать, что атрибут является «приватным». Добавление одного подчеркивания в начале имени атрибута (например, _name) сигнализирует о том, что он предназначен для внутреннего использования. Хотя это не является строгим требованием, это сильное соглашение, которого придерживаются разработчики.

Мы начнем с создания двух отдельных классов, Dog и Cat, чтобы увидеть, как они могут быть структурированы.

Сначала найдите файл animal_classes.py в проводнике файлов (file explorer) слева в WebIDE. Откройте его и добавьте следующий код на Python. Этот код определяет класс Dog и класс Cat, каждый из которых имеет приватный атрибут _name и методы для взаимодействия с ним.

## File: animal_classes.py

class Dog:
    def __init__(self, name):
        ## Префикс из одного подчеркивания указывает на "приватный" атрибут.
        self._name = name

    ## Публичный метод для получения значения приватного атрибута.
    def get_name(self):
        return self._name

    ## Публичный метод для установки значения приватного атрибута.
    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} says: Woof!")

class Cat:
    def __init__(self, name):
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} says: Meow!")

## Этот блок будет выполняться только при прямом запуске скрипта.
if __name__ == "__main__":
    ## Создание экземпляра класса Dog
    my_dog = Dog("Buddy")
    print(f"Initial dog name: {my_dog.get_name()}")

    ## Изменение имени собаки с помощью метода-сеттера
    my_dog.set_name("Rocky")
    print(f"New dog name: {my_dog.get_name()}")
    my_dog.say()

    print("-" * 20)

    ## Создание экземпляра класса Cat
    my_cat = Cat("Whiskers")
    print(f"Cat name: {my_cat.get_name()}")
    my_cat.say()

После добавления кода сохраните файл.

Теперь давайте запустим скрипт, чтобы увидеть инкапсуляцию в действии. Откройте терминал в WebIDE и выполните следующую команду:

python animal_classes.py

Вы увидите следующий вывод, который демонстрирует, что мы взаимодействуем с приватным атрибутом _name через наши публичные методы get_name и set_name.

Initial dog name: Buddy
New dog name: Rocky
Rocky says: Woof!
--------------------
Cat name: Whiskers
Whiskers says: Meow!

Реализация наследования и полиморфизма

В предыдущем шаге вы могли заметить, что классы Dog и Cat содержат много идентичного кода (__init__, get_name, set_name). Это идеальная возможность использовать наследование (inheritance). Наследование позволяет новому классу (дочернему или подклассу) наследовать атрибуты и методы от существующего класса (родительского или суперкласса), что способствует повторному использованию кода.

Мы также введем полиморфизм (polymorphism), что означает «много форм». В ООП это относится к способности разных классов реагировать на один и тот же вызов метода по-своему.

Давайте реорганизуем наш код. Мы создадим родительский класс Animal, чтобы разместить в нем общий код, а классы Dog и Cat будут от него наследоваться. Метод say, который различается для каждого класса, продемонстрирует полиморфизм.

Откройте файл animal_classes.py и замените его содержимое следующим кодом:

## File: animal_classes.py

class Animal:
    def __init__(self, name):
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} makes a generic animal sound.")

## Dog наследуется от Animal
class Dog(Animal):
    ## Это переопределяет метод say() из класса Animal
    def say(self):
        print(f"{self._name} says: Woof!")

## Cat наследуется от Animal
class Cat(Animal):
    ## Это также переопределяет метод say
    def say(self):
        print(f"{self._name} says: Meow!")

def make_animal_speak(animal_instance):
    animal_instance.say()

if __name__ == "__main__":
    generic_animal = Animal("Creature")
    my_dog = Dog("Buddy")
    my_cat = Cat("Whiskers")

    print("--- Calling say() on each object ---")
    generic_animal.say()
    my_dog.say()
    my_cat.say()

    print("\n--- Demonstrating Polymorphism ---")
    make_animal_speak(generic_animal)
    make_animal_speak(my_dog)
    make_animal_speak(my_cat)

Сохраните файл. Обратите внимание, что классы Dog и Cat стали намного проще. Они наследуют методы __init__, get_name и set_name от Animal. Каждый из них предоставляет свою версию метода say, что является примером переопределения методов (method overriding).

Теперь запустите обновленный скрипт в терминале:

python animal_classes.py

Вывод будет следующим:

--- Calling say() on each object ---
Creature makes a generic animal sound.
Buddy says: Woof!
Whiskers says: Meow!

--- Demonstrating Polymorphism ---
Creature makes a generic animal sound.
Buddy says: Woof!
Whiskers says: Meow!

Функция make_animal_speak принимает любой объект, у которого есть метод say. Несмотря на то, что мы передаем ей объекты разных типов (Animal, Dog, Cat), она работает корректно, потому что каждый объект знает, как выполнить действие say по-своему. В этом и заключается сила полиморфизма.

Использование метода super() для расширения функциональности

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

Это очень распространено в методе __init__. Дочернему классу часто необходимо выполнить собственные шаги инициализации в дополнение к инициализации, выполняемой его родителем.

Давайте добавим уникальные атрибуты в наши классы Dog и Cat. У Dog будет age (возраст), а у Cat будет color (цвет). Мы будем использовать super(), чтобы гарантировать, что метод __init__ родительского класса Animal все равно будет вызван для установки атрибута _name.

Измените файл animal_classes.py, заменив его содержимое следующим кодом:

## File: animal_classes.py

class Animal:
    def __init__(self, name):
        print(f"Animal __init__ called for {name}")
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} makes a generic animal sound.")

class Dog(Animal):
    def __init__(self, name, age):
        ## Вызов метода __init__ родителя для обработки атрибута 'name'
        super().__init__(name)
        print("Dog __init__ called")
        self.age = age

    def say(self):
        ## Мы также можем использовать super() для вызова метода say() родителя
        ## super().say()
        print(f"{self._name} says: Woof! I am {self.age} years old.")

class Cat(Animal):
    def __init__(self, name, color):
        ## Вызов метода __init__ родителя
        super().__init__(name)
        print("Cat __init__ called")
        self.color = color

    def say(self):
        print(f"{self._name} says: Meow! I have {self.color} fur.")

if __name__ == "__main__":
    my_dog = Dog("Buddy", 5)
    my_dog.say()

    print("-" * 20)

    my_cat = Cat("Whiskers", "black")
    my_cat.say()

Сохраните файл. В этой версии Dog.__init__ и Cat.__init__ сначала вызывают super().__init__(name). Это выполняет код в Animal.__init__, который устанавливает атрибут _name. После этого они переходят к своей собственной специфической инициализации (self.age = age и self.color = color).

Запустите скрипт из терминала:

python animal_classes.py

Вывод демонстрирует цепочку вызовов __init__ и расширенные методы say:

Animal __init__ called for Buddy
Dog __init__ called
Buddy says: Woof! I am 5 years old.
--------------------
Animal __init__ called for Whiskers
Cat __init__ called
Whiskers says: Meow! I have black fur.

Практика множественного наследования

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

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

Давайте рассмотрим это на новом примере. Откройте файл multiple_inheritance.py в проводнике файлов и добавьте следующий код:

## File: multiple_inheritance.py

class ParentA:
    def speak(self):
        print("Speaking from ParentA")

    def common_method(self):
        print("ParentA's common method")

class ParentB:
    def speak(self):
        print("Speaking from ParentB")

    def common_method(self):
        print("ParentB's common method")

## Child наследуется от A, затем от B
class Child_AB(ParentA, ParentB):
    pass

## Child наследуется от B, затем от A
class Child_BA(ParentB, ParentA):
    def common_method(self):
        print("Child_BA's own common method")

if __name__ == "__main__":
    child1 = Child_AB()
    child2 = Child_BA()

    print("--- Investigating Child_AB (ParentA, ParentB) ---")
    child1.speak()
    child1.common_method()
    ## Метод .mro() показывает Порядок разрешения методов
    print("MRO for Child_AB:", [c.__name__ for c in Child_AB.mro()])

    print("\n--- Investigating Child_BA (ParentB, ParentA) ---")
    child2.speak()
    child2.common_method()
    print("MRO for Child_BA:", [c.__name__ for c in Child_BA.mro()])

Сохраните файл. Здесь Child_AB наследуется от ParentA, а затем от ParentB. Child_BA наследуется в обратном порядке. Когда вызывается метод, Python ищет его в порядке, указанном MRO.

Запустите скрипт из терминала:

python multiple_inheritance.py

Вы увидите следующий вывод:

--- Investigating Child_AB (ParentA, ParentB) ---
Speaking from ParentA
ParentA's common method
MRO for Child_AB: ['Child_AB', 'ParentA', 'ParentB', 'object']

--- Investigating Child_BA (ParentB, ParentA) ---
Speaking from ParentB
Child_BA's own common method
MRO for Child_BA: ['Child_BA', 'ParentB', 'ParentA', 'object']

Из вывода вы можете заметить:

  • child1.speak() вызывает метод из ParentA, потому что ParentA идет первым в MRO Child_AB.
  • child2.speak() вызывает метод из ParentB, потому что ParentB идет первым в MRO Child_BA.
  • child2.common_method() вызывает версию, определенную непосредственно в Child_BA, так как Python находит ее там в первую очередь, прежде чем проверять родителей.

Понимание MRO имеет решающее значение для прогнозирования поведения в сценариях множественного наследования.

Резюме

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

Вы начали с инкапсуляции (encapsulation), научившись защищать данные класса по соглашению, используя приватные атрибуты и предоставляя публичные методы для доступа к ним. Затем вы провели рефакторинг кода, чтобы использовать наследование (inheritance), создав родительский класс Animal для уменьшения дублирования кода в подклассах Dog и Cat.

При реализации наследования вы увидели полиморфизм (polymorphism) в действии, поскольку объекты Dog и Cat по-разному реагировали на один и тот же вызов метода say(). Вы научились использовать метод super() для вызова и расширения функциональности родительского класса, особенно внутри метода __init__. Наконец, вы изучили множественное наследование (multiple inheritance) и важность Порядка разрешения методов (MRO) для определения того, какой метод родителя будет вызван.