Слоты, дескрипторы, декораторы
Расширения объектной модели 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 ☺)
Д/З
- Прочитать про всё, упомянутое выше. Пощёлкать примеры по каждой теме.
TODO
Написать параметрический декоратор TypeCheck(последовательность_типов, тип_результата), который бросает исключение TypeError при вызове функции со следующим сообщением:
"Type of argument Номер is not Тип", если не совпадает тип позиционного параметра функции и соответствующий ему по порядку тип в последовательности_типов
"Type of argument Имя is not Тип", если не совпадает тип именного параметра функции и соответствующий ему тип в последовательности_типов. Типы именованных параметров перечислены в конце последовательности_типов в порядке их описания в def …
"Type of result is not Тип", если тип возвращённого функцией значения не совпадает с типом_результата
"Function функция must have число arguments" — если количество переданных функции параметров (включая переданные по умолчанию) не соответствует длине последовательности_типов
- →
1 >>> valid(3, "--", 10) 2 18 3 >>> valid(3, 7, 10) 4 … 5 TypeError: Type of argument 2 is not <class 'str'> 6 >>> valid(3, "--", "*") 7 … 8 TypeError: Type of argument 3 is not <class 'int'> 9 >>> valid(3, "--", c=1.23) 10 … 11 TypeError: Type of argument 'c' is not <class 'int'> 12 >>> semivalid(2, 2) 13 4 14 >>> semivalid(1, 2) 15 … 16 TypeError: Type of result is not <class 'int'> 17 >>> variable(1,2,3,4) 18 4 19 >>> variable(1,2,a=100, b=500) 20 4 21 >>> variable(1,2,a=100) 22 … 23 TypeError: Function variable must have 4 arguments 24