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

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

Просто:

   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. Прочитать:

LecturesCMC/PythonIntro2025/09_InheritanceExceptions (последним исправлял пользователь FrBrGeorge 2025-11-01 18:20:28)