Читать книгу ООП на Python. 10 небольших моделей с объяснениями (Глеб Зайцев) онлайн бесплатно на Bookz (2-ая страница книги)
ООП на Python. 10 небольших моделей с объяснениями
ООП на Python. 10 небольших моделей с объяснениями
Оценить:

4

Полная версия:

ООП на Python. 10 небольших моделей с объяснениями

К списку моделей

Модель 3. Чек-лист владеет своими отметками

Ситуация и обещания

Различить передачу ссылки, копирование контейнера и создание нового предметного объекта.

Для подготовки двух небольших залов используют один шаблон пунктов: проверить свет и разместить афишу. Если оба чек-листа получают ссылку на общий изменяемый список отметок, готовность одного зала неожиданно появится в другом. Сходство начальных данных не означает, что дальнейшее состояние должно быть общим.

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

Контракт модели

Конструктор принимает точный list или tuple; каждый элемент — точный tuple длины 2 с ID и названием. Неверная форма вызывает TypeError.

ID и название — точные непустые после проверки strip() строки; сохраняется исходное написание. Другой тип вызывает TypeError, пустой текст — ValueError.

ID уникальны внутри чек-листа: повтор вызывает ValueError при создании. Пустой чек-лист допустим; порядок соответствует входу.

mark(ID, done=True) принимает только точный bool в done; другой тип даёт TypeError. Допустимый неизвестный ID вызывает KeyError. Успех возвращает None.

Повторная такая же отметка допустима и ничего дополнительно не меняет; done=False снимает отметку. Отказ не меняет ни пункты, ни отметки.

snapshot() возвращает кортеж троек (ID, название, bool). Входной контейнер и разные экземпляры независимы; снимок не обновляется после mark().

В упражнении fresh() создаёт самостоятельный Checklist с теми же ID, названиями и порядком, но без отметок. Старый объект остаётся прежним.

Почему такая граница объекта

Владение данными означает здесь простое правило: только этот экземпляр через свои публичные операции меняет собственные отметки. Для этого недостаточно написать self._items = items — такое присваивание сохранило бы ссылку на чужой контейнер. Конструктор собирает новый словарь, а для отметок создаёт отдельное пустое множество.

Пункты хранятся по ID, потому что отметка должна относиться к определённому пункту, а не к его позиции на экране. Множество хранит ID завершённых пунктов. Из этого следует инвариант: каждый ID в множестве обязан присутствовать в словаре. mark проверяет существование до изменения множества.

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

Конструктор проверяет пары во временном словаре local, а поля экземпляра присваивает после окончания проверки. Недопустимый поздний пункт не приводит к частично пригодному результату и не меняет исходный список. Отметки никогда не приходят извне: начальное состояние всегда однозначно.

snapshot — это значение на момент вызова, а не окно во внутреннее состояние. Внутри результата только кортежи, строки и bool. Такой формат чуть менее удобен для редактирования, чем список словарей, зато клиент не может случайно отметить пункт через возвращённую коллекцию.

Полная программа

001│def checked_text(value):

002│····if type(value) is not str:

003│········raise TypeError("ожидалась строка")

004│····if not value.strip():

005│········raise ValueError("текст не может быть пустым")

006│····return value

007│

008│

009│class Checklist:

010│····def __init__(self, items):

011│········if type(items) not in (list, tuple):

012│············raise TypeError("нужен список или кортеж пар")

013│········local = {}

014│········for pair in items:

015│············if type(pair) is not tuple or len(pair) != 2:

016│················raise TypeError("пункт должен быть парой")

017│············item_id, title = pair

018│············checked_text(item_id)

019│············checked_text(title)

020│············if item_id in local:

021│················raise ValueError("повтор ID")

022│············local[item_id] = title

023│········self._items = local

024│········self._done = set()

025│

026│····def mark(self, item_id, done=True):

027│········checked_text(item_id)

028│········if type(done) is not bool:

