Слоты, дескрипторы, декораторы
Расширения объектной модели 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()
Дескрипторы
подробрая статья (рекомендуется)
Механизм getter/setter
- (исторически) дисциплина доступа (реализуется функциями)
- (в Python): вызов метода при обращении к «полю» класса, поддерживающему протокол дескриптора
Протокол дескриптора — объект с методами .__get__(), .__set__() и .__delete__()
если определён только __get__(), значит, это не данные, а, скажем, метод (т. н. non-data descriptor)
- non-data descriptor можно перебить в пространстве имён экземпляра, а так — нет (даже есть нет setter-а, но есть deleter)
Это поле класса
⇒ одно на все экземпляры класса
- конкретный экземпляр передаётся вторым параметром
тип (класс) экземпляра передаётся третьим параметром в .__get__()
Например, если пытаться прочесть поле класса класс.дескриптор, второй параметр будет равен None
Имеет преимущество перед полем экземпляра (в отличие от обычных полей класса)
- если не задан, т. е. для non-data, то, конечно, первое же связывание заведёт на этом месте обычное поле экземпляра
1 class Dsc: 2 3 def __get__(self, obj, cls): 4 print(f"Get from {cls}:{obj}") 5 return obj._value 6 7 def __set__(self, obj, val): 8 print(f"Set in {obj} to {val}") 9 obj._value = val 10 11 def __delete__(self, obj): 12 print(f"Delete from {obj}") 13 obj._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}>"
- →
Обратите внимание на то, что ._value — это поле конкретного объекта, в которое ходит дескриптор
Слоты
Про слоты в документации
Недостатки реализации объектной модели в Python с помощью __dict__:
- Зачем использовать классы/объекты как динамический namespace?
Зачем в каждом объекте есть свой __dict__, если имена полей всех объектов обычно совпадают?
Слоты:
- Реализованы как структура дескрипторов классе
__dict__ у классов — фиксированный генерат на основании __slots__
__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 >>> slo.field
18 <member 'field' of 'slo' objects>
19 >>> type(slo.field)
20 <class 'member_descriptor'>
21
Стандартные декораторы
- →
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, который помогает сохранить исходное имя и строку документации функции, и @functools.partial сами посмотрите для чего ☺
Д/З
- Прочитать про всё, упомянутое выше. Пощёлкать примеры по каждой теме.
EJudge: TypeCast 'Приведение типов'
Написать параметрический декоратор cast(тип), который пытается преобразовать результат декорируемой функции к заданному типу. Исключения проверять не надо, но надо пользоваться @wraps.
@cast(int) def fun(a, b): return a * 2 + b print(fun(12, 34) * 2) print(fun("12", "34") * 2) print(fun(12.765, 34.654) * 2)
116 242468 120
Написать декоратор класса под названием sizer, который будет добавлять в класс поле size. При обращении к этому полю возвращается len() объекта, если объёкт имеет длину, иначе же abs() объекта, если от него вычисляется модуль, и 0 в противном случае.
QWER 4 (3+4j) 5.0 Exceptions know no lengths! 0
EJudge: FuncCount 'Счётчик вызовов'
Написать декоратор counter, который заводит внутри объекта-функции метод counter(). Этот метод возвращает, сколько раз эта функция была вызвана. Использовать @wraps. Дополнительное требование: никаких других глобальных объектов (кроме counter и wraps).
0 5 25
Написать класс Lock, который реализует абстракцию «двоичный семафор», а также декоратор класса @Lock.locked, который добавляет поле-семафор .lock в класс. Протокол работы семафора:
obj.lock = "имя" — задаём имя семафора, который собираемся захватывать
- Если при этом какой-то семафор был уже захвачен (в том числе с тем же именем), он освобождается
obj.lock — атомарная операция проверки доступности и одновременного захвата семафора. Совпадает с именем семафора, если его захватить удалось, если нет — равна None.
Если семафор уже захвачен именно этим lock-ом, результат — имя семафора
Если семафор не задан, результат — None
del obj.lock — освобождение семафора, если он захвачен именно этим lock-ом
- В противном случае не происходит ничего
При удалении объекта, содержащего lock (например, в результате уменьшения счётчика ссылок до 0), захваченный семафор необходимо освободить.
1 @Lock.locked 2 class A(str): 3 pass 4 5 6 a, b = A("a"), A("b") 7 a.lock = "S" # Регистрация на семафор S 8 b.lock = "S" # Регистрация на семафор S 9 print(a, a.lock) # Успешный захват семафора S 10 print(a, a.lock) # Семафор S уже захвачен нами 11 print(b, b.lock) # Неуспешный захват семафора S 12 del a.lock # Освобождение семафора S 13 print(b, b.lock) # Успешный захват семафора S 14 b.lock = "T" # Регистрация на семафор T, освобождает предыдущий семафор 15 print(b, b.lock) # Успешный захват семафора T 16 del b # Удаление объекта-носителя освобождает семафор 17 a.lock = "T" # Регистрация на семафор T, освобождает предыдущий семафор 18 print(a, a.lock) # Успешный захват семафора T
a S a S b None b S b T a T