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

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

Просто:

   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

TODO Сделать короткое описание и отослать к статье

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

Описание:

Алгоритм

Примеры

Нет линеаризации для 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:

FIXME: рассказ про 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(EGroup.exceptions)
   9 
  10 try:
  11     catch_value()
  12 except* TypeError as EGroup:
  13     print(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: ExceptionTree 'Дерево исключений'

    Написать класс ExceptionTree, экземпляр которого конструирует объекты-исключения, иерархия которых соответствует двоичному дереву:

    •                  Exception-1
                      /           \    
            Exception-2            Exception-3
            /         \           /          \
      Exception-4 Exception-5 Exception-6 Exception-7
      … и т. д.

    Единственный параметр при вызове экземпляра — индекс исключения в этом дереве. Индекс хранится также в самом исключении в виде поля .n.

    Input:

       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()
    
    Output:

    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
  3. EJudge: UnboldCalc 'Надёжный калькулятор'

    Написать программу — калькулятор с переменными и обработкой ошибок. Программа построчно вводит команды калькулятора, и если надо, выводит результат их выполнения или ошибку. Конец ввода — пустая строка. Все буквы — английские.

    • Строка, начинающаяся на '#' — комментарий, такие строки игнорируются

    • Пробелы считаются разделителями
    • Строка вида Идентификатор = выражение задаёт переменную Идентификатор

      • идентификатор определяется как .isidentifier()

    • Если слева от "=" стоит не идентификатор, выводится ошибка "Assignment error"; всё, что справа, игнорируется, присваивания не происходит

    • Выражение вычисляется по правилам Python с помощью eval() в пространстве имён заданных переменных (без __builtins__)

    • Если выражение нельзя вычислить, потому что оно синтаксически некорректно, выводится ошибка "Syntax error"

    • Если выражение нельзя вычислить, потому что в нём встречаются неопределённые переменные, выводится ошибка "Name error"

    • Если выражение нельзя вычислить по какой-то другой причине, выводится "Runtime error"

    • Соответствующая ошибка выводится даже в том случае, когда строка, содержащая «=», являлась допустимым выражением Python (например, A<=2 или A==3)

    • Строка вида выражение выводит значение выражения.

    Input:

    # Ошибок нет
    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
    Output:

    234
    3.3333333333333335
    118
    0
    Name error
    Assignment error
    Syntax error
    Runtime error
    Runtime error
    Syntax error
  4. EJudge: NegExtender 'Больше, чем минус'

    Написать класс NegExt, расширяющий унарный минус по следующей схеме:

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

      1. Если для родителя можно вызвать унарный минус, -потомок() возвращает то же, что и -родитель()

      2. Если для родителя унарный минус не работает, но работает операция секционирования, -потомок() возвращает собственную секцию [1:-1]

      3. В противном случае возвращается сам потомок

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

    Input:

       1 class nstr(NegExt, str):
       2     pass
       3 class nnum(NegExt, int):
       4     pass
       5 class ndict(NegExt, dict):
       6     pass
       7 print(-nstr("Python"), -nnum(123), -ndict({1: 2, 3: 4}), --nstr("NegExt"))
    
    Output:

    ytho -123 {1: 2, 3: 4} gE
  5. EJudge: MroC3 'MRO C3'

    Написать программу, на вход которой подаётся синтаксически верный код на ЯП Python, состоящий только из объявления классов верхнего уровня, без пустых строк и многострочных констант. В наследовании используются только уже определённые ранее в этом коде классы. На выходе программа должна отчитаться, допустимо ли наследование, которое (возможно) встретилось в коде (с точки зрения MRO C3), и вывести "Yes" или "No".

    • <!> функции eval()/exec() использовать нельзя.

    Input:

    class A:
        B = 0
    class B(A): pass
    class C(A, B):
        A = B = C = 5
    Output:

    No

LecturesCMC/PythonIntro2024/09_InheritanceExceptions (последним исправлял пользователь hbd 2024-11-12 12:06:06)