Наследование и исключения
Наследование
Просто:
Видимость:
- Поля объекта
- Поля класса
- Поля родительского класса (рекурсивно)
Вызов конструктора (например, для операция типа «"+"»):
Неправильно: 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
TODO Сделать короткое описание и отослать к статье
Общий принцип: обход дерева в ширину, при котором
Узел считается доступным на текущем уровне, если он соответствует двум порядкам сразу
- Если в какой-то момент дальнейший обход невозможен (все оставшиеся узлы согласно минимум одному порядку не находятся на текущем уровне), порождается исключение
Например, обход невозможен, если класс A является базовым для класса B, а в порядке объявления стоит позже (см. пример ниже)
Описание:
В Википедии слишком коротко
статья Gaël Pegliasco тоже недлинная, но зато с исторической справкой
Статья Елены Шамаевой «MROC3 - не магия, а справедливое слияние очередей»
Описание с примерами в документации Python
Алгоритм
- Линеаризация графа наследования классов — это
- Сам класс
Совмещение двух последовательностей:
- линеаризаций всех непосредственных родительских классов,
- списка самих родительских классов
- Совмещение — это упорядочивание по следующему принципу:
- Рассматриваем список (всех линеаризаций + список родительских классов) слева направо
- Рассматриваем нулевой элемент очередного списка.
Если он входит только в начала некоторых списков (или не входит никуда),
- то есть:
не является ничьим предком и
не следует после кого-то оставшихся элементов в объявлениях классов
- добавляем его в линеаризацию
- удаляем его из всех списков
- переходим к п. 1.
- то есть:
- В противном случае переходим к следующему элементу
- Если хороших кандидатов не нашлось, линеаризация невозможна
Примеры
Нет линеаризации для 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 всего один, зато тотальный! Любой метод можно применять к объекту любого класса, всё равно пока не проверишь, не поймёшь ☺.
Проверка наследования issubcalss(потомок, родитель) (для удобства класс является подклассом самого себя)
isinstance(объект, класс) — является ли объект экземпляром класса или его предка:
→
False True
Про полиморфизм — всё ☺.
(На самом деле — нет, всё это ещё понадобится в случае статической типизации).
Проксирование
Попробуем унаследоваться от str и добавить туда унарный - (который будет переворачивать строку)
Проблема: какого типа должна быть -строка?
Проблема поглобальнее: какого типа должны быть результаты всех остальных строковых операций (например, .upper())?
Автоматически перезадать спецметоды в классе (а их нужно почти все обернуть в преобразование типа) можно только при использовании классической модели. Не будут работать
Стандартные классы, написанные на Си (например. str)
Классы, использующие «слоты» (будет на следующей лекции)
- Вообще всякие динамические эксперименты над пространством имён класса
Решение: хранить «родительский» объект в виде поля, а все методы нового класса делать обёрткой вокруг методов родительского объекта.
Как эта проблема решена в collections.UserString (см. тут)
Возможно, ту же задачу можно решить с помощью метаклассов и __new__() (будет на следующей лекции)
Исключения
Исключения – это механизм управления вычислительным потоком, который завязан на разнесении по коду проверки свойств данных и обработки результатов этой проверки.
Оператор try:
Клауза except Исключение
Исключения — объекты Python3 (унаследованы от BaseException)
- Дерево исключений, перехват всех дочерних
Собственные исключения (унаследованы от Exception, а не BaseException — некоторые исключения перехватывать не стоит)
А теперь попереставляем пары строк except … print()
Вариант except Исключение as идентификатор, произвольные параметры исключения
Поле .agrs
Клауза else — если исключений не было
Клауза finally — выполняется даже если исключение не перехвачено
FIXME: рассказ про 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: ExceptionTree 'Дерево исключений'
Написать класс ExceptionTree, экземпляр которого конструирует объекты-исключения, иерархия которых соответствует двоичному дереву:
Exception-1 / \ Exception-2 Exception-3 / \ / \ Exception-4 Exception-5 Exception-6 Exception-7 … и т. д.
Единственный параметр при вызове экземпляра — индекс исключения в этом дереве. Индекс хранится также в самом исключении в виде поля .n.
1 etree = ExceptionTree() 2 excs = [etree(i) for i in (1, 2, 5, 12, 20)] 3 for Ethrow in excs: 4 print(f"Throw {Ethrow.n}", end="") 5 for Ecatch in excs: 6 if Ethrow != Ecatch: 7 try: 8 raise Ethrow 9 except Ecatch: 10 print(f", {Ecatch.n} caught", end="") 11 except Exception: 12 print(f", {Ecatch.n} missed", end="") 13 print()
Throw 1, 2 missed, 5 missed, 12 missed, 20 missed Throw 2, 1 caught, 5 missed, 12 missed, 20 missed Throw 5, 1 caught, 2 caught, 12 missed, 20 missed Throw 12, 1 caught, 2 missed, 5 missed, 20 missed Throw 20, 1 caught, 2 caught, 5 caught, 12 missed
EJudge: UnboldCalc 'Надёжный калькулятор'
Написать программу — калькулятор с переменными и обработкой ошибок. Программа построчно вводит команды калькулятора, и если надо, выводит результат их выполнения или ошибку. Конец ввода — пустая строка. Все буквы — английские.
Строка, начинающаяся на '#' — комментарий, такие строки игнорируются
- Пробелы считаются разделителями
Строка вида Идентификатор = выражение задаёт переменную Идентификатор
идентификатор определяется как .isidentifier()
Если слева от "=" стоит не идентификатор, выводится ошибка "Assignment error"; всё, что справа, игнорируется, присваивания не происходит
Выражение вычисляется по правилам Python с помощью eval() в пространстве имён заданных переменных (без __builtins__)
Если выражение нельзя вычислить, потому что оно синтаксически некорректно, выводится ошибка "Syntax error"
Если выражение нельзя вычислить, потому что в нём встречаются неопределённые переменные, выводится ошибка "Name error"
Если выражение нельзя вычислить по какой-то другой причине, выводится "Runtime error"
Соответствующая ошибка выводится даже в том случае, когда строка, содержащая «=», являлась допустимым выражением Python (например, A<=2 или A==3)
Строка вида выражение выводит значение выражения.
# Ошибок нет 234 10/3 A = 3*(2+(1-7)%5) A+100 + ++ - -- - + - - 0 for = 100500 # Начинаются ошибки 7*B 3=3 A=B=5 A() A/0 for
234 3.3333333333333335 118 0 Name error Assignment error Syntax error Runtime error Runtime error Syntax error
EJudge: NegExtender 'Больше, чем минус'
Написать класс NegExt, расширяющий унарный минус по следующей схеме:
Производный класс должен конструироваться с помощью class потомок(NegExt, родитель):
Если для родителя можно вызвать унарный минус, -потомок() возвращает то же, что и -родитель()
Если для родителя унарный минус не работает, но работает операция секционирования, -потомок() возвращает собственную секцию [1:-1]
В противном случае возвращается сам потомок
Результат нужно во всех трёх случаях явно преобразовывать к типу потомка (предполагается, что такое преобразование возможно)
ytho -123 {1: 2, 3: 4} gE
Написать программу, на вход которой подаётся синтаксически верный код на ЯП Python, состоящий только из объявления классов верхнего уровня, без пустых строк и многострочных констант. В наследовании используются только уже определённые ранее в этом коде классы. На выходе программа должна отчитаться, допустимо ли наследование, которое (возможно) встретилось в коде (с точки зрения MRO C3), и вывести "Yes" или "No".
функции eval()/exec() использовать нельзя.
class A: B = 0 class B(A): pass class C(A, B): A = B = C = 5
No