Различия между версиями 11 и 12
Версия 11 от 2019-12-01 23:33:51
Размер: 7680
Редактор: FrBrGeorge
Комментарий:
Версия 12 от 2019-12-01 23:51:29
Размер: 7668
Редактор: FrBrGeorge
Комментарий:
Удаления помечены так. Добавления помечены так.
Строка 170: Строка 170:
'''TODO'''

Дескрипторы и наследование

Хитрые техники

Дескрипторы

Вместо .__dict__ — механизм с getter-ами и setter-ами.

  • Если в .__dict__ тоже есть такой ключ, полноценный дескриптор «главнее»

  • Протокол дескриптора — объект с методами .__get__(), .__set__() и .__delete__() (если без __set__(), значит, это не данные, а, скажем, функция)

  • Это поле класса

    • одно на все экземпляры класса

    • конкретный экземпляр передаётся вторым параметром (None, если вызывается как метод класса), так что при желании можно различить

  • Имеет преимущество перед полем экземпляра (в отличие от обычных полей класса)

   1 class Dsc:
   2     value = None
   3     def __get__(self, obj, cls):
   4         print(f"Get from {cls}:{obj}")
   5         return self.value
   6 
   7     def __set__(self, cls, val):
   8         print(f"Set {cls} to {val}")
   9         self.value = val
  10 
  11     def __delete__(self, cls):
  12         print("Delete {cls}")
  13         self.value = None
  14 
  15 class C:
  16         data = Dsc()
  17 
  18         def __init__(self, name):
  19             self.name = name
  20 
  21         def __str__(self):
  22             return f"<{self.name}>"
  23 
  24 d = C("De")
  25 print(d.data)
  26 d.data = 1
  27 print(d.data)
  28 del d.data

Обратите внимание: дескриптор — поле класса, так что

   1 e = C("Ye")
   2 d.data = 100500
   3 print(d.data)
   4 print(e.data)

даст

Set <De> to 100500
Get from <class '__main__.C'>:<De>
100500
Get from <class '__main__.C'>:<Ye>
100500

В методах дескриптора всегда известно, через какой объект идёт доступ, поэтому при желании можно брать id(объект) и на этом основании делать что-то разное.

Если указать только __get__, получится т. н. non-data descriptor, основное отличие которого — возможность «загородить» его полем из __dict__.

Слоты

   1 class slo:
   2 
   3     __slots__ = ["field", "schmield"]
   4     readonly = 100500
   5 
   6     def __init__(self, f, s):
   7         self.field, self.schmield = f, s

А теперь попробуем:

   1 >>> s=slo(2,3)
   2 >>> s.readonly
   3 100500
   4 >>> s.field
   5 2
   6 >>> s.schmield=4
   7 >>> s.schmield
   8 4
   9 >>> s.foo = 0
  10 Traceback (most recent call last):
  11   File "<stdin>", line 1, in <module>
  12 AttributeError: 'slo' object has no attribute 'foo'
  13 >>> s.readonly = 0
  14 Traceback (most recent call last):
  15   File "<stdin>", line 1, in <module>
  16 AttributeError: 'slo' object attribute 'readonly' is read-only
  17 >>>
  18 

Метаклассы (одним глазом)

  • Про метаклассы в документации

  • На realpython.com

  • Метакласс — конструктор классов
  • <!> Никто не запрещает делать конструктор классов так:

   1 def fc(st):
   2     class _bzz:
   3         def __init__(self):
   4             self.st = st
   5     return _bzz
   6 
   7 C = fc(10500)
   8 c = C()
   9 print(c.st)

Все заметили, что это почти декоратор? Что ему не хватает до того, чтобы быть настоящим декоратором?

