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

TODO Не много ли?

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

Просто:

   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), т. к. подменяет тип. Например:

Родительский прокси-объект 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

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

Описание:

Алгоритм

Примеры

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

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

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

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

Хранить родительский объект в виде поля, а все методы нового класса делать обёрткой вокруг методов родительского объекта.

Исключения

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

Синтаксическая ошибка SyntaxError — не обрабатывается (ещё такие ошибки?)

Оператор try:

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

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

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

Оператор `raise`

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

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

TODO Ещё пример?

Локальность имени в операторе 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: групповые исключения и клауза except*.

Д/З

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

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

    • Пробелы в строках игнорируются
    • Команда, начинающаяся на '#' — комментарий, такие команды игнорируются

    • Команда вида Идентификатор = выражение задаёт переменную Идентификатор

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

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

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

    • Выражение вычисляется по правилам арифметики, и может состоять из

      • целых десятичных чисел

      • уже определённых идентификаторов
      • круглых скобок, внутри которых должно находиться непустое выражение
      • действий +, -, *, /, % и унарных + и -.

        • Деление целочисленное

      • Любое другое выражение приводит к выводу ошибки "Syntax error"

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

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

    Input:

    # Ошибок нет
    234
    10/3
    A = 3*(2+(1-7)%5)
    A+100
    + ++ - -- - + - - 0
    # Начинаются ошибки
    7*B
    3=3
    A=4=5
    A()
    A/0
    Output:

    234
    3
    118
    0
    Name error
    Assignment error
    Syntax error
    Syntax error
    Runtime 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

LecturesCMC/PythonIntro2022/09_InheritanceExceptions (последним исправлял пользователь FrBrGeorge 2022-11-13 11:01:11)