Наследование и исключения
Наследование
Просто:
Видимость:
- Поля объекта
- Поля класса
- Поля базового класса
Вызов конструктора (например, для операция типа «"+"»):
Использование A(self.val + other.val) неправильно, т. к. подменяет тип.
Какого типа должно быть B()+B()?
Родительский прокси-объект super()
super() возвращает пространство имён, содержащее поля базового класса
при создании super()-объекта не создаётся экземпляр базового класса (например, не вызывается __new__() и т. п.)
Вызов методов базового класса:
Защита от коллизии имён
- Если пользователь класса перегрузил поле родительского класса, значит, он так хотел
- Если он так не хотел, ССЗБ
Исключение: разработчик родительского класса не хотел, чтобы поле перегружали
- Если оно публичное — getter/setter/deleter (потом)
Если оно приватное — назвать его «__что-то»
Поле __чтото класса какойто в действительности называется _какойто__чтото
Если пользователь перегрузил это имя — ССЗБ премиум-класса
Множественное наследование
- Проблема ромбовидного наследования:
Обход в глубину добирается до A.v раньше, чем до C.v
Обход в ширину добирается до A.v раньше, чем до B.v
Линеаризация
Создание линейного списка родительских классов для поиска полей в нём (Method Resolution Order, MRO).
Монотонность: соблюдение порядка наследования
Если для класса class B(…, A, …): порядок поиска полей такой:
B: [B, …, A, …]
⇒ то для класса C(…, B, …): порядок должен быть таким:
[C, …, B, …, A, …]
Соблюдение порядка объявления:
class C(D, E):
→ [C, …, D, …, E, …]
- Два разных порядка ⇒ некоторые ситуации невозможны
MRO C3
Общий принцип: обход дерева в ширину, при котором
Узел считается доступным на текущем уровне, если он соответствует двум порядкам сразу
- Если в какой-то момент дальнейший обход невозможен (все оставшиеся узлы согласно минимум одному порядку не находятся на текущем уровне), порождается исключение
Например, если класс A является базовым для класса B, а в порядке объявления стоит раньше (см. пример ниже)
Описание:
В Википедии слишком коротко
статья Gaël Pegliasco тоже недлинная, но зато с исторической справкой
Доклад Елены Шамаевой «MROC3 — не магия, а справедливое слияние очередей»
Описание с примерами в документации Python
Алгоритм
- Линеаризация графа наследования классов — это
- Сам класс
Совмещение
- линеаризаций всех непосредственных родительских классов,
- списка самих родительских классов
- Совмещение — это упорядочивание по следующему принципу:
- Рассматриваем список (всех линеаризаций + список родительских классов справа) слева направо
- Рассматриваем нулевой элемент очередного списка.
Если он входит только в начала списков (или не входит никуда),
- то есть:
не является ничьим предком и
не следует после кого-то оставшихся элементов в объявлениях классов
- добавляем его в линеаризацию
- удаляем его из всех списков
- переходим к п. 1.
- то есть:
- В противном случае переходим к следующему списку (перед этим классом в линеаризации должны быть другие)
- Если хороших кандидатов не нашлось, линеаризация невозможна
Пример
- Простое наследование (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)):
- то
- (проверьте!)
Соответственно, нет решения для X, но есть для Y:
Y: Y + [BA, B, A] = YBA
X: X + [AB, B, A] , невозможно выбрать очередной элемент (на самом деле — потому что порядок объявления AB конфликтует с порядком наследования BA, но это не всегда так очевидно)
super():
- как всегда — объект-прокси всех методов родительских классов
- в случае множественного наследования аналогов не имеет
- это как бы объект несуществующего класса, в котором проделан MRO, но ещё нет ни одного поля нашего класса
1 class A:
2 def __new__(cls, *args):
3 print(f"New A: {cls}, {args}")
4 return super().__new__(cls, *args)
5
6 def f(self):
7 return f"A: {self}"
8
9 def __str__(self):
10 return f"<{type(self).__name__}>"
11
12 class B:
13 def __new__(cls, *args):
14 print(f"New B: {cls}, {args}")
15 return super().__new__(cls, *args)
16
17 def g(self):
18 return f"G: {self}"
19
20 def __str__(self):
21 return f"<<{type(self).__name__}>>"
22
23 class AB(A, B):
24 def __new__(cls, *args):
25 print(f"New: {cls}, {args}")
26 return super().__new__(cls, *args)
27
28 ab = AB()
29 print(ab, ab.f(), ab.g())
→
New: <class '__main__.AB'>, () New A: <class '__main__.AB'>, () New B: <class '__main__.AB'>, () <AB> A: <AB> G: <AB>
Обратите внимание на вызов обоих __new__() из super().__new__
Полиморфизм
Полиморфизм в случае duck typing всего один, зато тотальный!
TODO .issubcalss() / .isinstance()
Про полиморфизм — всё ☺.
(На самом деле — нет, всё это ещё понадобится в случае статической типизации).
Проксирование
Хранить родительский объект в виде поля, а все методы нового класса делать обёрткой вокруг методов родительского объекта.
Напрямую не работает, т. к. __init__() не может перебить спецметоды
- (т. е. работает, но спецметоды надо руками задавать)
Возможно, решается с помощью метаклассов и __new__()
TODO Попробуем унаследоваться от str и добавить туда унарный - (который будет переворачивать строку)
В чём проблема?
Как эта проблема решена в collections.UserString (см. тут)
Исключения
Исключения – это механизм управления вычислительным потоком, который завязан на разнесении по коду проверки свойств данных и обработки результатов этой проверки.
Синтаксическая ошибка SyntaxError — не обрабатывается (ещё такие ошибки?)
Оператор try:
Клауза except
Вариант except Исключение
Исключения — объекты Python3 (унаследованы от BaseException)
- Дерево исключений, перехват всех дочерних
Собственные исключения (унаследованы от Exception, а не BaseException — некоторые исключения перехватывать не стоит)
А теперь по переставляем пары строк except … print()
Вариант except Исключение as идентификатор, произвольные параметры исключения
Клауза else: — если исключений не было
Клауза finally: — выполняется даже если исключение не перехвачено
Управление вычислениями
Исключение — это не «ошибка», а нелинейная передача управления
TODO
Оператор `raise`
вариант raise Exception vs. raise Exception(параметры) — по идее Exception — это класс, а Exception() — объект, но на самом деле при выходе исключения всё равно изготавливается объект
Исключения — не «ошибки», а способ обработки некоторых условий не там, где они были обнаружены.
Пример:
1 class Exc1(Exception): pass
2 class Exc2(Exception): pass
3
4 def funerr(a,b):
5 if a<b:
6 raise Exc1("A must be greater than B")
7 return a//b
8
9 def justfun(a,b):
10 if a<b:
11 raise Exc2("A must be greater than B")
12 c = funerr(2*a, 3*b)
13 return c
14
15 for a,b in (10,3),(5,5),(10,0):
16 try:
17 c = justfun(a,b)
18 except Exc1:
19 c = -1
20 except Exc2:
21 c = -2
22 except ZeroDivisionError:
23 c = -3
24 print(c)
Локальность имени в операторе as:
('QQ!', 'QQ!', 'QQ-QRKQ.') F=Exception('QQ!', 'QQ!', 'QQ-QRKQ.') No E
Вариант raise from явная подмена или удаление причины двойного исключения. См. документацию
Д/З
- Прочитать:
Про C3 MRO на Хабре и в документации Python
Про исключения в учебнике и в справочнике про исключения, try и raise
TODO
EJudge: DivStr 'Строка с делением'
Написать класс DivStr, унаследованный от str, который поддерживал бы операцию деления «//» и остатка от деления «%». Деление на N должно возвращать список из N подстрок одинаковой наибольшей длины, на которые можно разделить исходную строку, а остаток — оставшуюся концевую подстроку меньшей длины (возможно, пустую).
Дополнительное требование: все пользовательские (не начинающиеся на "__") методы str, которые возвращают строку, должны возвращать экземпляр DivStr. Это же требование распространяется на методы __getitem__, __add__, __mul__ и __rmul__. См. советы и комментарии в полном условии задачи.
XcDf QWEa sdER Tdfg RTY ERT dfg RTY y cD fQ WE
EJudge: PepelaC3 'Пепелац-Ц-III'
Пепелац собирается из отдельных деталей по инструкции. В инструкцию входит перечень подсистем, из которых состоит пепелац (не менее двух), собственных деталей пепелаца и списка всех деталей, которые требуются для сборки (он непуст). Это последняя строка инструкции. В начале инструкции идут описания самих подсистем в формате «имя_системы» «перечень подсистем» «список собственных деталей». Проверить корректность инструкции — соответствует ли она правилам. Если соответствует — вывести «Correct», в противном случае — «Incorrect».
Правила:
- Имя подсистемы — одна заглавная латинская буква (проверять не надо)
- Имя детали — одна строчная латинская буква (проверять не надо)
- «Список» деталей или подсистем — это строка, возможно, пустая
- В разных подсистемах могут встречаться детали с одинаковыми именами
В каждом списке все буквы разные
Поиск детали осуществляется по алгоритму «MRO C3»: в порядке появления подсистем в списке, но также и в том порядке, в котором они требуют детали друг из друга. Если совмещение порядков невозможно, инструкция некорректна
Например, если подсистеме B требуются детали из подсистемы A, но в како-то списке A идет раньше B — инструкция некорректна
Деталь из списка необходимых должна присутствовать среди собственных или в перечисленных подсистемах
A abc B cde C A f D AB e DC e abcdef
Correct
EJudge: TestFun 'Тестировщик'
Написать класс Tester, при создании экземпляра которого ему передаётся единственный параметр — некоторая функция fun. Сам экземпляр должен быть callable, и принимать два параметра — последовательность кортежей suite и необязательная (возможно, пустая) последовательность исключений allowed. При вызове должна осуществляться проверка, можно ли функции fun() передавать каждый элемент suite в качестве позиционных параметров. Если исключений не возникло, результат работы — 0, если исключения попадали под классификацию одного из allowed, результат — -1, если же были исключения не из allowed — 1.
0 -1 1