Слоты, дескрипторы, декораторы
Расширения объектной модели 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.
- Чаще всего это тот же самый класс, только поправленный немножко (aka monkey patch)
- Вариант: честно от него унаследоваться и вернуть потомка
- Но тип у такого объекта будет… так себе…
В частности, functools.total_ordering()
Дескрипторы
подробная статья в документации (рекомендуется)
Механизм getter/setter
- (исторически) дисциплина доступа к скрытому объекту (реализуется функциями)
- (в Python): вызов метода при обращении к «полю» класса, поддерживающему протокол дескриптора
Протокол дескриптора — объект с методами .__get__(), .__set__() и .__delete__()
если определён только __get__(), значит, это не данные, а, скажем, метод (т. н. non-data descriptor)
для non-data descriptor (если .__set__() не задан), конечно, первое же связывание заведёт на этом месте обычное поле экземпляра
а если есть .__set__(), то уже нельзя —
Это поле класса
⇒ одно на все экземпляры класса
- конкретный экземпляр передаётся вторым параметром
тип (класс) экземпляра передаётся третьим параметром в .__get__()
Например, если пытаться прочесть поле класса класс.дескриптор, второй параметр будет равен None
Имеет преимущество перед механизмом .__dict__[]
Например, если подсунуть соответствующее поле прямо в obj.__dict__[], его никто не увидит
- А вот если перебить его в классе, всё, конечно, начинает работать по-страрому
Пример. Для пущей наглядности напишем пример сперва без repr() в __get__() — и споткнёмся о рекурсию:
1 class Dsc: 2 3 def __get__(self, obj, cls): 4 print(f"Get from {cls}:{repr(obj)}") 5 return obj._value 6 7 def __set__(self, obj, val): 8 print(f"Set in {repr(obj)} to {val}") 9 obj._value = val 10 11 def __delete__(self, obj): 12 print(f"Delete from {repr(obj)}") 13 obj._value = None 14 15 class C: 16 data = Dsc() 17 18 def __init__(self, name): 19 self.data = name 20 21 def __str__(self): 22 return f"<{self.data}>"
- →
1 >>> c = C("Obj") 2 Set in <__main__.C object at 0x7f0ce74909d0> to Obj 3 >>> c._value 4 'Obj' 5 >>> c.data = 100500 6 Set in <__main__.C object at 0x7f0ce74909d0> to 100500 7 >>> c.data 8 Get from <class '__main__.C'>:<__main__.C object at 0x7f0ce74909d0> 9 100500 10 >>> c._value 11 100500 12 >>> del c.data 13 Delete from <__main__.C object at 0x7f0ce74909d0> 14 >>> print(c.data) 15 Get from <class '__main__.C'>:<__main__.C object at 0x7f0ce74909d0> 16 None 17 >>> C.data = "muggle" 18 >>> c.data 19 'muggle' 20 >>> c.data = 42 21 >>> c.data 22 42 23 >>> del c.data 24 >>> c.data 25 'muggle' 26
Обратите внимание на то, что ._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 >>> type(s.field)
2 <class 'int'>
3 >>> type(slo.field)
4 <class 'member_descriptor'>
5 >>> slo.field.__get__()
6 Traceback (most recent call last):
7 File "<stdin>", line 1, in <module>
8 TypeError: expected at least 1 argument, got 0
9
10 expected at least 1 argument, got 0
11 >>> slo.field.__get__(s)
12 2
13
- Т. е. слоты реализованы как стандартные дескрипторы (но это уже слишком глубоко для нас)
Стандартные декораторы
- →
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 — обёртка вокруг дескриптора
Обратите внимание на троекратное def x(… — не надо придумывать ненужные имена (нельзя, actually ☺)
Примеры:
В частности, @functools.wraps, который помогает сохранить исходное имя и строку документации функции,
и @functools.partial сами посмотрите для чего ☺
Д/З
- Прочитать про всё, упомянутое выше. Пощёлкать примеры по каждой теме.
EJudge: ClassOnly 'Только поля класса'
Написать класс Struct, элементы которого будут содержать поля, соответствующие всем возможным четырёхбуквенным последовательностям из букв a, b, c и d (от aaaa до dddd). Значение этих полей совпадают с их именами. Попытка обращения к полям с любыми другими именами (кроме начинающихся на «_») должна приводить к исключению AttributeError. Записывать что-либо в объекты этого класса не предполагается, но предполагается делать много его экземпляров.
Необязательное упражнение: уложить описание класса в одну строку (а с помощью itertools — вполне компактную). Или хотя бы в две.
print(Struct().abba)
abba
EJudge: DefArgs 'Параметры по умолчанию'
Написать параметрический декоратор (функцию или класс) DefArgs(*константы), которым можно декорировать функции с фиксированным числом позиционных параметров. Возвращаемая им функция должна принимать произвольное количество позиционных параметров, не большее чем у исходной функции. Все опущенные параметры должны получать соответствующие их позиции значения из кортежа константы. Если констант меньше, чем параметров декорируемой функции, декоратор инициирует исключение TypeError. Это же исключение инициируется при вызове функции со слишком большим количеством параметров. Дополнительно декоратор должен проверять, что тип параметров при вызове соответствует типу констант, в противном случае также инициировать исключение TypeError.
6 12 56 Nope Nope Nope
EJudge: RecordPlus 'Структура плюс'
Напишите параметрический декоратор Record(строка, **именные_параметры) произвольного класса, использующего __slots__ в качестве объектной модели. Декоратор должен добавлять в возвращаемый класс слоты, имена которых перечислены через пробел в строке, и поля только для чтения, имена и значения которых перечислены в именных_параметрах. Имена не могут начинаться на "_". Слоты возвращаемого класса перечисляются в алфавитном порядке. Имена полей могут перекрывать имена слотов родительского класса.
Дополнительно должна поддерживаться итерация по объекту — она возвращает имена слотов и полей (имя которых не начинается на "_") в алфавитном порядке
Дополнительно должно поддерживаться преобразование в строку в таком формате (поля берутся также в алфавитном порядке): для неопределённых слотов выводится только имя, для определённых — имя=значение, для переменных — имя:значение, разделитель — «|» (см. пример).
1 @Record("b c", d=11, e=12) 2 class C: 3 __slots__ = ["a", "b"] 4 c = 8 5 d = 9 6 7 8 c = C() 9 c.a, c.c = 42, 100500 10 print(c, "//", "".join(c.__slots__)) 11 print(*(getattr(c, attr, "<NOPE>") for attr in c)) 12 for i, attr in enumerate(c): 13 try: 14 setattr(c, attr, i) 15 except AttributeError: 16 pass 17 print(c, "//", *(getattr(c, attr, "<NOPE>") for attr in c))
a=42|b|c=100500|d:11|e:12 // abc 42 <NOPE> 100500 11 12 a=0|b=1|c=2|d:11|e:12 // 0 1 2 11 12
Написать класс 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