029│············raise TypeError("отметка должна быть bool")

030│········if item_id not in self._items:

031│············raise KeyError(item_id)

032│········if done:

033│············self._done.add(item_id)

034│········else:

035│············self._done.discard(item_id)

036│

037│····def snapshot(self):

038│········return tuple(

039│············(item_id, title, item_id in self._done)

040│············for item_id, title in self._items.items()

041│········)

042│

043│

044│source = [("light", "свет"), ("poster", "афиша")]

045│a, b = Checklist(source), Checklist(source)

046│saved = a.snapshot()

047│a.mark("light")

048│source[0] = ("door", "дверь")

049│print(a.snapshot())

050│print(b.snapshot())

051│print(saved)

Разбор исполнения

Ожидаемый вывод

(('light', 'свет', True), ('poster', 'афиша', False))

(('light', 'свет', False), ('poster', 'афиша', False))

(('light', 'свет', False), ('poster', 'афиша', False))

A и B получают один и тот же source, но каждый конструктор создаёт собственные словарь и множество. Вызов mark для ID light меняет только множество A. Поэтому первая строка вывода содержит True у света, а вторая показывает два False.

Присваивание source[0] другой пары влияет только на исходный список. Ни в A, ни в B не появляется дверь: их словари уже содержат строки из первоначальных пар. Это не магическая глубокая копия — нам достаточно отделить контейнер, потому что разрешённые элементы неизменяемы.

saved был получен до отметки и остаётся полностью неотмеченным. В проверках дополнительно снимаем отметку дважды: discard делает повторное снятие спокойной операцией. Неизвестный ID и целое число вместо bool отвергаются до изменения, после чего снимок сверяется с явным ожиданием.

Проверки ниже добавьте после полной программы. При успехе они ничего не печатают; ожидаемый вывод остаётся тем же. Не используйте режим -O.

Проверки исходной модели

001│def reject(error, action):

002│····try:

003│········action()

004│····except error:

005│········pass

006│····else:

007│········raise AssertionError("ожидался отказ")

008│

009│

010│source = [("a", "свет"), ("b", "афиша")]

011│a, b = Checklist(source), Checklist(source)

012│saved = a.snapshot()

013│source.clear()

014│assert a.mark("a") is None

015│a.mark("a")

016│assert a.snapshot() == (("a", "свет", True),

017│························("b", "афиша", False))

018│assert b.snapshot() == (("a", "свет", False),

019│························("b", "афиша", False))

020│assert saved == (("a", "свет", False),

021│·················("b", "афиша", False))

022│reject(KeyError, lambda: a.mark("missing"))

023│reject(TypeError, lambda: a.mark("a", 1))

024│reject(TypeError, lambda: a.mark(None))

025│reject(ValueError, lambda: a.mark(" "))

026│assert a.snapshot() == (("a", "свет", True),

027│························("b", "афиша", False))

028│a.mark("a", False)

029│a.mark("a", False)

030│assert a.snapshot() == (("a", "свет", False),

031│························("b", "афиша", False))

032│assert Checklist([]).snapshot() == ()

033│duplicate = [("x", "один"), ("x", "два")]

034│reject(ValueError, lambda: Checklist(duplicate))

035│assert duplicate == [("x", "один"), ("x", "два")]

036│reject(TypeError, lambda: Checklist({"x": "свет"}))

037│reject(TypeError, lambda: Checklist([["x", "свет"]]))

038│reject(TypeError, lambda: Checklist([("x",)]))

039│reject(TypeError, lambda: Checklist([(1, "свет")]))

040│reject(ValueError, lambda: Checklist([("x", " ")]))

041│try:

042│····saved[0][2] = True

043│except TypeError:

044│····pass

045│else:

046│····raise AssertionError("снимок должен быть неизменяемым")

Меняем требование

