Слоты, дескрипторы, декораторы
Расширения объектной модели Python
Декораторы
Что, если мы хотим «обмазать» все вызовы некоторой функции отладочной информацией?
Неудобно! Поиск с заменой fun(a,b) на dfun(fun,a,b).
Создадим обёрнутую функцию вместо старой:
Всё равно поиск с заменой, хотя и попроще. Тогда просто перебьём имя fun!
Вот это и есть декоратор, записывается так:
Закомментировали @genf — убрали декоратор!
BTW, Запись вида
означает то, что вы подумали: функцию функция(), обмазанную сначала декоратором декоратор1(), а затем — декоратор2().
Параметрические декораторы
Конструкторы декораторов!
вместо объекта-функции @декоратор мы пишем вызов этого объекта @п_декоратор(параметры), значит, в этом месте произойдёт вызов п_декоратор(параметры), а вот то, что оно вернёт, и послужит декоратором:
вторая часть статьи (+декораторы методов) примеры
Декораторы методов и классов
Методы в классах тоже можно декорировать. И сами классы.
- Декоратор метода — это то же самое, что декоратор функции
Класс — это callable, так что ему ничто не мешает быть декоратором
Однако нужно, чтобы экземпляр класса тоже был callable (иначе как он будет декорировать), так что надо определить метод __call__()
1 class Timer: 2 from time import time 3 from sys import stderr 4 5 def __init__(self, fun): 6 self.function = fun 7 8 def __call__(self, *args, **kwargs): 9 start_time = self.time() 10 result = self.function(*args, **kwargs) 11 end_time = self.time() 12 print(f"Duration: {end_time-start_time} seconds", file=self.stderr) 13 return result 14 15 16 # adding a decorator to the function 17 @Timer 18 def payload(delay): 19 return sorted(sum(range(i)) for i in range(delay)) 20 21 print(payload(10000)[-1])
- Декоратор класса — проще, чем кажется ☺! Это функция, которой передаётся класс, она его жуёт (например, подсовывает или даже перебивает поля), и возвращает новый, пережёванный класc.
- Вариант: честно от него наследуется и возвращает потомка
В частности, functools.total_ordering()
Дескрипторы
подробрая статья (рекомендуется)
Вместо .__dict__ — механизм с getter-ами и setter-ами.
Если в .__dict__ тоже есть такой ключ, полноценный дескриптор «главнее»
Протокол дескриптора — объект с методами .__get__(), .__set__() и .__delete__()
если без __set__(), значит, это не данные, а, скажем, метов (т. н. non-data descriptor)
Это поле класса
⇒ одно на все экземпляры класса
- конкретный экземпляр передаётся вторым параметром
тип (класс) экземпляра передаётся третьим параметром в .__get__()
Например, если пытаться прочесть поле класса класс.дескриптор, второй параметр будет равен None
Если задан .__set__(), Имеет преимущество перед полем экземпляра (в отличие от обычных полей класса)
- если не задан, т. е. для non-data, то, конечно, первое же связывание заведёт на этом месте обычное поле экземпляра
1 class Dsc: 2 def __get__(self, obj, cls): 3 print(f"Get from {cls}:{obj}") 4 return obj._value 5 6 def __set__(self, obj, val): 7 print(f"Set in {obj} to {val}") 8 obj._value = val 9 10 def __delete__(self, obj): 11 print("Delete from {obj}") 12 obj._value = None 13 14 class C: 15 data = Dsc() 16 17 def __init__(self, name): 18 self.name = name 19 20 def __str__(self): 21 return f"<{self.name}>"
- →
Обратите внимание на то, что ._value — это поле конкретного объекта, в которое ходит дескриптор
Слоты
Про слоты в документации
- Зачем использовать классы/объекты как динамический 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
Стандартные декораторы
- →
1 >>> C.fun(1,2,3) 2 Normal: (1, 2, 3) 3 >>> C.cfun(1,2,3) 4 Class: (<class '__main__.C'>, 1, 2, 3) 5 >>> C.sfun(1,2,3) 6 Static: (1, 2, 3) 7 >>> 8 >>> e = C() 9 >>> e.fun(1,2,3) 10 Normal: (<__main__.C object at 0x7f5d72290130>, 1, 2, 3) 11 >>> e.cfun(1,2,3) 12 Class: (<class '__main__.C'>, 1, 2, 3) 13 >>> e.sfun(1,2,3) 14 Static: (1, 2, 3) 15
@property — обёртка вокруг дескриптора
Важное отличие: property — это поле объекта, а не класса, т. е. именно реализация шаблона getter/setter в чистом виде
Обратите внимание на троекратное def x( — не надо придумывать ненужные имена (нельзя, actually ☺)
dataclasses, functools, contextlib…
В частности, @functools.wraps, который помогает сохранить исходное имя и строку документации функции
Д/З
- Прочитать про всё, упомянутое выше. Пощёлкать примеры по каждой теме.
TODO
EJudge: TypeCheck 'Проверка типов'
Написать параметрический декоратор TypeCheck(последовательность_типов, тип_результата), который бросает исключение TypeError при вызове функции со следующим сообщением:
"Type of argument Номер is not Тип", если не совпадает тип позиционного параметра функции и соответствующий ему по порядку тип в последовательности_типов
"Type of argument 'Имя' is not Тип", если не совпадает тип именного параметра функции и соответствующий ему тип в последовательности_типов.
Типы именованных параметров перечислены в конце последовательности_типов
- Типы именованных параметров проверяются в порядке их появления при вызове функции
"Type of result is not Тип", если тип возвращённого функцией значения не совпадает с типом_результата
"Function функция must have число arguments" — если количество переданных функции параметров (включая переданные по умолчанию) не соответствует длине последовательности_типов
- Сначала проверяются параметры в порядке описания в функции, затем вызывается функция, после чего проеряется результат. Ислкючение возникает при первом несовпадении типа.
18 Type of argument 2 is not <class 'str'>
18 Type of argument 2 is not <class 'str'>
EJudge: FieldCounter 'Статистика с полей'
Написать функцию,Stat() которая умеет выполнять две роли:
С одним параметром Stat(класс) — работает как декоратор класса
- Добавляет в объекты класса сбор статистики по всем полям данных
упомянутым в самом классе (т. е. встречающимся в vars(класс))
имя которых не начинается с "_"
По всем остальным атрибутам (методам, спецметодам и т. п., а так же атрибутам, динамически добавленным в __init__() и других методах) статистика не ведётся
- Добавляет в объекты класса сбор статистики по всем полям данных
С двумя параметрами Stat(объект, "поле") — выводит статистику использования поля поле: два целых числа (количество чтений, количество записей)
объект — экземпляр класса, декорированного с помощью Stat
поле — поле этого класса
- Если статистика по данному полю не ведётся, или поля не существует, обя значения равны 0
Экземпляр класса инициализируется с помощью __init__(), т. е. класс не является потомком встроенного класса, не переопределяет __new__(), __getattribute__() и т. п., изначально не содержит слотов/дескрипторов.
1 @Stat 2 class C: 3 A, B = 3, 4 4 def __init__(self, a=None): 5 if a: 6 self.A = a 7 8 c, d = C(), C(123) 9 print(Stat(c, "A"), Stat(d, "A")) 10 d.A = c.A * 2 + c.B 11 c.B = d.A - 1 - len([d.B, d.B, d.B]) 12 print(Stat(c, "A"), Stat(c, "B")) 13 print(Stat(d, "A"), Stat(d, "B")) 14 print(Stat(c, "Foo"))
(0, 0) (0, 1) (1, 0) (1, 1) (1, 2) (3, 0) (0, 0)