Наследование и исключения

Наследование

Просто:

   1 class New(Old):
   2     # поля и методы, возможно, перекрывающие Old.что-то-там

Видимость:

Вызов конструктора (например, для операция типа «"+"»):

   1 class A:
   2 
   3     def __add__(self, other):
   4         return self.__class__(self.val + other.val)
   5 

Неправильно: return A(self.val + other.val), т. к. подменяет тип. Например:

Использование type()

Производный класс можно задать при помощи type() с тремя параметрами (имя, список родителей, словарь полей):

   1 #!python3
   2 C = type("C", (), {"a": 42, "__str__": lambda self: f"{self.__class__.__name__}"})
   3 D = type("D", (C,), {"b": 100500})
   4 c, d = C(), D()
   5 print(f"{C=}, {D=}")
   6 print(f"{c=}, {d=}")
   7 print(f"{c.a=}, {d.a=}, {d.b=}")

Родительский прокси-объект super()

Вызов методов базового класса:

   1 class A:
   2     def fun(self):
   3         return "A"
   4 
   5 class B(A):
   6     def fun(self):
   7         return super().fun()+"B"

<!> super() как-то сам добирается до пространства имён класса, ему не нужен self(). Это неприятно похоже на магию ☺.

Защита от коллизии имён

   1 >>> class C:
   2 ...     __A=1
   3 ...
   4 >>> dir(C)
   5 ['_C__A', '__class__', '__delattr__', …
   6 

Множественное наследование

Общая задача: унаследовать атрибуты некоторого множества классов.

Проблема ромбовидного наследования (примитивные MRO):

Линеаризация

Линеаризация — это создание линейного списка родительских классов для поиска методов в нём, в этом случае MRO — это последовательный просмотр списка до первого класса, содержащего подходящее имя.

⇒ Попытка создать непротиворечивый MRO чревата обманутыми ожиданиями

MRO C3

Общий принцип: обход дерева в ширину, при котором

Описание:

Алгоритм

Если коротко: MRO C3 линеаризация — это обычный алгоритм слияния очередей, применённый к N+1 списку:

  1. Сам класс + N родительских классов в порядке, взятом из объявления этого класса
  2. до N. N линеаризаций — для каждого родительского класса

Слияние очередей:

  1. Рассматриваем набор (всех линеаризаций + список родительских классов) слева направо
  2. Рассматриваем очередной элемент очередного списка, начиная с нулевого элемента
    • Если он входит только в начала некоторых списков (или не входит никуда),

      • то есть:
        1. не является ничьим предком и

        2. не следует после кого-то оставшихся элементов в объявлениях классов

      • добавляем его в линеаризацию
      • удаляем его из всех списков
      • переходим к п. 1.
    • В противном случае возобновляем п. 2
  3. Если в п.2 хороших кандидатов не нашлось, линеаризация невозможна

Примеры

Нет линеаризации для X, но есть для Y (базовый класс — A, находится внизу):

   1 class A: pass
   2 class B(A): pass
   3 class X(A, B): pass
   4 class Y(B, A): pass

Как меняется линеаризация при изменении порядка объявления:

   1 O = object
   2 class F(O): pass
   3 class E(O): pass
   4 class D(O): pass
   5 class C(D,F): pass
   6 class B(D,E): pass
   7 class A(B,C): pass

Но если написать B(E,D) вместо B(D,E):

   1 O = object
   2 class F(O): pass
   3 class E(O): pass
   4 class D(O): pass
   5 class C(D,F): pass
   6 class B(E,D): pass
   7 class A(B,C): pass

   1 >>> B.mro()
   2 [<class '__main__.B'>, <class '__main__.E'>, <class '__main__.D'>, <class 'object'>]
   3 >>> A.mro()
   4 [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.D'>, <class '__main__.F'>, <class 'object'>]
   5 

super() в множественном наследовании

super():

   1 class A:
   2     def __str__(self):
   3         return f"<{self.val}>"
   4 
   5 class B:
   6     def __init__(self, val):
   7         self.val = val
   8 
   9 class C(A, B):
  10     def __init__(self, val):
  11         super().__init__(f"[{val}]")
  12 
  13 c = C(123)
  14 print(c.val, c)

[123] <[123]>

Полиморфизм

Полиморфизм в случае duck typing всего один, зато тотальный! Любой метод можно применять к объекту любого класса, всё равно пока не проверишь, не поймёшь ☺.

False
True

Про полиморфизм — всё ☺.

<!> (На самом деле — нет, всё это ещё понадобится в случае статической типизации).

Проксирование

Попробуем унаследоваться от str и добавить туда унарный - (который будет переворачивать строку)

Автоматически перезадать спецметоды в классе (а их нужно почти все обернуть в преобразование типа) можно только при использовании классической модели. Не будут работать

Решение: хранить «родительский» объект в виде поля, а все методы нового класса делать обёрткой вокруг методов родительского объекта.

Как эта проблема решена в collections.UserString (см. тут)

Возможно, ту же задачу можно решить с помощью метаклассов и __new__() (будет на следующей лекции)

Исключения

Исключения – это механизм управления вычислительным потоком, который завязан на разнесении по коду проверки свойств данных и обработки результатов этой проверки.

Оператор try:

TODO: рассказ про with (втч для прака нужно)

Управление вычислениями

Исключение — это не «ошибка», а нелинейная передача управления, способ обработки некоторых условий не там, где они были обнаружены.

   1 from math import inf
   2 
   3 def divisor(a, b):
   4     c = a / b
   5     return -c
   6 
   7 def proxy(fun, *args):
   8     try:
   9         return fun(*args)
  10     except ZeroDivisionError:
  11         return inf
  12 
  13 for i in range(-2, 3):
  14     print(proxy(divisor, 100, i))

Наличие в программе конструкций вида

  • except Exception:

  •     pass

помогают избегать сообщений об исключениях и многократно затрудняют обработку ошибок и отладку.

Не делайте так!

Оператор raise

Допустим и вариант raise Exception, и raise Exception(параметры):

Пример: встроимся в протокол итерации

   1 class Expectancy:
   2     from random import random as __random
   3 
   4     def __getitem__(self, idx):
   5         if self.__random() > 6/7:
   6             raise IndexError("Bad karma happens")
   7         return self.__random()

{i} Если есть время, можно модифицировать пример с ZeroDivisionError на обработку числа 13.

Локальность имени в операторе as:

   1 try:
   2     raise Exception("QQ!", "QQ!", "QQ-QRKQ.")
   3 except Exception as E:
   4     print(F:=E)
   5 
   6 print( f"{F=}" if "F" in globals() else "No F")
   7 print( f"{E=}" if "E" in globals() else "No E")

('QQ!', 'QQ!', 'QQ-QRKQ.')
F=Exception('QQ!', 'QQ!', 'QQ-QRKQ.')
No E

Вариант raise from: явная подмена или удаление причины двойного исключения.

Python3.11+: групповые исключения и оператор try: / except*. Используются для случаев, когда надо явно вызвать сразу несколько исключений, которые могут обрабатываться независимо:

   1 def fun():
   2     raise ExceptionGroup("Oops!", [ValueError("Ping"), TypeError("Bang")])
   3 
   4 def catch_value():
   5     try:
   6         fun()
   7     except* ValueError as EGroup:
   8         print("Cath_value:", EGroup.exceptions)
   9 
  10 try:
  11     catch_value()
  12 except* TypeError as EGroup:
  13     print("main:", EGroup.exceptions)

Попробуем вместо except* ValueError написать except* Exception — фильтрации не будет.

Вариант обработки с помощью обычного try: / except:

   1 def fun():
   2     raise ExceptionGroup("Oops!", [ValueError("Ping"), TypeError("Bang")])
   3 
   4 try:
   5     fun()
   6 except ExceptionGroup as EGroup:
   7     print(EGroup.exceptions)

Д/З

  1. Прочитать:
  2. EJudge: SubString 'Строки с вычитанием'

    Реализовать класс SubString, который бы полностью воспроизводил поведение str, но вдобавок бы поддерживал операцию вычитания строк. Вычитание устроено так: «уменьшаемое» просматривается посимвольно, и если соответствующий символ присутствует в «вычитаемом», то он однократно удаляется из обеих строк. Исходные объекты не меняются; то, что осталось от уменьшаемого, объявляется результатом вычитания.

    • К моменту прохождения теста ничего нового, кроме класса SubString в глобальном пространстве имён быть не должно

    Input:

       1 print(SubString("qwertyerty")-SubString("ttttr"))
    
    Output:

    qweyery
  3. EJudge: DefCounter 'Счётчик с умолчанием'

    Написать класс DefCounter, унаследованный от collections.Counter, в котором значения для несуществующих элементов были бы не 0, а задавались в конструкторе именным параметром missing= (по умолчанию — -1). Дополнительно класс должен поддерживать операцию abs(экземпляр), возвращающую сумму положительных элементов счётчика.

    Input:

       1 A = DefCounter("QWEqweQWEqweQWE", missing=-10)
       2 print(A)
       3 A["P"] += 5
       4 print(A["T"], A["P"], abs(A), A.total())
       5 print(A)
    
    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})
  4. EJudge: TestFun 'Тестировщик'

    Написать класс Tester, при создании экземпляра которого ему передаётся единственный параметр — некоторая функция fun. Сам экземпляр должен быть callable, и принимать два параметра — последовательность кортежей suite и необязательная (возможно, пустая) последовательность исключений allowed. При вызове должна осуществляться проверка, можно ли функции fun() передавать каждый элемент suite в качестве позиционных параметров. Если исключений не возникло, результат работы — 0, если исключения были, но попадали под классификацию какого-нибудь из allowed, результат — -1, если же были исключения не из allowed — 1.

    Input:

       1 T = Tester(int)
       2 print(T([(12,), ("12", 16)], []))
       3 print(T([(12,), ("12", 16), ("89", 8)], [ValueError, IndexError]))
       4 print(T([(12,), ("12", 16), ("89", 8), (1, 1, 1)], [ValueError, IndexError]))
    
    Output:

    0
    -1
    1
  5. EJudge: WhatWhereWho 'Что? Где? Когда?'

    Викторина проводится по следующим правилам. Вначале участник опрашивает в заданном порядке некоторых других участников, нет ли у них ответа, причём удовлетворяется первым же вариантом. Если ответа не нашлось, он может придумать свой или признаться, что не знает. Если ответ получен, он может его скорректировать (потому что нашёл недочёт) или ответить как есть. Назовём опросным планом индивидуальный список каждого участника, по которому он опрашивает остальных. Очевидно, не всякая совокупность планов хороша:

    • например, участники могут начать спрашивать друг друга по кругу;
    • или будет выбран первый из вариантов ответа вместо скорректированного (который идёт дальше в плане).

    Впрочем,

    • если несколько участников, не спрашивая друг у друга, придумали или скорректировали ответ, годится любой из вариантов;

    • если кто-то из участников мог бы скорректировать ответ, но его спрашивать и не собирались, это тоже нормально: мало ли, отчего ему не доверяют.

    Можно ли, не противореча индивидуальным опросным планам, составить полный опросный план игрока — строгую последовательность, в которой опрашиваются участники, если вопрос задан конкретному игроку?

    • Построчно в виде «Кто_спрашивает: Кого_спрашивает_1, Кого_спрашивает_2, …» вводится список участников и их опросных планов. Если участник считает, что он и так всё знает, план может быть пустой. Последняя строка ввода — пустая. Запятых и двоеточий в именах нет, пробелы могут встречаться только внутри и только поштучно.

    • Выводится строка вида «Кого_спросили: У_кого_узнать_1,  У_кого_узнать_2, …» — полный опросный план для каждого из участников в порядке их ввода

    • Если полный опросный план для какого-то игрока невозможен, ни один из планов не выводится, а вместо этого выводится":
      • «CYCLE», если участники могут начать спрашивать друг друга по кругу

      • «UNKNOWN», если у кого-то в плане опроса встречается неизвестный участник

      • «INEFFECTIVE», если кто-то может дать нескорректированный ответ, хотя мог бы узнать скорректированный

    Input:

    Милован Ильясович Михеев: Савватий Эдгардович Моисеев, Михалыч
    Савватий Эдгардович Моисеев:
    левый какой-то:
    Михалыч: Капитон Силин
    Капитон Силин:
    Output:

    Милован Ильясович Михеев: Савватий Эдгардович Моисеев, Михалыч, Капитон Силин
    Савватий Эдгардович Моисеев: 
    левый какой-то: 
    Михалыч: Капитон Силин
    Капитон Силин:

LecturesCMC/PythonIntro2025/09_InheritanceExceptions (последним исправлял пользователь FrBrGeorge 2025-11-04 13:47:37)