Part 10

Иерархии классов

Специальные классы для специальных целей

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

Рассмотрим два определения классов: Student и Teacher. Методы-геттеры и сеттеры пока опущены, чтобы пример был короче.


class Student:

    def __init__(self, name: str, id: str, email: str, credits: str):
        self.name = name
        self.id = id
        self.email = email
        self.credits = credits

class Teacher:

    def __init__(self, name: str, email: str, room: str, teaching_years: int):
        self.name = name
        self.email = email
        self.room = room
        self.teaching_years = teaching_years

Даже в таком сокращенном примере уже довольно много повторения: оба класса содержат атрибуты name и email. Было бы хорошо иметь единое определение этих атрибутов, чтобы одной функции хватило для изменения обоих атрибутов.

Например, представьте, что адрес электронной почты школы изменился. Все адреса нужно было бы обновить. Мы могли бы написать две отдельные версии по сути одной и той же функции:


def update_email(o: Student):
    o.email = o.email.replace(".com", ".edu")

def update_email2(o: Teacher):
    o.email = o.email.replace(".com", ".edu")

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

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

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

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

С учетом этого логично, чтобы у классов Teacher и Student был общий базовый или родительский класс Person:


class Person:

   def __init__(self, name: str, email: str):
       self.name = name
       self.email = email

Новый класс содержит те свойства, которые являются общими для двух других классов. Теперь Student и Teacher могут наследовать эти свойства и добавить к ним свои собственные.

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

class Person:

   def __init__(self, name: str, email: str):
       self.name = name
       self.email = email

   def update_email_domain(self, new_domain: str):
       old_domain = self.email.split("@")[1]
       self.email = self.email.replace(old_domain, new_domain)


class Student(Person):

   def __init__(self, name: str, id: str, email: str, credits: str):
       self.name = name
       self.id = id
       self.email = email
       self.credits = credits


class Teacher(Person):

   def __init__(self, name: str, email: str, room: str, teaching_years: int):
       self.name = name
       self.email = email
       self.room = room
       self.teaching_years = teaching_years

# Let's test our classes
if __name__ == "__main__":
   saul = Student("Saul Student", "1234", "saul@example.com", 0)
   saul.update_email_domain("example.edu")
   print(saul.email)

   tara = Teacher("Tara Teacher", "tara@example.fi", "A123", 2)
   tara.update_email_domain("example.ex")
   print(tara.email)

И Student, и Teacher наследуют класс Person, поэтому оба имеют свойства, определенные в классе Person, включая метод update_email_domain. Один и тот же метод работает с экземплярами обоих производных классов.

Рассмотрим еще один пример. У нас есть Bookshelf, который наследует класс BookContainer:

class Book:
   """ This class models a simple book """
   def __init__(self, name: str, author: str):
       self.name = name
       self.author = author


class BookContainer:
   """ This class models a container for books """

   def __init__(self):
       self.books = []

   def add_book(self, book: Book):
       self.books.append(book)

   def list_books(self):
       for book in self.books:
           print(f"{book.name} ({book.author})")


class Bookshelf(BookContainer):
   """ This class models a shelf for books """

   def __init__(self):
       super().__init__()

   def add_book(self, book: Book, location: int):
       self.books.insert(location, book)

Класс Bookshelf содержит метод add_book. Метод с таким же именем определен в базовом классе BookContainer. Это называется переопределением: если в производном классе есть метод с тем же именем, что и в базовом классе, версия из производного класса переопределяет исходную для экземпляров производного класса.

Идея в примере выше такая: новая книга, добавленная в BookContainer, всегда попадает наверх, но в Bookshelf можно самому указать место. Метод list_books работает одинаково для обоих классов, потому что в производном классе нет переопределяющего метода.

Попробуем эти классы:

