Наследование и исключения
Наследование
Просто:
Видимость:
- Поля объекта
- Поля класса
- Поля родительского класса (рекурсивно)
Вызов конструктора (например, для операция типа «"+"»):
Неправильно: return A(self.val + other.val), т. к. подменяет тип. Например:
Какого типа должно быть B()+B()?
Использование type()
Производный класс можно задать при помощи type() с тремя параметрами (имя, список родителей, словарь полей):
Родительский прокси-объект super()
super() возвращает пространство имён, содержащее атрибуты родительского класса
при создании super()-объекта не создаётся экземпляр родительского класса (например, не вызывается __new__() и т. п.)
Вызов методов базового класса:
super() как-то сам добирается до пространства имён класса, ему не нужен self(). Это неприятно похоже на магию ☺.
Защита от коллизии имён
- Если пользователь класса перегрузил поле родительского класса, значит, он так хотел
- Если он так не хотел, но перегрузил, ССЗБ
Исключение: разработчик родительского класса не хотел, чтобы поле случайно перегружали
- Если оно публичное — getter/setter/deleter (потом)
Если оно приватное — назвать его «__что-то»
Поле __чтото класса какойто в действительности называется _какойто__чтото
Если пользователь перегрузил это имя — ССЗБ премиум-класса
- ⇒ Это не сокрытие имени, а защита от коллизий
Множественное наследование
Общая задача: унаследовать атрибуты некоторого множества классов.
- Эти классы сами могут быть производными, в т. ч друг от друга
⇒ тип графа наследования — это сеть
- В этом графе там и сям могут встречаться методы с одинаковыми названиями. Какой из них актуален для производного класса?
⇒ Проблема Method Resolution Order, MRO
Проблема ромбовидного наследования (примитивные MRO):
- Поиск в порядке объявления списка наследников (вглубину):
⇒ Обход в глубину добирается до A.v раньше, чем до C.v
- Поиск по уровням старшинства (в ширину):
⇒ Обход в ширину добирается до A.v раньше, чем до B.v
Линеаризация
Линеаризация — это создание линейного списка родительских классов для поиска методов в нём, в этом случае MRO — это последовательный просмотр списка до первого класса, содержащего подходящее имя.
Монотонность: соблюдение порядка наследования
Если для класса class B(…, A, …): порядок поиска полей такой:
B: [B, …, A, …]
⇒ то для класса C(…, B, …): порядок должен быть таким:
[C, …, B, …, A, …]
Соблюдение порядка объявления:
class C(D, E):
→ [C, …, D, …, E, …]
Два разных порядка могут конфликтовать
⇒ Попытка создать непротиворечивый MRO чревата обманутыми ожиданиями
MRO C3
Общий принцип: обход дерева в ширину, при котором
Узел считается доступным на текущем уровне, если он соответствует двум порядкам сразу
- Если в какой-то момент дальнейший обход невозможен (все оставшиеся узлы согласно минимум одному порядку не находятся на текущем уровне), порождается исключение
Например, обход невозможен, если класс A является базовым для класса B, а в порядке объявления стоит позже (см. пример ниже)
Описание:
В Википедии слишком коротко
статья Gaël Pegliasco тоже недлинная, но зато с исторической справкой
Статья Елены Шамаевой «MROC3 — не магия, а справедливое слияние очередей» Описание с примерами в документации Python
Алгоритм
Если коротко: MRO C3 линеаризация — это обычный алгоритм слияния очередей, применённый к N+1 списку:
- Сам класс + N родительских классов в порядке, взятом из объявления этого класса
до N. N линеаризаций — для каждого родительского класса
Слияние очередей:
- Рассматриваем набор (всех линеаризаций + список родительских классов) слева направо
- Рассматриваем очередной элемент очередного списка, начиная с нулевого элемента
Если он входит только в начала некоторых списков (или не входит никуда),
- то есть:
не является ничьим предком и
не следует после кого-то оставшихся элементов в объявлениях классов
- добавляем его в линеаризацию
- удаляем его из всех списков
- переходим к п. 1.
- то есть:
- В противном случае возобновляем п. 2
Если в п.2 хороших кандидатов не нашлось, линеаризация невозможна
Примеры
Нет линеаризации для X, но есть для Y (базовый класс — A, находится внизу):
Y: Y + [BA, B, A] = YBA
X: X + [AB, B, A]
, невозможно выбрать очередной элемент (на самом деле — потому что порядок объявления AB конфликтует с порядком наследования BA, но это не всегда так очевидно)
Как меняется линеаризация при изменении порядка объявления:
- Простое наследование (L[X] — линеаризация класса X):
L[O] = O L[D] = D + O L[E] = E + O L[F] = F + O
- Множественное наследование (самый правый список — порядок объявления)
L[B] = B + merge(DO, EO, DE) D? Good L[B] = B + D + merge(O, EO, E) O? Not good (EO) E? Good L[B] = B + D + E + merge(O, O, …) O? Good L[B] = B + D + E + O → BDEO
соответственно,L[C] → CDFO
наконец,L[A]: A + merge(BDEO, CDFO, BC) B? + A + B + merge(DEO, CDFO, C) D? × C? + A + B + C + merge(DEO, DFO, …) D? + A + B + C + D + merge(EO, FO, …) E? + A + B + C + D + E + merge(O, FO, …) O? × F? + A + B + C + D + E + F + merge(O, O, …) O? + → ABCDEFO
То есть:
Но если написать B(E,D) вместо B(D,E):
- то получится:
- (проверьте!)
super() в множественном наследовании
super():
- как всегда — объект-прокси всех методов родительских классов
- в случае множественного наследования аналогов не имеет
- это как бы объект несуществующего класса, в котором проделан MRO, но ещё нет ни одного нового атрибута
- →
[123] <[123]>
Класс A по факту виртуальный (его экземпляры не до конца рабочие — str(…) выдаёт ошибку), предназначен только для обмазывания классов с полем .val
super() использует MRO для поиска .__init__()
Полиморфизм
Полиморфизм в случае duck typing всего один, зато тотальный! Любой метод можно применять к объекту любого класса, всё равно пока не проверишь, не поймёшь ☺.
Проверка наследования issubclass(потомок, родитель) (для удобства класс является подклассом самого себя)
isinstance(объект, класс) — является ли объект экземпляром класса или его предка:
→
False True
Про полиморфизм — всё ☺.
(На самом деле — нет, всё это ещё понадобится в случае статической типизации).
Проксирование
Попробуем унаследоваться от str и добавить туда унарный - (который будет переворачивать строку)
Проблема: какого типа должна быть -строка?
Проблема поглобальнее: какого типа должны быть результаты всех остальных строковых операций (например, .upper())?
Автоматически перезадать спецметоды в классе (а их нужно почти все обернуть в преобразование типа) можно только при использовании классической модели. Не будут работать
Стандартные классы, написанные на Си (например. str)
Классы, использующие «слоты» (будет на следующей лекции)
- Вообще всякие динамические эксперименты над пространством имён класса
Решение: хранить «родительский» объект в виде поля, а все методы нового класса делать обёрткой вокруг методов родительского объекта.
Как эта проблема решена в collections.UserString (см. тут)
Возможно, ту же задачу можно решить с помощью метаклассов и __new__() (будет на следующей лекции)
Исключения
Исключения – это механизм управления вычислительным потоком, который завязан на разнесении по коду проверки свойств данных и обработки результатов этой проверки.
Оператор try:
Клауза except Исключение
Исключения — объекты Python3 (унаследованы от BaseException)
- Дерево исключений, перехват всех дочерних
Собственные исключения (унаследованы от Exception, а не BaseException — некоторые исключения перехватывать не стоит)
А теперь попереставляем пары строк except … print()
Вариант except Исключение as идентификатор, произвольные параметры исключения
Поле .args
Клауза else — если исключений не было
Клауза finally — выполняется даже если исключение не перехвачено
TODO: рассказ про with (втч для прака нужно)
Управление вычислениями
Исключение — это не «ошибка», а нелинейная передача управления, способ обработки некоторых условий не там, где они были обнаружены.
В основном цикле никаких try:
В divisor() — никаких except()
Исключение переключает поток вычислений в место соответствующего except
- Выполнение продолжается с этого места, весь стек вызовов после него удаляется
Наличие в программе конструкций вида
except Exception:
pass
помогают избегать сообщений об исключениях и многократно затрудняют обработку ошибок и отладку.
Не делайте так!
Оператор raise
Допустим и вариант raise Exception, и raise Exception(параметры):
по идее Exception — это класс, а Exception() — объект,
- но на самом деле при входе в исключение всё равно изготавливается объект.
Пример: встроимся в протокол итерации
Посмотрим, что оно умеет, оценим матожидание длины list(Expectancy()) ☺
Если есть время, можно модифицировать пример с ZeroDivisionError на обработку числа 13.
Локальность имени в операторе as:
('QQ!', 'QQ!', 'QQ-QRKQ.')
F=Exception('QQ!', 'QQ!', 'QQ-QRKQ.')
No EВариант raise from: явная подмена или удаление причины двойного исключения.
Пример в учебнике
Нужен для различения ситуации «при обработки исключения случилось другое исключение» и ситуации «при обработке исключения мы вызвали другое исключение
Python3.11+: групповые исключения и оператор try: / except*. Используются для случаев, когда надо явно вызвать сразу несколько исключений, которые могут обрабатываться независимо:
- В примере
except * и except смешивать нельзя — это разные виды try:
except *: не бывает
Произошла фильтрация: в каждый except* приехали только соответствующие исключения
Попробуем вместо except* ValueError написать except* Exception — фильтрации не будет.
Вариант обработки с помощью обычного try: / except:
Д/З
- Прочитать:
Про C3 MRO на Хабре и в документации Python
Про исключения в учебнике и в справочнике про исключения, try и raise
EJudge: SubString 'Строки с вычитанием'
Input:Реализовать класс SubString, который бы полностью воспроизводил поведение str, но вдобавок бы поддерживал операцию вычитания строк. Вычитание устроено так: «уменьшаемое» просматривается посимвольно, и если соответствующий символ присутствует в «вычитаемом», то он однократно удаляется из обеих строк. Исходные объекты не меняются; то, что осталось от уменьшаемого, объявляется результатом вычитания.
К моменту прохождения теста ничего нового, кроме класса SubString в глобальном пространстве имён быть не должно
Output:1 print(SubString("qwertyerty")-SubString("ttttr"))qweyery
EJudge: DefCounter 'Счётчик с умолчанием'
Input:Написать класс DefCounter, унаследованный от collections.Counter, в котором значения для несуществующих элементов были бы не 0, а задавались в конструкторе именным параметром missing= (по умолчанию — -1). Дополнительно класс должен поддерживать операцию abs(экземпляр), возвращающую сумму положительных элементов счётчика.
Output:DefCounter({'Q': 3, 'W': 3, 'E': 3, 'q': 2, 'w': 2, 'e': 2}) -10 -5 15 10 DefCounter({'Q': 3, 'W': 3, 'E': 3, 'q': 2, 'w': 2, 'e': 2, 'P': -5})EJudge: TestFun 'Тестировщик'
Input:Написать класс Tester, при создании экземпляра которого ему передаётся единственный параметр — некоторая функция fun. Сам экземпляр должен быть callable, и принимать два параметра — последовательность кортежей suite и необязательная (возможно, пустая) последовательность исключений allowed. При вызове должна осуществляться проверка, можно ли функции fun() передавать каждый элемент suite в качестве позиционных параметров. Если исключений не возникло, результат работы — 0, если исключения были, но попадали под классификацию какого-нибудь из allowed, результат — -1, если же были исключения не из allowed — 1.
Output:0 -1 1
EJudge: WhatWhereWho 'Что? Где? Когда?'
Input:Викторина проводится по следующим правилам. Вначале участник опрашивает в заданном порядке некоторых других участников, нет ли у них ответа, причём удовлетворяется первым же вариантом. Если ответа не нашлось, он может придумать свой или признаться, что не знает. Если ответ получен, он может его скорректировать (потому что нашёл недочёт) или ответить как есть. Назовём опросным планом индивидуальный список каждого участника, по которому он опрашивает остальных. Очевидно, не всякая совокупность планов хороша:
- например, участники могут начать спрашивать друг друга по кругу;
- или будет выбран первый из вариантов ответа вместо скорректированного (который идёт дальше в плане).
Впрочем,
если несколько участников, не спрашивая друг у друга, придумали или скорректировали ответ, годится любой из вариантов;
если кто-то из участников мог бы скорректировать ответ, но его спрашивать и не собирались, это тоже нормально: мало ли, отчего ему не доверяют.
Можно ли, не противореча индивидуальным опросным планам, составить полный опросный план игрока — строгую последовательность, в которой опрашиваются участники, если вопрос задан конкретному игроку?
Построчно в виде «Кто_спрашивает: Кого_спрашивает_1, Кого_спрашивает_2, …» вводится список участников и их опросных планов. Если участник считает, что он и так всё знает, план может быть пустой. Последняя строка ввода — пустая. Запятых и двоеточий в именах нет, пробелы могут встречаться только внутри и только поштучно.
Выводится строка вида «Кого_спросили: У_кого_узнать_1, У_кого_узнать_2, …» — полный опросный план для каждого из участников в порядке их ввода
- Если полный опросный план для какого-то игрока невозможен, ни один из планов не выводится, а вместо этого выводится":
«CYCLE», если участники могут начать спрашивать друг друга по кругу
«UNKNOWN», если у кого-то в плане опроса встречается неизвестный участник
«INEFFECTIVE», если кто-то может дать нескорректированный ответ, хотя мог бы узнать скорректированный
Output:Милован Ильясович Михеев: Савватий Эдгардович Моисеев, Михалыч Савватий Эдгардович Моисеев: левый какой-то: Михалыч: Капитон Силин Капитон Силин:
Милован Ильясович Михеев: Савватий Эдгардович Моисеев, Михалыч, Капитон Силин Савватий Эдгардович Моисеев: левый какой-то: Михалыч: Капитон Силин Капитон Силин:
