Part 10

Приемы объектно-ориентированного программирования

Класс может содержать метод, который возвращает объект того же самого класса. Например, ниже у нас есть класс Product, метод product_on_sale которого возвращает новый объект Product с тем же именем, что и у исходного, но с ценой на 25 % ниже:

class Product:
    def __init__(self, name: str, price: float):
        self.__name = name
        self.__price = price

    def __str__(self):
        return f"{self.__name} (price {self.__price})"

    def product_on_sale(self):
        on_sale = Product(self.__name, self.__price * 0.75)
        return on_sale
apple1 = Product("Apple", 2.99)
apple2 = apple1.product_on_sale()
print(apple1)
print(apple2)
Пример вывода

Apple (price 2.99) Apple (price 2.2425)

Повторим назначение переменной self: внутри определения класса она ссылается на сам объект. Обычно она используется для обращения к собственным свойствам объекта, его атрибутам и методам. Переменную можно использовать и для обращения ко всему объекту целиком, например если сам объект нужно вернуть клиентскому коду. В примере ниже мы добавили в определение класса метод cheaper. Он принимает другой Product в качестве аргумента и возвращает более дешевый из двух:

class Product:
    def __init__(self, name: str, price: float):
        self.__name = name
        self.__price = price

    def __str__(self):
        return f"{self.__name} (price {self.__price})"

    @property
    def price(self):
        return self.__price

    def cheaper(self, Product):
        if self.__price < Product.price:
            return self
        else:
            return Product
apple = Product("Apple", 2.99)
orange = Product("Orange", 3.95)
banana = Product("Banana", 5.25)

print(orange.cheaper(apple))
print(orange.cheaper(banana))
Пример вывода

Apple (2.99) Orange (3.95)

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

Перегрузка операторов

В Python есть некоторые встроенные методы со специальными именами для работы со стандартными арифметическими операторами и операторами сравнения. Эта техника называется перегрузкой операторов. Если вы хотите иметь возможность использовать определенный оператор с экземплярами самостоятельно определенных классов, можно написать специальный метод, который возвращает правильный результат оператора. Мы уже использовали эту технику с методом __str__: Python знает, что нужно искать метод с таким именем, когда требуется строковое представление объекта.

Начнем с оператора >, который сообщает, больше ли первый операнд второго. Определение класса Product ниже содержит метод __gt__, сокращение от greater than. Этот метод со специальным именем должен возвращать правильный результат сравнения. В частности, он должен возвращать True тогда и только тогда, когда текущий объект больше объекта, переданного как аргумент. Используемые критерии определяет программист. Под текущим объектом мы подразумеваем объект, для которого метод вызван через точечную нотацию ..

class Product:
    def __init__(self, name: str, price: float):
        self.__name = name
        self.__price = price

    def __str__(self):
        return f"{self.__name} (price {self.__price})"

    @property
    def price(self):
        return self.__price

    def __gt__(self, another_product):
        return self.price > another_product.price

В реализации выше метод __gt__ возвращает True, если цена текущего продукта больше цены продукта, переданного в качестве аргумента. Иначе метод возвращает False.

Теперь оператор сравнения > доступен для использования с объектами типа Product:

orange = Product("Orange", 2.90)
apple = Product("Apple", 3.95)

if orange > apple:
    print("Orange is greater")
else:
    print("Apple is greater")
Пример вывода

Apple is greater

Как сказано выше, именно программист определяет критерий, по которому решается, что больше, а что меньше. Например, мы могли бы решить, что порядок должен основываться не на цене, а на алфавитном порядке имен. Тогда orange теперь был бы "больше чем" apple, потому что "orange" идет позже по алфавиту.

class Product:
    def __init__(self, name: str, price: float):
        self.__name = name
        self.__price = price

    def __str__(self):
        return f"{self.__name} (price {self.__price})"

    @property
    def price(self):
        return self.__price

    @property
    def name(self):
        return self.__name

    def __gt__(self, another_product):
        return self.name > another_product.name
Orange = Product("Orange", 4.90)
Apple = Product("Apple", 3.95)

if Orange > Apple:
    print("Orange is greater")
else:
    print("Apple is greater")
Пример вывода

Orange is greater

Другие операторы

Здесь приведена таблица стандартных операторов сравнения вместе с методами, которые нужно реализовать, если мы хотим сделать их доступными для наших объектов:

ОператорТрадиционное значениеИмя метода
<Меньше чем__lt__(self, another)
>Больше чем__gt__(self, another)
==Равно__eq__(self, another)
!=Не равно__ne__(self, another)
<=Меньше или равно__le__(self, another)
>=Больше или равно__ge__(self, another)

Можно также реализовать некоторые другие операторы, включая следующие арифметические операторы:

ОператорТрадиционное значениеИмя метода
+Сложение__add__(self, another)
-Вычитание__sub__(self, another)
*Умножение__mul__(self, another)
/Деление (результат с плавающей точкой)__truediv__(self, another)
//Деление (целочисленный результат)__floordiv__(self, another)

Другие операторы и имена методов легко найти в интернете. Также помните о команде dir, которая выводит методы, доступные для заданного объекта.

Очень редко необходимо реализовывать все арифметические операторы и операторы сравнения в собственных классах. Например, деление - операция, которая редко имеет смысл вне числовых объектов. Что было бы результатом деления объекта Student на три или на другой объект Student? Тем не менее некоторые из этих операторов часто очень полезны и в собственных классах. Выбор методов для реализации зависит от того, что имеет смысл с учетом свойств ваших объектов.

