Дескрипторы и наследование
Хитрые техники
Дескрипторы
Вместо .__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
Обратите внимание: дескриптор — поле класса, так что
даст
Set <De> to 100500 Get from <class '__main__.C'>:<De> 100500 Get from <class '__main__.C'>:<Ye> 100500
В методах дескриптора всегда известно, через какой объект идёт доступ, поэтому при желании можно брать id(объект) и на этом основании делать что-то разное.
Если указать только __get__, получится т. н. non-data descriptor, основное отличие которого — возможность «загородить» его полем из __dict__.
Слоты
Про слоты в документации
- Зачем использовать классы/объекты как динамический namespace?
Зачем в каждом объекте есть свой __dict__, если имена полей всех объектов совпадают?
А теперь попробуем:
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
Метаклассы (одним глазом)
Про метаклассы в документации
- Метакласс — конструктор классов
Никто не запрещает делать конструктор классов так:
Все заметили, что это почти декоратор? Что ему не хватает до того, чтобы быть настоящим декоратором?
В любом случае есть несколько вариантов, когда такой механизм не работает. Есть более правильный способ — метаклассы.
Наследование
Хранить родительский объект в виде поля, а все методы нового класса делать обёрткой вокруг методов родительского объекта.
- Видимость и перегрузка методов
- Преобразование типов и создание новых объектов текущего типа:
type(self)(…)
- Вызов метода родительского класса
super() — прокси-объект, аккумулирующий методы всех родительских классов (см. множественное наследование)
Защита полей от случайной перегрузки («__»)
- Проблема ромбовидного наследования:
Обход в глубину добирается до A.v раньше, чем до C.v
Обход в ширину добирается до 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.
О super() на realpython.com
Д/З
Почитать ещё о классах и наследовании в учебнике
EJudge: UniSize 'Размер объекта'
Написать декоратор класса sizer, который добавляет в него поле size, равное длине объекта, если у объекта есть длина, или модулю целочисленного представления объекта в противном случае (предполагается, что ошибок нет). Предоставить пользователю возможность произвольно менять это поле.
QSXWDC 2.718281828459045 6 2 Wait what?
Исследовательская задача. Результаты моего исследования в комментариях по ссылке (нажать «комментарии»).
EJudge: DivStr 'Делимая строка'
Написать класс DivStr(str), полностью (за исключением правых операций со строками и split()/splitlines()) воспроизводящий работу str. Дополнительно класс должен поддерживать операцию деления «/n», где n — натуральное число, которая должна возвращать n-ю часть от начала строки (если не делится, округлённую в меньшую сторону, если n>len(s) — пустую строку). Задача в том, чтобы любое возвращаемое методами значение типа str превращалось в DivStr. Согласно документации, мы не можем подсунуть методы, начинающиеся на «__», прямо в __dict__, или поймать __getattr__-ом, или даже __getattribute__-ом, а должны задать их явно, например, с помощью def. С другой стороны, руками все методы перебивать не хочется. См. далее.
s = DivStr("Qwertupy") print(len(s)) print(s*3/2) print((s+"ZZZZZZZZZZ")/2) print(s[3:]/2) print(s[0].join("12345678")/2)
8 QwertupyQwer QwertupyZ rt 1Q2Q3Q4
EJudge: LetterAttr 'Буквенное поле'
Написать класс LetterAttr, в котором будут допустимы поля с любым именем; значение каждого поля по умолчанию будет совпадать с именем поля (строка), а при задании нового строкового значения туда будут попадать только буквы, встречающиеся в имени поля.
letter digit teller rteteller