Иерархии классов
Специальные классы для специальных целей
Иногда встречается ситуация, когда вы уже определили класс, но затем понимаете, что некоторым, но не всем, экземплярам этого класса нужны особые свойства. Бывает и так, что вы определили два очень похожих класса, отличающихся лишь небольшими деталями. Как программисты мы стремимся повторяться как можно меньше, сохраняя при этом ясность и читаемость. Как же поддержать разные реализации объектов, которые по сути очень похожи?
Рассмотрим два определения классов: 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", "[email protected]", 0)
saul.update_email_domain("example.edu")
print(saul.email)
tara = Teacher("Tara Teacher", "[email protected]", "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
Текущие баллы можно посмотреть в синем индикаторе в правом нижнем углу страницы.