Differences between revisions 1 and 24 (spanning 23 versions)
Revision 1 as of 2020-11-10 08:59:30
Size: 9257
Editor: FrBrGeorge
Comment:
Revision 24 as of 2020-11-21 09:52:39
Size: 13691
Editor: FrBrGeorge
Comment:
Deletions are marked like this. Additions are marked like this.
Line 1: Line 1:
= Наследование = = Наследование и исключения =
Line 61: Line 61:
...  ...
Line 74: Line 74:
* Монотонность C: [C, …, B, …, A] ⇒ D(...(C)...): [D, …, C, …, B, …, A]
* Соблюдение порядка объявления: `class C(D,E,F): …` ⇒ `[C, D, E, F, …]
* ⇒ Н
екоторые ситуации невозможны
Создание линейного списка родительских классов для поиска полей в нём ('''M'''ethod '''R'''esolution '''O'''rder, MRO).
 
* Монотонность: соблюдение порядка наследования `C`: `[C, …, B, …, A]` → `D(…, C, …)`: `[D, …, C, …, B, …, A]`
 * Соблюдение порядка объявления: `class C(D,E): …` → `[C, …, D, …, E, …]`
 * Два разных порядка
некоторые ситуации невозможны
Line 80: Line 81:
 * [[https://habr.com/post/62203/\Большая статья на Хабре]]  * [[https://habr.com/post/62203/|Большая статья на Хабре]]
Line 85: Line 86:
  1. __Совмещение__ списка   1. ''Совмещение''
Line 87: Line 88:
   1. самих родительских классов    1. списка самих родительских классов
Line 89: Line 90:
  1. Рассматриваем список слева направо (родительские классы в конце)   1. Рассматриваем список (всех линеаризаций + список родительских классов справа) слева направо
Line 91: Line 92:
   * Если он входит только в ''начала'' списков (или не входит), то есть не является ничьим ''предком'' и не следует ни за кем из оставшихся в списках классов    * Если он входит только в ''начала'' списков (или не входит никуда),
    *
то есть:
     1.
не является ничьим ''предком'' и
     1.
не следует ''после'' кого-то оставшихся элементов в объявлениях классов
Line 97: Line 101:
==== Пример =====
Пример ''значимо'' упрощённый, для полного понимания читайте документацию.
==== Пример ====
 {{attachment:MRO.svg}}
Line 117: Line 121:
L[B] = B + merge(DO, EO) L[B] = B + merge(DO, EO, DE)
Line 119: Line 123:
L[B] = B + D + merge(O, EO) L[B] = B + D + merge(O, EO, E)
Line 122: Line 126:
L[B] = B + D + E + merge(O, O) L[B] = B + D + E + merge(O, O, …)
Line 124: Line 128:
L[B] = BDEO L[B] = B + D + E + O
→ B
DEO
Line 128: Line 133:
L[C] = CDFO L[C] CDFO
Line 133: Line 138:
A + merge(BDEO,CDFO) A + merge(BDEO, CDFO, BC)
Line 135: Line 140:
A + B + merge(DEO,CDFO) A + B + merge(DEO, CDFO, C)
Line 137: Line 142:
A + B + C + merge(DEO,DFO) A + B + C + merge(DEO, DFO, …)
Line 139: Line 144:
A + B + C + D + merge(EO,FO) A + B + C + D + merge(EO, FO, …)
Line 141: Line 146:
A + B + C + D + E + merge(O,FO)
F? +
A + B + C + D + E + F + merge(O,O)
A + B + C + D + E + merge(O, FO, …)
O? × F? +
A + B + C + D + E + F + merge(O, O, …)
Line 145: Line 150:
ABCDEFO ABCDEFO
Line 152: Line 157:
 * Но если (`B(E,D)` вместо `B(D,E)`):
 {{{#!python
Но если (`B(E,D)` вместо `B(D,E)`):
 {{attachment:MRO2.svg}}
{{
{#!python
Line 163: Line 169:
 {{{#!highlight pycon {{{#!highlight pycon
Line 169: Line 175:

Соответственно, нет решения для:
 (проверьте!)
Соответственно, нет решения для X, но есть для Y:
 {{attachment:MROx.svg}}
Line 174: Line 181:
class X(A,B): pass
}}}

Потому что A, [B] A → , X [A] [B, A] [A, B] ?

Зато есть для `class X(B, A): …`

Потому что A, [B] A → BA, X [A] [B, A] [B, A]
class X(A, B): pass
class Y(B, A): pass
}}}
 * Y: `Y + [BA, B, A]` = `YBA`
 * X: `X + [AB, B, A]` :( , невозможно выбрать очередной элемент (на самом деле — потому что порядок объявления AB конфликтует с порядком наследования BA, но это не всегда так очевидно)
Line 185: Line 189:
 * в случае множественного наследования аналогов не имеет   * в случае множественного наследования аналогов не имеет
Line 191: Line 195:
    ¦ print(f"New A: {cls}, {args}")
    ¦ return super().__new__(cls, *args)
      print(f"New A: {cls}, {args}")
      return super().__new__(cls, *args)
Line 195: Line 199:
    ¦ return f"A: {self}"       return f"A: {self}"
Line 198: Line 202:
    ¦ return f"<{type(self).__name__}>"       return f"<{type(self).__name__}>"
Line 202: Line 206:
    ¦ print(f"New B: {cls}, {args}")
    ¦ return super().__new__(cls, *args)
      print(f"New B: {cls}, {args}")
      return super().__new__(cls, *args)
Line 206: Line 210:
    ¦ return f"G: {self}"       return f"G: {self}"
Line 209: Line 213:
    ¦ return f"<<{type(self).__name__}>>"       return f"<<{type(self).__name__}>>"
Line 213: Line 217:
    ¦ print(f"New: {cls}, {args}")
    ¦ return super().__new__(cls, *args)
      print(f"New: {cls}, {args}")
      return super().__new__(cls, *args)
Line 234: Line 238:
== Исключения ==
Исключения – это механизм управления вычислительным потоком, который завязан на разнесении по коду проверки свойств данных и обработки результатов этой проверки.

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

Оператор `try`:
 * Клауза `except`
  * Вариант `except Исключение`
   * Исключения — объекты Python3 (унаследованы от `BaseException`)
   * Дерево исключений, перехват всех дочерних
   * Собственные исключения (унаследованы от `Exception`, а не `BaseException` — некоторые исключения перехватывать не стоит)
    {{{#!python
class B(Exception):
    pass

class C(B):
    pass

class D(C):
    pass

for cls in [B, C, D]:
    try:
        raise cls()
    except D:
        print("D")
    except C:
        print("C")
    except B:
        print("B")
}}}
   А теперь по переставляем пары строк `except … print()` :)
  * Вариант `except Исключение as идентификатор`, произвольные параметры исключения
 * Клауза `else:` — если исключений не было
 * Клауза `finally:` — выполняется даже если исключение не перехвачено

==== Оператор `raise` ====
 * вариант `raise Exception` vs. `raise Exception(параметры)` — по идее `Exception` — это класс, а `Exception()` — объект, но на самом деле при выходе исключения всё равно изготавливается объект

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

Пример:
{{{#!highlight python3
class Exc1(Exception): pass
class Exc2(Exception): pass

def funerr(a,b):
    if a<b:
        raise Exc1("A must be greater than B")
    return a//b

def justfun(a,b):
    if a<b:
        raise Exc2("A must be greater than B")
    c = funerr(2*a, 3*b)
    return c

for a,b in (10,3),(5,5),(10,0):
    try:
        c = justfun(a,b)
    except Exc1:
        c = -1
    except Exc2:
        c = -2
    except ZeroDivisionError:
        c = -3
    print(c)
}}}

На следующую лекцию: локальность имени в операторе `as`:
{{{#!highlight python
try:
    raise Exception("QQ!", "QQ!", "QQ-QRKQ.")
except Exception as E:
    print(F:=E)

print( f"{F=}" if "F" in globals() else "No F")
print( f"{E=}" if "E" in globals() else "No E")
}}}

{{{
('QQ!', 'QQ!', 'QQ-QRKQ.')
F=Exception('QQ!', 'QQ!', 'QQ-QRKQ.')
No E
}}}
Line 235: Line 325:
'''TODO'''  1.#0 Прочитать:
  * Про C3 MRO [[https://habr.com/post/62203/|на Хабре]] и [[https://www.python.org/download/releases/2.3/mro/|в документации Python]]
  * Про исключения [[py3tut:errors|в учебнике]] и в справочнике [[py3ref:executionmodel.html#exceptions|про исключения]], [[py3ref:compound_stmts.html#the-try-statement|try]] и [[py3ref:simple_stmts.html#the-raise-statement|raise]]
 1. <<EJCMC(148, MroC3, MRO C3)>>
 1. <<EJCMC(148, BoldCalc, Надёжный калькулятор)>>
 1. <<EJCMC(148, SubString, Строки с вычитанием)>>

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

Полиморфизм в случае duck typing всего один, зато тотальный ☺

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

Просто:

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

Видимость:

  • Поля объекта
  • Поля класса
  • Поля базового класса

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

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

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

   1 class B(A):
   2         pass

Какого типа должно быть B()+B()?

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

  • super() возвращает пространство имён, содержащее поля базового класса

  • при создании super()-объекта не создаётся экземпляр базового класса (например, не вызывается __new__() и т. п.)

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

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

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

  • Если пользователь класса перегрузил поле родительского класса, значит, он так хотел
  • Если он так не хотел, ССЗБ
  • Исключение: разработчик родительского класса не хотел, чтобы поле перегружали

    • Если оно публичное — getter/setter/deleter (потом)
    • Если оно приватное — назвать его «__что-то»

      • Поле __чтото класса какойто в действительности называется _какойто__чтото

      • Если пользователь перегрузил это имя — ССЗБ премиум-класса

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

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

  • Проблема ромбовидного наследования:
    • ../../PythonIntro2019/12_MultipleInheritanceExceptions/1.1.png

      • Обход в глубину добирается до A.v раньше, чем до C.v

    • ../../PythonIntro2019/12_MultipleInheritanceExceptions/2.3.png

      • Обход в ширину добирается до A.v раньше, чем до B.v

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

Создание линейного списка родительских классов для поиска полей в нём (Method Resolution Order, MRO).

  • Монотонность: соблюдение порядка наследования C: [C, …, B, …, A]D(…, C, …): [D, …, C, …, B, …, A]

  • Соблюдение порядка объявления: class C(D,E): …[C, …, D, …, E, …]

  • Два разных порядка ⇒ некоторые ситуации невозможны

MRO C3

Общий принцип

  • Линеаризация графа наследования классов — это
    1. Сам класс
    2. Совмещение

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

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

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

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

Пример

  • MRO.svg

   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
  • Простое наследование (L[X] — линеаризация класса X):
    L[O] = O
    L[D] = D + O
    L[E] = E + O
    L[F] = F + O
  • Множественное наследование
    L[B] = B + merge(DO, EO, DE)
    D? Good
    L[B] = B + D + merge(O, EO, E)
    O? Not good (EO)
    E? Good
    L[B] = B + D + E + merge(O, O, …)
    O? Good
    L[B] = B + D + E + O
    → BDEO
    соответственно,
    L[C] → CDFO
    наконец,
        L[A]:
    A + merge(BDEO, CDFO, BC)
    B? +
    A + B + merge(DEO, CDFO, C)
    D? × C? +
    A + B + C + merge(DEO, DFO, …)
    D? +
    A + B + C + D + merge(EO, FO, …)
    E? +
    A + B + C + D + E + merge(O, FO, …)
    O? × F? +
    A + B + C + D + E + F + merge(O, O, …)
    O? +
    → ABCDEFO
    То есть:
       1 >>> A.mro()
       2 [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class '__main__.E'>, <class '__main__.F'>, <class 'object'>]
       3 
    

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

  • MRO2.svg

   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 
  • (проверьте!)

Соответственно, нет решения для X, но есть для Y:

  • MROx.svg

   1 class A: pass
   2 class B(A): pass
   3 class X(A, B): pass
   4 class Y(B, A): pass
  • Y: Y + [BA, B, A] = YBA

  • X: X + [AB, B, A] :( , невозможно выбрать очередной элемент (на самом деле — потому что порядок объявления AB конфликтует с порядком наследования BA, но это не всегда так очевидно)

super():

  • как всегда — объект-прокси всех методов родительских классов
  • в случае множественного наследования аналогов не имеет
  • это как бы объект несуществующего класса, в котором проделан MRO, но ещё нет ни одного поля нашего класса

   1 class A:
   2     def __new__(cls, *args):
   3         print(f"New A: {cls}, {args}")
   4         return super().__new__(cls, *args)
   5 
   6     def f(self):
   7         return f"A: {self}"
   8 
   9     def __str__(self):
  10         return f"<{type(self).__name__}>"
  11 
  12 class B:
  13     def __new__(cls, *args):
  14         print(f"New B: {cls}, {args}")
  15         return super().__new__(cls, *args)
  16 
  17     def g(self):
  18         return f"G: {self}"
  19 
  20     def __str__(self):
  21         return f"<<{type(self).__name__}>>"
  22 
  23 class AB(A, B):
  24     def __new__(cls, *args):
  25         print(f"New: {cls}, {args}")
  26         return super().__new__(cls, *args)
  27 
  28 ab = AB()
  29 print(ab, ab.f(), ab.g())

New: <class '__main__.AB'>, ()
New A: <class '__main__.AB'>, ()
New B: <class '__main__.AB'>, ()
<AB> A: <AB> G: <AB>

Обратите внимание на вызов обоих __new__() из super().__new__

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

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

  • Напрямую не работает, т. к. __init__() не может перебить спецметоды

  • (т. е. работает, но спецметоды надо руками задавать)
  • Возможно, решается с помощью метаклассов и __new__() {i}

Исключения

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

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

Оператор try:

  • Клауза except

    • Вариант except Исключение

      • Исключения — объекты Python3 (унаследованы от BaseException)

      • Дерево исключений, перехват всех дочерних
      • Собственные исключения (унаследованы от Exception, а не BaseException — некоторые исключения перехватывать не стоит)

        •    1 class B(Exception):
             2     pass
             3 
             4 class C(B):
             5     pass
             6 
             7 class D(C):
             8     pass
             9 
            10 for cls in [B, C, D]:
            11     try:
            12         raise cls()
            13     except D:
            14         print("D")
            15     except C:
            16         print("C")
            17     except B:
            18         print("B")
          

        А теперь по переставляем пары строк except … print() :)

    • Вариант except Исключение as идентификатор, произвольные параметры исключения

  • Клауза else: — если исключений не было

  • Клауза finally: — выполняется даже если исключение не перехвачено

Оператор `raise`

  • вариант raise Exception vs. raise Exception(параметры) — по идее Exception — это класс, а Exception() — объект, но на самом деле при выходе исключения всё равно изготавливается объект

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

Пример:

   1 class Exc1(Exception): pass
   2 class Exc2(Exception): pass
   3 
   4 def funerr(a,b):
   5     if a<b:
   6         raise Exc1("A must be greater than B")
   7     return a//b
   8 
   9 def justfun(a,b):
  10     if a<b:
  11         raise Exc2("A must be greater than B")
  12     c = funerr(2*a, 3*b)
  13     return c
  14 
  15 for a,b in (10,3),(5,5),(10,0):
  16     try:
  17         c = justfun(a,b)
  18     except Exc1:
  19         c = -1
  20     except Exc2:
  21         c = -2
  22     except ZeroDivisionError:
  23         c = -3
  24     print(c)

На следующую лекцию: локальность имени в операторе 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

Д/З

  1. Прочитать:
  2. EJudge: MroC3 'MRO C3'

    Написать программу, на вход которой подаётся ситнаксически верный код на ЯП Python, без пустых строк и многострочных констант; пустая только последняя строка. В этом коде

    • Могут быть определены некоторые классы с помощью оператора class на верхнем уровне программы (т. е. не внутри классов/функций)

    • Имена классов не меняются (т. е. после class C: … никогда не бывает C = …)

    • В наследовании используются только уже определённые в этом коде классы
    • На выходе программа должна отчитаться, допустимо ли наследование, которое (возможно) встретилось в коде (с точки зрения NRO C3), и вывести "Yes" или "No".

    Input:

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

    No
  3. EJudge: BoldCalc 'Надёжный калькулятор'

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

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

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

      • идентификатор определяется стандартно: «произвольная последовательность латинских букв, цифр и символов подчёркивания не начинающаяся с цифры»

      • Если слева от "=" стоит не идентификатор, выводится "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: SubString 'Строки с вычитанием'

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

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

    Input:

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

    qweyery

LecturesCMC/PythonIntro2020/10_Inheritance (last edited 2020-11-21 09:52:39 by FrBrGeorge)