Перегрузка операторов контейнера
Возможности перегрузки операторов в Python не ограничиваются базовыми арифметическими операциями и сравнением объектов, так как существуют методы, позволяющие создать свою собственную коллекцию, то есть класс, представляющий собой контейнер элементов, как список или словарь. Объекты такого класса могут быть перебраны в цикле for, использовать оператор in и функцию len(), а также синтаксис обращения по индексу или ключу через квадратные скобки.
Создание итерируемых объектов
Методы __iter__() и __next__() лежат в основе механизма итерации в Python и позволяют перебирать объекты в цикле for или использовать их в других функциях, которые принимают итерируемый объект, например, sum() или zip(). Также эти методы можно явно вызывать с помощью функций iter() и next(), как мы уже делали для встроенных типов данных, когда рассматривали работу итерируемых объектов.
Метод __iter__() превращает объект в итератор. В простейшем случае, сам итерируемый объект часто выступает в роли своего итератора, поэтому __iter__() просто возвращает self.
Метод __next__() является основным рабочим механизмом итератора. Он возвращает следующий элемент в последовательности. При этом, когда элементов больше нет, метод обязан вызвать исключение StopIteration. Именно это исключение сигнализирует циклу for о том, что итерация закончена.
Рассмотрим класс Countdown, объекты которого выполняют обратный отсчет от заданного числа start до нуля:
class Countdown:
def __init__(self, start: int):
self.current = start # Начальное значение
# Создание итератора из объекта
def __iter__(self):
return self
# Возврат следующего элемента итератора
def __next__(self):
if self.current <= 0:
# Условие достигнуто
raise StopIteration
# Возвращаем текущее значение
result = self.current
# Уменьшаем его для следующего шага
self.current -= 1
return result
Объект этого класса можно перебрать в цикле for:
countdown = Countdown(5)
for number in countdown:
print(number, end=" ")
# Вывод: 5 4 3 2 1
Когда Python выполняет цикл for number in countdown происходит следующая последовательность шагов:
- Вызывается функция
iter(countdown), которая вызывает метод__iter__()объектаcountdown. - Метод
__iter__()возвращает объект-итератор. В данном случае это сам объектcountdown. - На каждой итерации цикла вызывается функция
next()для объекта-итератора, что приводит к вызову метода__next__(). - Когда в методе
__next__()выполняется выражениеraise StopIteration, циклforзавершается.
Класс Countdown реализует оба метода __iter__() и __next__(), что делает его одновременно итерируемым объектом и одноразовым итератором. И так как состояние self.current изменяется в процессе перебора, то при повторном обращении метод __next__() сразу вызывает StopIteration и мы не можем использовать этот итератор ещё раз, например, в функции sum():
print(sum(countdown))
# Вывод: 0
Решение этой проблемы заключается в разделении этих ролей на два класса. Основной класс должен быть только итерируемым объектом, который создаёт новый независимый объект-итератор с помощью вспомогательного класса при каждом вызове __iter__().
В таком случае метод __iter__() класса Countdown должен возвращать итератор не как объект self, а как экземпляр класса CountdownIterator. Данный класс при инициализации принимает исходное значение start и реализует механизм получения каждого следующего элемента методе __next__():
class CountdownIterator:
def __init__(self, start: int):
# Итератор хранит своё собственное состояние
self.current = start
def __next__(self) -> int:
if self.current <= 0:
raise StopIteration
result = self.current
self.current -= 1
return result
class Countdown:
def __init__(self, start: int):
# Храним исходное значение, которое никогда не меняется
self.start = start
def __iter__(self) -> "CountdownIterator":
# Каждый вызов iter() создаёт новый итератор с исходным значением
return CountdownIterator(self.start)
Теперь мы можем многократно использовать итератор одного и того же объекта:
countdown = Countdown(3)
# Первая итерация
for number in countdown:
print(number, end=" ")
# Вывод: 3 2 1
# Использование в функции sum() (вторая итерация)
total = sum(countdown)
print(f"\nСумма элементов: {total}") # 3 + 2 + 1 = 6
# Вывод: Сумма элементов: 6
# Третья итерация
for number in countdown:
print(number, end=", ") # 3, 2, 1
# Вывод: 3, 2, 1
Перегрузка оператора доступа по ключу/индексу
Методы __getitem__(), __setitem__() и __delitem__() позволяют классу имитировать поведение упорядоченных коллекций, таких как списки и словари, и обращаться к элементам с помощью квадратных скобок:
- Метод
__getitem__(self, key)предназначен для чтения элемента по ключуkey, например, числу (индексу). - Метод _
_setitem__(self, key, value)предназначен для добавления или изменения значенияvalueпо ключуkey. - Метод
__delitem__(self, key)предназначен для удаления элемента по ключуkey.
При этом метод __getitem__() возвращает элемент по ключу, а методы __setitem__() и __delitem__() изменяют сам объект и возвращают None. Также если элемент, к которому обращается один из этих методов не найден, то можно настроить вызов исключений KeyError или IndexError.
Давайте разработаем класс Cart для корзины в интернет-магазине. У каждой корзины есть размер size, и словарь _items с товарами, где ключами являются названия товаров, а значениями – их количество:
class Cart:
def __init__(self, size: int=99):
self._items = {}
self.size = size
def __getitem__(self, item: str) -> int:
# Вызывается при обращении к объекту по ключу.
if item not in self._items:
# Если товара нет в корзине, возвращаем None
print(f"Товар '{item}' отсутствует в корзине")
return None
# Возвращаем количество этого товара
return self._items[item]
def __setitem__(self, item: str, quantity: int) -> None:
# Вызывается при установке нового значения по ключу
if len(self._items) <= self.size:
self._items[item] = quantity
print(f"Добавлен/обновлён товар '{item}', {quantity} шт.")
else:
print("Корзина заполнена")
def __delitem__(self, item: str) -> None:
# Вызывается при удалении объекта по ключу
if item in self._items:
print(f"Товар '{item}' удален из корзины")
del self._items[item]
else:
print(f"Товар '{item}' отсутствует в корзине")
return None
При добавлении нового товара в корзину или при изменении количества уже добавленного товара вызывается метод __setitem__(), который проверяет, не достигнуто ли максимальное количество товаров в корзине:
cart = Cart()
cart["Простой карандаш"] = 4
# Вывод: Добавлен/обновлён товар 'Простой карандаш', 4 шт.
cart["Синяя гелевая ручка"] = 2
# Вывод: Добавлен/обновлён товар 'Синяя гелевая ручка', 2 шт.
cart["Синяя гелевая ручка"] = 3
# Вывод: Добавлен/обновлён товар 'Синяя гелевая ручка', 3 шт.
При обращении к элементу по ключу вызывается метод __getitem__(), который позволяет быстро узнать текущее количество указанного товара:
print(f"Простые карандаши в корзине: {cart['Простой карандаш']} шт.")
# Вывод: Простые карандаши в корзине: 4 шт.
print(f"Ластики в корзине: {cart['Ластик']} шт.")
# Вывод: Ластики в корзине: 0 шт.
При удалении товара по его названию вызывается метод __delitem__(), который, если элемент не найден, выводит соответствующее сообщение:
del cart["Синяя гелевая ручка"]
# Вывод: Товар 'Синяя гелевая ручка' удален из корзины
del cart["Блокнот A5"]
# Вывод: Товар 'Блокнот A5' отсутствует в корзине
Таким образом, тройка методов __getitem__(), __setitem__() и __delitem__() позволяет реализовать полноценную работу с элементами объекта через квадратные скобки. При этом мы можем написать любую логику, как например, обычно при отсутствии ключа в словаре вызывается исключение KeyError, но в нашем классе Cart мы просто выводит информационное сообщение об этом, но не прерываем выполнение программы.
Перегрузка оператора in
Оператор принадлежности (in) используется для проверки, находится ли элемент в коллекции, например, в списке, словаре или множестве. Для собственных классов вы можете переопределить это поведение, определив логику членства внутри вашего объекта.
Это реализуется с помощью магического метода __contains__(self, item), который принимает ссылку на объект self и элемент item, принадлежность которого проверяется.
Рассмотрим класс Inventory для хранения предметов в игре. Инвентарь может вмещать в себя не более, чем size предметов, а названия предметов хранятся в списке stock:
class Inventory:
def __init__(self, stock: list, size: int):
self.size = size
self.stock = stock
def __contains__(self, item: str) -> bool:
return item in self.stock
def add_item(self, item: str) -> None:
if len(self.stock) <= self.size:
self.stock.append(item)
else:
print("Достигнуто максимальное количество предметов")
Метод __contains__() проверяет наличие строки item именно в атрибуте stock, а не size:
backpack = Inventory(["Лампа", "Верёвка"], 25)
backpack.add_item("Нож")
print(f"Лампа в инвентаре? {'Лампа' in backpack}")
# Вывод: Лампа в инвентаре? True
print(f"Палатка в инвентаре? {'Палатка' in backpack}")
# Вывод: Палатка в инвентаре? False
Если вы не переопределите метод __contains__() для своего класса, Python будет пытаться выполнить операцию item in obj путем итерации по объекту. Для этого сначала ищется метод __iter__(), и если он есть, то создаётся итератор и элементы перебираются в поисках искомого. Если метод __iter__() тоже отсутствует, то ищется метод __getitem__() для обращения к элементам по ключу.
Но если объект не содержит ни один из этих методов, то Python вызывает исключение TypeError.
Перегрузка функции len()
Метод __len__() позволяет определить поведение функции len(), если ей будет передан экземпляр собственного класса. Его необходимо реализовать так, чтобы он возвращал логически обоснованное целое число, представляющее длину или размер экземпляра этого класса. При этом метод __len__(self) принимает только ссылку на экземпляр класса self и должен вернуть неотрицательное целое число
Представим класс TaskManager, который управляет списком задач. Длина этого объекта должна соответствовать количеству невыполненных задач. Сам список задач хранится в атрибуте tasks – словаре, новые элементы в который добавляются с помощью метода add_tasks():
class TaskManager:
def __init__(self):
# Список задач, где каждая задача - это словарь {Задача: Статус}
self.tasks = {}
def add_task(self, description: str, is_done=False) -> None:
self.tasks[description] = is_done
def __len__(self) -> int:
# Считаем задачи, где статус 'is_done' равен False
active_count = 0
for status in self.tasks.values():
if status is False:
active_count += 1
return active_count
По умолчанию словарь tasks с задачами – пустой, поэтому функция len() возвращает ноль:
manager = TaskManager()
print(len(manager))
# Вывод: 0
Но мы можем добавить в него задачи (выполненные со статусом True и невыполненные со статусом False). Тогда функция len() вернёт количество невыполненных или активных задач (со статусом False):
manager.add_task("Написать отчет", False)
manager.add_task("Ответить на письмо", False)
manager.add_task("Позвонить клиенту", True) # Выполнена
manager.add_task("Запланировать встречу", False)
print(f"Количество активных задач: {len(manager)}")
# Вывод: Количество активных задач: 3
То есть функция len() исключает задачу "Позвонить клиенту", так как её статус равен True.
Перегрузка метода __len__() имеет дополнительное, очень важное следствие: она влияет на то, как ваш объект интерпретируется с точки зрения его истинности или ложности:
- Объект считается ложным (
False), если функцияlen()возвращает 0. - Объект считается истинным (
True) если функцияlen()возвращает любое число, отличное от 0, то есть объект непустой.
В нашем примере, если нет активных задач или все задачи выполнены и функция len() вернула 0, то объект manager считается ложным, что является логически правильным поведением для пустого менеджера задач.
Перегрузка функции bool()
Однако мы можем явно определить истинность или ложность объекта с помощью метода __bool__(self), который принимает ссылку на объект self и вызывается всякий раз, когда Python пытается преобразовать экземпляр вашего класса в логическое значение, то есть явно или неявно вызывает функцию bool(). Это происходит, например, в условном выражении, цикле while или при использовании логических операторов.
Метод __bool__(self) обладает более высоким приоритетом при определении логического значения объекта, чем метод __len__(), и сначала Python пытается вызвать именно него. Но если ни один из этих методов не определён, то по умолчанию объект считается истинным.
Представим класс Permission для управления правами доступа пользователей. По умолчанию у пользователя нет ролей, то есть множество roles пустое, а статус его активности is_active равен True. Логическое значение объекта должно отражать, действительно ли у пользователя есть какие-либо права, поэтому метод __bool__() возвращает True только в том случае, если у пользователя есть хотя бы одна роль и он активный:
class Permission:
def __init__(self, roles: list | None=None, is_active: bool=True):
self.roles = set(roles) if roles is not None else set()
self.is_active = is_active
def __bool__(self) -> bool:
# Объект считается истинным, если:
# - у пользователя есть хотя бы одна роль
# - пользователь активный
return len(self.roles) > 0 and self.is_active
То есть логическая истинность объекта зависит от двух атрибутов roles и is_active, что выходит за рамки простой проверки длины:
admin = Permission(roles=["Администратор", "Модератор"], is_active=True)
if admin: # Вызывается p_admin.__bool__()
print(f"Доступ разрешен")
# Вывод: Доступ разрешён
inactive_user = Permission(roles=['view'], is_active=False)
if not inactive_user:
print(f"Доступ запрещен (Пользователь неактивен)")
# Вывод: Доступ запрещен (Пользователь неактивен)
Хотя Python может использовать метод __len__() для определения логического значения, явное определение метода __bool__() предпочтительнее, когда логика логического значения сложнее, чем проверка того, является ли объект пустым.
Примеры
Пример 1. Хранение поисковых запросов
Класс SearchHistory предназначен для хранения пользовательских поисковых запросов. История поиска представляет собой список поисковых запросов, добавленных с помощью метода add_query(). Оператор in позволяет проверить наличие запроса в этом списке, функция len() возвращает длину этого списка, а также этот список можно перебрать в цикле for:
class SearchHistory:
"""Описывает историю поиска."""
def __init__(self):
"""Конструктор класса SearchHistory."""
self.queries = []
def add_query(self, query: str) -> None:
"""Добавляет новый запрос в историю.
Параметры:
query: Строка с поисковым запросом.
"""
self.queries.append(query.lower())
def __contains__(self, item: str) -> bool:
"""Перегрузка оператора in.
Проверяет наличие запроса в истории."""
return item.lower() in self.queries
def __len__(self) -> int:
"""Перегрузка функции len().
Возвращает количество запросов в истории."""
return len(self.queries)
def __iter__(self) -> "SearchHistory":
"""Возвращает итератор и подготавливает копию данных для перебора."""
self._iter_list = self.queries[:]
self._iter_index = 0
return self
def __next__(self) -> str:
"""Возвращает следующий элемент при переборе."""
if self._iter_index >= len(self._iter_list):
raise StopIteration
result = self._iter_list[self._iter_index]
self._iter_index += 1
return result
history = SearchHistory()
history.add_query("новости спорта")
history.add_query("как пришить пуговицу")
history.add_query("смотреть шерлок онлайн бесплатно")
# Наличие поискового запроса в истории
print(f"Был сделан запрос 'новости спорта'? {'новости спорта' in history}")
print(f"Был сделан запрос 'милые кружки'? {'милые кружки' in history}")
# Длина истории
print(f"Количество запросов: {len(history)}")
# Перебор поисковых запросов
print("История запросов:")
for query in history: # Вызываются __iter__() и __next__()
print(f"- {query}")
В этом примере объект SearchHistory сам является итератором. Однако метод __iter__() не просто возвращает объект self, а подготавливает внутреннее состояние для перебора, создавая атрибуты iter_list и iter_index с копией списка queries и начальным индексом, которые и используются в методе __next__(). Это позволяет использовать итератор несколько раз без создания дополнительного класса.
Вывод:
Был сделан запрос 'новости спорта'? True
Был сделан запрос 'милые кружки'? False
Количество запросов: 3
История запросов:
- новости спорта
- купить ворота
- смотреть шерлок онлайн бесплатно
Пример 2. Проверка принадлежности точки заданной территории
Класс GeographicZone определяет границы территории, используя минимальную и максимальную широту (атрибуты min_lat и max_lat) и долготу (атрибуты min_lon и max_lon). Точка с координатами (кортеж или список из двух чисел), принадлежит этой территории, если её широта (первая координата) находится в диапазоне от min_lat до max_lat, а долгота (вторая координата) – в диапазоне от min_lon до max_lon. Также объект, описывающий территорию, считается истинным только в том случае, если эта территория имеет ненулевую площадь:
class GeographicZone:
"""Описывает границы территории."""
def __init__(
self,
min_lat: int | float,
max_lat: int | float,
min_lon: int | float,
max_lon: int | float
):
"""Конструктор класса GeographicZone.
Параметры:
min_lat: Минимальная широта (от -90.0 до 90.0).
max_lat: Максимальная широта (от -90.0 до 90.0).
min_lon: Минимальная долгота (от -180.0 до 180.0).
max_lon: Максимальная долгота (от -180.0 до 180.0).
"""
# Базовая проверка широты
if not (-90.0 <= min_lat <= max_lat <= 90.0):
raise ValueError("Неверное значение широты")
# Базовая проверка долготы
if not (-180.0 <= min_lon <= 180.0 and -180.0 <= max_lon <= 180.0):
raise ValueError("Неверное значение долготы")
self.min_lat = min_lat
self.max_lat = max_lat
self.min_lon = min_lon
self.max_lon = max_lon
def __str__(self) -> str:
"""Пользовательское описание объекта"""
return (f"Широта: от {self.min_lat} до {self.max_lat}."
f"Долгота: от {self.min_lon} до {self.max_lon}")
def __contains__(self, coordinate: tuple[int | float]) -> bool:
"""Перегрузка оператора in.
Точка находится внутри территории, если её широта находится
в пределах минимальной и максимальной широты, и её долгота
находится в пределах минимальной и максимальной долготы
"""
lat, lon = coordinate[0], coordinate[1]
is_lat_valid = self.min_lat <= lat <= self.max_lat
is_lon_valid = self.min_lon <= lon <= self.max_lon
return is_lat_valid and is_lon_valid
def __bool__(self) -> bool:
"""Перегрузка функции bool()
Территория считается истинной (True), если она имеет ненулевую
площадь.
"""
return self.min_lat < self.max_lat and self.min_lon < self.max_lon
moscow_zone = GeographicZone(55.5, 56.0, 37.0, 38.0) # Москва
point_in_moscow = (55.752, 37.618) # Кремль
point_in_spb = (59.939, 30.315) # Эрмитаж
# Проверка принадлежности точки территории
print(f"Точка {point_in_moscow} в Москве? {point_in_moscow in mos-cow_zone}")
print(f"Точка {point_in_spb} в Москве> {point_in_spb in moscow_zone}")
# Проверка истинности точки
print(f"Москва существует? {bool(moscow_zone)}")
neverland = GeographicZone(0, 0, 0, 0)
print(f"Неверленд существует? {bool(neverland)}")
Вывод:
Точка (55.752, 37.618) в Москве? True
Точка (59.939, 30.315) в Москве> False
Москва существует? True
Неверленд существует? False
Пример 3. Управление настройками в игре
Класс GameSettings предназначен для управления настройками в игре. Настройки хранятся в атрибуте data, с которым работают операции чтения, записи и удаления элементов по ключу. Метод set_default() изменяет этот словарь и устанавливает значения настроек по умолчанию:
class GameSettings:
"""Описывает настройки игры."""
def __init__(self):
"""Конструктор класса GameSettings"""
self._data = {} # Внутреннее хранилище настроек
def set_default(self) -> None:
"""Устанавливает настройки по умолчанию"""
self._data = {
"Яркость": 50,
"Громкость": 75,
"Разрешение": "1920 x 1080",
"Качество": "Высокое",
}
print(f"Установлены настройки по умолчанию:", self._data)
def __getitem__(self, key: str) -> str | int:
"""Перегрузка чтения значения элемента по ключу."""
# Если ключа нет, вызывается стандартное исключение KeyError
return self._data[key]
def __setitem__(self, key: str, value: int | str) -> None:
"""Перегрузка добавления/обновления значения по ключу."""
# Можно добавить логику проверки типа или диапазона значений
if key == "Яркость" and not (0 <= value <= 100):
print("Яркость должна быть от 0 до 100")
return
self._data[key] = value
def __delitem__(self, key: str) -> None:
"""Перегрузка удаления элемента по ключу."""
print(f"Настройка '{key}' удалена")
del self._data[key]
settings = GameSettings()
settings.set_default()
# Добавление нового значения по ключу (__setitem__())
settings["Яркость"] = 80
# Изменение существующего значения по ключу (__setitem__())
settings["Язык"] = "Русский язык"
# Чтение значения по ключу (__getitem__())
print(f"Текущее разрешение: {settings['Разрешение']}")
# Удаление элемента по ключу (__delitem__())
del settings['Громкость']
Вывод:
Установлены настройки по умолчанию: {'Яркость': 50, 'Громкость': 75, 'Разрешение': '1920 x 1080', 'Качество': 'Высокое'}
Текущее разрешение: 1920 x 1080
Настройка 'Громкость' удалена
Итоги
|
Метод |
Операция |
Описание |
Возвращаемое значение |
|---|---|---|---|
|
|
|
Проверка принадлежности |
|
|
|
|
Получение размера объекта |
Целое неотрицательное число |
|
|
|
Создание итератора |
Итератор |
|
|
|
Последовательное получение элемента объекта |
Соответствующий элемент объекта |
|
|
|
Определение истинности объекта |
True или False |
|
|
|
Обращение по ключу |
Соответствующее значение |
|
|
|
Запись значения по ключу |
|
|
|
|
Удаление значения по ключу |
|
Задания для самопроверки
1. Что делают методы __iter__() и __next__()? Почему они обычно используются вместе?
Оба метода __iter__() и __next__() являются ключевыми для создания итератора в Python. Метод __iter__() возвращает итератор, а метод __next__() отвечает за возврат следующего элемента в последовательности при каждой итерации и вызывает исключение StopIteration, когда элементы заканчиваются. Они используются вместе, потому что __iter__() создаёт объект, который умеет итерироваться, а __next__() обеспечивает извлечение элементов из этого объекта.
2. Создайте класс для студенческой группы StudentsGroup, который инициализируется атрибутом students со списком студентов и group_number для номера группы. Реализуйте метод __len__(), который возвращает количество студентов в списке students. Создайте произвольный объект класса StudentsGroup и с помощью функции len() выведите на экран его длину.
class StudentsGroup:
def __init__(self, students: list[str], group_number: str):
self.students = students
self.group_number = group_number
def __len__(self):
return len(self.students)
students = [
"Карамзин Н.М.",
"Соловьёв С.М.",
"Ключевский В.О.",
"Костомаров Н.И.",
"Татищев В.Н.",
"Покровский М.Н.",
"Рыбаков Б.А."
]
history_group = StudentsGroup(students, "Ист-401")
print(len(history_group))
# Вывод: 7
3. Создайте класс для недели Week, который инициализируется атрибутом days со значением по умолчанию days = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"]. Реализуйте метод __getitem__(), который возвращает день недели по его индексу (например, [0] должен вернуть "Понедельник"). Выведите на экран Week()[1].
class Week:
def __init__(self):
self.days = [
"Понедельник",
"Вторник",
"Среда",
"Четверг",
"Пятница",
"Суббота",
"Воскресенье"
]
def __getitem__(self, index: int) -> str:
return self.days[index]
print(Week()[1])
# Вывод: Вторник
4. Создайте класс для температуры Temperature, который инициализируется атрибутами data для списка температур и scale для шкалы со значением по умолчанию scale = "Цельсий". Реализуйте в этом классе метод __contains__(), который возвращает True, если переданное значение содержится в списке data, иначе – False. Используя оператор in, проверьте содержится ли число 1 в объекте Temperature([0, -1, 5, 3, -2, -1, 1, 2, 2]) и выведите результат на экран.
class Temperature:
def __init__(self, data: list[int, float], scale: str = "Цельсий"):
self.data = data
self.scale = scale
def __contains__(self, item: int | float) -> bool:
return item in self.data
print(1 in Temperature([0, -1, 5, 3, -2, -1, 1, 2, 2]))
# Вывод: True
5. Создайте класс для зачётной книжки GradeBook, который инициализируется атрибутом grades для словаря с оценками и со значением по умолчанию grades = {}. Реализуйте метод __setitem__(), который добавляет в этот словарь оценку (значение) по имени студента (ключу). Если оценка не является числом, то он должен вызывать исключение TypeError. Создайте объект класса GradeBook и добавьте в него трёх студентов и их оценки. Выведите на экран значение атрибута grades.
class GradeBook:
def __init__(self):
self.grades = {}
def __setitem__(self, student_name: str, grade: int | float) -> None:
if not isinstance(grade, (int, float)):
raise TypeError("Оценка должна быть числом")
self.grades[student_name] = grade
grade_book = GradeBook()
grade_book["Баженов В.И."] = 4.5
grade_book["Казаков М.Ф."] = 5.0
grade_book["Ворохин А.Н."] = 3.8
print(grade_book.grades)
# Вывод: {'Баженов В.И.': 4.5, 'Казаков М.Ф.': 5.0, 'Ворохин А.Н.': 3.8}
0 комментариев