Рассмотрим класс, моделирующий одну заметку. Если мы реализуем метод __add__ внутри определения класса, оператор сложения + станет доступен для объектов Note:

from datetime import datetime

class Note:
    def __init__(self, entry_date: datetime, entry: str):
        self.entry_date = entry_date
        self.entry = entry

    def __str__(self):
        return f"{self.entry_date}: {self.entry}"

    def __add__(self, another):
        # The date of the new note is the current time
        new_note = Note(datetime.now(), "")
        new_note.entry = self.entry + " and " + another.entry
        return new_note
entry1 = Note(datetime(2016, 12, 17), "Remember to buy presents")
entry2 = Note(datetime(2016, 12, 23), "Remember to get a tree")

# These notes can be added together with the + operator
# This calls the  __add__ method in the Note class
both = entry1 + entry2
print(both)
Пример вывода

2020-09-09 14:13:02.163170: Remember to buy presents and Remember to get a tree

Строковое представление объекта

Вы уже реализовали довольно много методов __str__ в своих классах. Как вы знаете, этот метод возвращает строковое представление объекта. Еще один довольно похожий метод - __repr__, который возвращает техническое представление объекта. Метод __repr__ часто реализуют так, чтобы он возвращал программный код, выполнение которого создаст объект с идентичным содержимым текущему объекту.

Функция repr возвращает это техническое строковое представление объекта. Техническое представление также используется всякий раз, когда для объекта не определен метод __str__. Пример ниже сделает это понятнее:

class Person:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age
        
    def __repr__(self):
        return f"Person({repr(self.name)}, {self.age})"
person1 = Person("Anna", 25)
person2 = Person("Peter", 99)
print(person1)
print(person2)
Пример вывода

Person('Anna', 25) Person('Peter', 99)

Обратите внимание, что сам метод __repr__ использует функцию repr, чтобы получить техническое представление строки. Это нужно, чтобы включить символы ' в результат.

Следующий класс содержит определения и для __repr__, и для __str__:

class Person:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age
        
    def __repr__(self):
        return f"Person({repr(self.name)}, {self.age})"

    def __str__(self):
        return f"{self.name} ({self.age} years)"
Person = Person("Anna", 25)
print(Person)
print(repr(Person))
Пример вывода

Anna (25 years) Person('Anna', 25)

Стоит упомянуть, что для структур данных, например списков, Python всегда использует метод __repr__ для строкового представления содержимого. Иногда это может выглядеть немного неожиданно:

persons = []
persons.append(Person("Anna", 25))
persons.append(Person("Peter", 99))
persons.append(Person("Mary", 55))
print(persons)
Пример вывода

[Person('Anna', 25), Person('Peter', 99), Person('Mary', 55)]

Loading
Loading

Итераторы

Мы знаем, что инструкцию for можно использовать для итерации по многим разным структурам данных, файлам и коллекциям элементов. Типичным примером использования была бы следующая функция:


def count_positives(my_list: list):
    n = 0
    for item in my_list:
        if item > 0:
            n += 1
    return n

Функция проходит по элементам списка один за другим и отслеживает, сколько элементов были положительными.

Собственные классы тоже можно сделать итерируемыми. Это полезно, когда основная цель класса связана с хранением коллекции элементов. Класс Bookshelf из предыдущего примера был бы хорошим кандидатом, поскольку логично использовать цикл for, чтобы пройти по книгам на полке. То же относится, скажем, к реестру студентов. Возможность итерироваться по коллекции студентов может быть полезной.

Чтобы сделать класс итерируемым, нужно реализовать методы итератора __iter__ и __next__. Мы вернемся к подробностям этих методов после следующего примера:

class Book:
    def __init__(self, name: str, author: str, page_count: int):
        self.name = name
        self.author = author
        self.page_count = page_count

class Bookshelf:
    def __init__(self):
        self._books = []

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

    # This is the iterator initialization method
    # The iteration variable(s) should be initialized here
    def __iter__(self):
        self.n = 0
        # the method returns a reference to the object itself as 
        # the iterator is implemented within the same class definition
        return self

    # This method returns the next item within the object
    # If all items have been traversed, the StopIteration event is raised
    def __next__(self):
        if self.n < len(self._books):
            # Select the current item from the list within the object
            book = self._books[self.n]
            # increase the counter (i.e. iteration variable) by one
            self.n += 1
            # return the current item
            return book
        else:
            # All books have been traversed
            raise StopIteration

Метод __iter__ инициализирует переменную или переменные итерации. В этом случае достаточно простого счетчика, содержащего индекс текущего элемента в списке. Также нужен метод __next__, который возвращает следующий элемент итератора. В примере выше метод возвращает элемент с индексом n из списка внутри объекта Bookshelf, а переменная итератора также увеличивается.

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

Теперь наш Bookshelf готов к итерации, например с помощью цикла for:

if __name__ == "__main__":
    b1 = Book("The Life of Python", "Montague Python", 123)
    b2 = Book("The Old Man and the C", "Ernest Hemingjavay", 204)
    b3 = Book("A Good Cup of Java", "Caffee Coder", 997)

    shelf = Bookshelf()
    shelf.add_book(b1)
    shelf.add_book(b2)
    shelf.add_book(b3)

    # Print the names of all the books
    for book in shelf:
        print(book.name)
Пример вывода

The Life of Python The Old Man and the C A Good Cup of Java

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

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