if __name__ == "__main__":
   # Create some books for testing
   b1 = Book("Old Man and the Sea", "Ernest Hemingway")
   b2 = Book("Silent Spring", "Rachel Carson")
   b3 = Book("Pride and Prejudice", "Jane Austen")

   # Create a BookContainer and add the books
   container = BookContainer()
   container.add_book(b1)
   container.add_book(b2)
   container.add_book(b3)

   # Create a Bookshelf and add the books (always to the beginning)
   shelf = Bookshelf()
   shelf.add_book(b1, 0)
   shelf.add_book(b2, 0)
   shelf.add_book(b3, 0)


   # Tulostetaan
   print("Container:")
   container.list_books()

   print()

   print("Shelf:")
   shelf.list_books()
Пример вывода

Container: Old Man and the Sea (Ernest Hemingway) Silent Spring (Rachel Carson) Pride and Prejudice (Jane Austen)

Shelf: Pride and Prejudice (Jane Austen) Silent Spring (Rachel Carson) Old Man and the Sea (Ernest Hemingway)

Итак, класс Bookshelf также имеет доступ к методу list_books. Благодаря наследованию этот метод является членом всех классов, производных от класса BookContainer.

Наследование и область видимости свойств

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

Поскольку атрибуты Bookshelf идентичны атрибутам BookContainer, не было необходимости заново писать конструктор Bookshelf. Мы просто вызвали конструктор базового класса:

class Bookshelf(BookContainer):

   def __init__(self):
       super().__init__()

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

Но что, если атрибуты не идентичны; можем ли мы все равно как-то использовать конструктор базового класса? Рассмотрим класс с именем Thesis, который наследует класс Book. Производный класс все равно может вызвать конструктор базового класса:


class Book:
    """ This class models a simple book """

    def __init__(self, name: str, author: str):
        self.name = name
        self.author = author


class Thesis(Book):
    """ This class models a graduate thesis """

    def __init__(self, name: str, author: str, grade: int):
        super().__init__(name, author)
        self.grade = grade

Конструктор в классе Thesis вызывает конструктор базового класса Book с аргументами для name и author. Кроме того, конструктор в производном классе задает значение атрибута grade. Естественно, это не может быть частью конструктора базового класса, так как у базового класса нет такого атрибута.

Класс выше можно использовать так:

if __name__ == "__main__":
    thesis = Thesis("Python and the Universe", "Peter Pythons", 3)

    # Print out the values of the attributes
    print(thesis.name)
    print(thesis.author)
    print(thesis.grade)
Пример вывода

Python and the Universe Peter Pythons 3

Даже если производный класс переопределяет метод базового класса, он все равно может вызвать переопределенный метод базового класса. В следующем примере у нас есть обычная BonusCard и специальная PlatinumCard для особенно лояльных клиентов. Метод calculate_bonus переопределяется в производном классе, но переопределяющий метод вызывает базовый метод:


class Product:

    def __init__(self, name: str, price: float):
        self.name = name
        self.price = price

class BonusCard:

    def __init__(self):
        self.products_bought = []

    def add_product(self, product: Product):
        self.products_bought.append(product)

    def calculate_bonus(self):
        bonus = 0
        for product in self.products_bought:
            bonus += product.price * 0.05

        return bonus

class PlatinumCard(BonusCard):

    def __init__(self):
        super().__init__()

    def calculate_bonus(self):
        # Call the method in the base class
        bonus = super().calculate_bonus()

        # ...and add five percent to the total
        bonus = bonus * 1.05
        return bonus

Итак, бонус для PlatinumCard вычисляется вызовом переопределенного метода базового класса, а затем к базовому результату добавляется еще 5 процентов. Пример использования этих классов:

if __name__ == "__main__":
    card = BonusCard()
    card.add_product(Product("Bananas", 6.50))
    card.add_product(Product("Satsumas", 7.95))
    bonus = card.calculate_bonus()

    card2 = PlatinumCard()
    card2.add_product(Product("Bananas", 6.50))
    card2.add_product(Product("Satsumas", 7.95))
    bonus2 = card2.calculate_bonus()

    print(bonus)
    print(bonus2)
Пример вывода

0.7225 0.7586250000000001

Loading
Loading
Loading
Loading
Вы дошли до конца этого раздела! Перейти к следующему разделу:

Текущие баллы можно посмотреть в синем индикаторе в правом нижнем углу страницы.