Добавьте метод fresh(), создающий новый чек-лист из пунктов текущего: те же ID, названия и порядок, но все отметки False. Это новый цикл подготовки, а не точная копия состояния. Последующие отметки любого из двух объектов не должны затрагивать другой.

Подсказка 3

Решение упражнения 3

Когда проще без класса

Для готового перечня завершённых пунктов достаточно функции над парами и множеством ID. Если состояние меняется лишь один раз в одной функции, класс добавляет лишний протокол создания и чтения. Здесь он полезен продолжительной подготовкой и обещанием владения, а не тем, что любой список следует оборачивать в объект.

Модель не позволяет добавлять, удалять или переименовывать пункты после создания, не хранит время отметок и не разрешает совместное редактирование. Независимость объяснена для входного списка пар из строк, а не для произвольных вложенных объектов. Конечные проверки не доказывают безопасность при прямом изменении внутренних атрибутов или конкурентном доступе.

К списку моделей

Модель 4. Доска разрешает только предметные изменения

Ситуация и обещания

Выбрать композицию со списком внутри вместо наследования от списка, который разрешает слишком много операций.

Для эскиза мозаики нужна прямоугольная доска. На ней можно поставить метку R, B или Y, заменить её другой разрешённой меткой и очистить ячейку. Во время работы число строк и столбцов не меняется. Обычный список списков всё это хранит, но позволяет также удалить строку, добавить короткую строку или записать в ячейку произвольный объект.

Наследование от list ради готового хранения оставило бы эти лишние способы изменения доступными. Вместо этого MosaicBoard будет содержать список внутри и показывать только операции доски. Состав публичных действий здесь важнее сходства синтаксиса с коллекцией.

Контракт модели

rows и columns — точные положительные int; bool и остальные типы дают TypeError, ноль и отрицательные числа — ValueError.

Координаты row и column — точные int, нумеруются с нуля; отрицательные и выходящие за границы вызывают IndexError, неверные типы — TypeError.

Метка — точная str из набора R, B, Y; другой тип вызывает TypeError, другая строка — ValueError. None обозначает пустоту только внутри снимка.

place() ставит или заменяет метку, remove() очищает ячейку. Удалить уже пустую ячейку допустимо. Успешные изменяющие операции возвращают None.

Форма постоянна; snapshot() возвращает кортеж строк-кортежей со строками или None. Это независимый неизменяемый снимок, не ссылка на внутренние списки.

Любой предусмотренный отказ сохраняет доску. Разные строки внутри доски и разные экземпляры не разделяют изменяемые списки.

В упражнении protect() защищает только заполненную ячейку; пустая даёт ValueError. Повторная защита допустима, но place() и remove() защищённой ячейки дают ValueError даже при той же метке; protected() возвращает отсортированный кортеж координат.

Почему такая граница объекта

Фраза «доска содержит сетку» описывает композицию: один объект использует другой для своей работы. Она не говорит, что доска обязана вести себя как любой список. Отказ от наследования убирает обязанность поддерживать append, pop, изменение срезов и другие операции, которым нет смысла в предметном контракте.

Каждая строка создаётся отдельным выражением внутри внешнего включения. Умножение одного вложенного списка на число строк дало бы несколько ссылок на один и тот же список: изменение одной строки отразилось бы в остальных. Это вопрос владения, а не внешнего вида нарисованной сетки.

Конец ознакомительного фрагмента.

Текст предоставлен ООО «Литрес».

Прочитайте эту книгу целиком, купив полную легальную версию на Литрес.

Безопасно оплатить книгу можно банковской картой Visa, MasterCard, Maestro, со счета мобильного телефона, с платежного терминала, в салоне МТС или Связной, через PayPal, WebMoney, Яндекс.Деньги, QIWI Кошелек, бонусными картами или другим удобным Вам способом.

Вы ознакомились с фрагментом книги.

Для бесплатного чтения открыта только часть текста.

Приобретайте полный текст книги у нашего партнера:


Полная версия книги

Всего 10 форматов

bannerbanner