Приемы объектно-ориентированного программирования
Класс может содержать метод, который возвращает объект того же самого класса. Например, ниже у нас есть класс 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_saleapple1 = 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 Productapple = 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.nameOrange = 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_noteentry1 = 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)]
Итераторы
Мы знаем, что инструкцию 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
Текущие баллы можно посмотреть в синем индикаторе в правом нижнем углу страницы.