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

TODO Многовато, надо сделать отдельную статью про MRO, а тут оставить только необходимое.

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

Просто:

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

  1. 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})
  2. EJudge: SubString 'Строки с вычитанием'

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

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

    Input:

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

    qweyery
  3. EJudge: SafeEval '«Надёжный» eval'

    Написать функцию safeval(), которая работает почти как eval(), с несколькими отличиями:

    • С помощью этой функции нельзя напрямую модифицировать глобальное пространство имён — ничего не произойдёт (см. пример)
      • модифицировать объекты в нём можно

    • Если в процессе вычисления произошло исключение NameError, возвращается исходная строка без изменений.

    • Все остальные исключения, начиная от Exception, игнорируются. В этом случае функция возвращает сам объект-исключение.

    Input:

       1 print(safeval("1+2"))
       2 print(safeval("a+b"))
       3 print(safeval("bin[12]"))
       4 print(safeval("globals().__delitem__('safeval')"))
       5 print(safeval("safeval.__name__"))
    
    Output:

    3
    a+b
    'builtin_function_or_method' object is not subscriptable
    None
    safeval
  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

LecturesCMC/PythonIntro2023/09_InheritanceExceptions (последним исправлял пользователь FrBrGeorge 2023-11-07 09:26:16)