В любом случае есть несколько вариантов, когда такой механизм не работает. Есть более правильный способ — метаклассы.

   1 from time import ctime
   2 
   3 class D(type):
   4     def __str__(self):
   5         self.__date = ctime()
   6         return f"<class {self.__class__.__name__} created at {self.__date}>"
   7 
   8 class C(metaclass=D):
   9     pass
  10 
  11 class E:
  12     pass

   1 >>> c, e = C(), E()
   2 >>> print(C, E)
   3 <class D created at Sun Dec  1 18:17:59 2019> <class '__main__.E'>
   4 

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

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

  • Видимость и перегрузка методов
  • Преобразование типов и создание новых объектов текущего типа:
    • type(self)(…)

  • Вызов метода родительского класса
    • super() — прокси-объект, аккумулирующий методы всех родительских классов (см. множественное наследование)

  • Защита полей от случайной перегрузки («__»)

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

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

    • 2.3.png

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

  • Что нужно? Линеаризация:

    • Монотонность C: [C, …, B, …, A] ⇒ D(...(C)...): [D, …, C, …, B, …, A]
    • Соблюдение порядка объявления: class C(D,E,F): … ⇒ `[C, D, E, F, …]

    • ⇒ Некоторые ситуации невозможны

super()/isinstance()/issubclass()

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

Д/З

  1. Почитать ещё о классах и наследовании в учебнике

  2. EJudge: UniSize 'Размер объекта'

    Написать декоратор класса sizer, который добавляет в него поле size, равное длине объекта, если у объекта есть длина, или модулю целочисленного представления объекта в противном случае (предполагается, что ошибок нет). Предоставить пользователю возможность произвольно менять это поле.

    Input:

       1 @sizer
       2 class S(str): pass
       3 
       4 @sizer
       5 class N(float): pass
       6 
       7 s = S("QSXWDC")
       8 n = N(2.718281828459045)
       9 print(s, n)
      10 print(s.size, n.size)
      11 s.size, n.size = "Wait", "what?"
      12 print(s.size, n.size)
    
    Output:

    QSXWDC 2.718281828459045
    6 2
    Wait what?
  3. Исследовательская задача. Результаты моего исследования в комментариях по ссылке (нажать «комментарии»).

    EJudge: DivStr 'Делимая строка'

    Написать класс DivStr(str), полностью (за исключением правых операций со строками и split()/splitlines()) воспроизводящий работу str. Дополнительно класс должен поддерживать операцию деления «/n», где n — натуральное число, которая должна возвращать n-ю часть от начала строки (если не делится, округлённую в меньшую сторону, если n>len(s) — пустую строку). Задача в том, чтобы любое возвращаемое методами значение типа str превращалось в DivStr. Согласно документации, мы не можем подсунуть методы, начинающиеся на «__», прямо в __dict__, или поймать __getattr__-ом, или даже __getattribute__-ом, а должны задать их явно, например, с помощью def. С другой стороны, руками все методы перебивать не хочется. См. далее.

    Input:

    s = DivStr("Qwertupy")
    print(len(s))
    print(s*3/2)
    print((s+"ZZZZZZZZZZ")/2)
    print(s[3:]/2)
    print(s[0].join("12345678")/2) 
    Output:

    8
    QwertupyQwer
    QwertupyZ
    rt
    1Q2Q3Q4
  4. EJudge: LetterAttr 'Буквенное поле'

    Написать класс LetterAttr, в котором будут допустимы поля с любым именем; значение каждого поля по умолчанию будет совпадать с именем поля (строка), а при задании нового строкового значения туда будут попадать только буквы, встречающиеся в имени поля.

    Input:

       1 A = LetterAttr()
       2 print(A.letter)
       3 print(A.digit)
       4 A.letter = "teller"
       5 print(A.letter)
       6 A.letter = "fortune teller"
       7 print(A.letter)
    
    Output:

    letter
    digit
    teller
    rteteller

LecturesCMC/PythonIntro2019/11_InheritanceDescriptors (последним исправлял пользователь FrBrGeorge 2019-12-01 23:51:29)