Слоты, дескрипторы, декораторы
Расширения объектной модели Python
Декораторы
Что, если мы хотим «обмазать» все вызовы некоторой функции отладочной информацией?
Неудобно! Поиск с заменой fun(a,b) на dfun(fun,a,b).
Создадим обёрнутую функцию вместо старой:
Всё равно поиск с заменой, хотя и попроще. Тогда просто перебьём имя fun!
Вот это и есть декоратор, записывается так:
Закомментировали @genf — убрали декоратор!
BTW, Запись вида
означает то, что вы подумали: функцию функция(), обмазанную сначала декоратором декоратор1(), а затем — декоратор2().
Параметрические декораторы
Конструкторы декораторов!
вместо объекта-функции @декоратор мы пишем вызов этого объекта @п_декоратор(параметры), значит, в этом месте произойдёт вызов п_декоратор(параметры), а вот то, что оно вернёт, и послужит декоратором:
вторая часть статьи (+декораторы методов)
Декораторы методов и классов
Методы в классах тоже можно декорировать. И сами классы.
Декоратор метода — это то же самое, что декоратор функции, не забываем только про self в первом параметре.
Класс — это 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)
- Вариант: честно от него унаследоваться и вернуть потомка
- Но тип у такого объекта будет… так себе…
Дескрипторы
подробная статья в документации (рекомендуется)
Механизм getter/setter:
- (исторически) дисциплина доступа к скрытому объекту (реализуется функциями)
- (в Python): вызов метода при обращении к атрибуту класса, поддерживающему протокол дескриптора
Реализация в Python:
Протокол дескриптора — объект с методами .__get__(), .__set__() и .__delete__()
если определён только __get__(), значит, это не данные, а, скажем, метод (т. н. non-data descriptor)
1 class AddSub: 2 from random import random 3 def __get__(self, obj, cls): 4 return int.__add__ if self.random() > .3 else int.__sub__ 5 6 class C: 7 fun = AddSub() 8 9 >>> e = C() 10 >>> [e.fun(50271, 50229) for i in range(12)] 11 [100500, 42, 100500, 100500, 42, 42, 100500, 100500, 100500, 100500, 100500, 42]
для non-data descriptor (если .__set__() не задан), конечно, первое же связывание заведёт на этом месте обычное поле экземпляра
а если есть .__set__(), то так просто перебить поле нельзя — вызовется этот самый .__set__()
Это поле класса
⇒ одно на все экземпляры класса
- конкретный экземпляр передаётся вторым параметром
тип (класс) экземпляра передаётся третьим параметром в .__get__()
Например, если пытаться прочесть поле класса класс.дескриптор, второй параметр будет равен None
Пример. Для пущей наглядности напишем пример сперва без 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 — это поле конкретного объекта, в которое ходит дескриптор
Будучи полем класса, дескриптор отсутствует в .__dict__[] объекта, но имеет перед ни приоритет:
1 >>> c = C("Obj") 2 >>> getattr(c, "data") 3 Get from <class '__main__.C'>:<__main__.C object at 0x7fe01caad520> 4 'Obj' 5 >>> "data" in c.__dict__ 6 False 7 >>> c.__dict__["data"] = "Nobody cares" 8 >>> c.data 9 Get from <class '__main__.C'>:<__main__.C object at 0x7fe01caad520> 10 'Obj' 11 >>> "data" in c.__dict__ 12 True 13 >>> c.__dict__["data"] 14 'Nobody cares' 15 >>> C.data = 100500 16 >>> c.data 17 'Nobody cares' 18
Если подсунуть соответствующее поле прямо в obj.__dict__[], его никто не увидит иначе, чем через прямое чтение obj.__dict__[]
- Если перебить поле классе, всё, конечно, начинает работать с ним и с полем объекта, если оно есть
Вот такой дескриптор может быть в классе только один, потому что он изменяет одно конкретное поле:
Два дескриптора типа Descr() буду менять одно и то же поле ._x.
Мы могли бы хранить значение дескрипторов d = Descr() и e = Descr() непосредственно в obj.__dict__["d"] и obj.__dict__["e"]… но для этого нам нужно как-то узнать, что экземпляры Descr() назывались «d» и «e».
Метод __set_name__() (pep-0487) вызывается при формировании пространства имён класса, и решает ровно эту задачу!
Воспользуемся:
Проблема первоначального заполнения всё ещё не решена: имя поля становится известно строго после создания экземпляра.
Слоты
Про слоты в документации
Недостатки реализации объектной модели в 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
- Т. е. слоты реализованы как стандартные дескрипторы (но это уже слишком глубоко для нас)
Стандартные декораторы
В стандартной библиотеке Python полно декораторов:
functools.total_ordering() (там вообще сплошь декораторы)
В частности, @functools.wraps, который помогает сохранить исходное имя и строку документации функции,
и @functools.partial сами посмотрите для чего ☺
TODO ещё?
Модификаторы методов
- →
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 ☺)
Примеры адского использования неклассической объектной модели в стандартной библиотеке Python
dataclasses (исходник) — слоты
- …
Д/З
- Прочитать про всё, упомянутое выше. Пощёлкать примеры по каждой теме.
EJudge: ParentProp 'Родитель'
Написать класс Sire, у всех экземпляров наследников которого будет поле .parent, содержащее имя непосредственного класса-родителя (более точно — первого из классов в списке наследования). Например, для дочерних классов это поле должно быть равно "Sire".
Sire C
Написать декоратор класса под названием sizer, который будет добавлять в класс поле size. При обращении к этому полю возвращается len() объекта, если объект имеет длину, иначе же abs() объекта, если от него вычисляется модуль, и 0 в противном случае. Если в объекте присвоить этому полю некоторое значение, будет возвращаться это значение до тех пор, пока поле не удалят.
1 @sizer 2 class S(list): 3 pass 4 5 @sizer 6 class N(complex): 7 pass 8 9 @sizer 10 class E(Exception): 11 pass 12 13 for obj in S("QWER"), N(3+4j), E("Exceptions know no lengths!"): 14 print(obj, obj.size) 15 p = S(range(10, 15)) 16 print(p.size) 17 p.size = p.pop() 18 print(p.size) 19 del p.size 20 print(p.size)
['Q', 'W', 'E', 'R'] 4 (3+4j) 5.0 Exceptions know no lengths! 0 5 14 4
EJudge: CorrectFloat 'Фиксированная точность'
Написать класс-параметрический декоратор Fix(n), с помощью которого все вещественные (как позиционные, так и именные) параметры произвольной декорируемой функции, а также её возвращаемое значение, округляются до n-го знака после запятой (1 ⩽ n ⩽ 16). Если какие-то параметры функции оказались не вещественными, или не вещественно возвращаемое значение, эти объекты не меняются.
Требуется использовать @wraps
-13.1916
EJudge: CompactPairs 'Двухбуквенные поля'
Написать класс Pairs, в экземпляре которого присутствуют поля с именами всех латинских букв (и строчных, и прописных). При инициализации экземпляру передаётся единственный параметр 1 ⩽ N ⩽ 52 — значение поля .a. Поле .b должно быть на 1 больше, и так далее до 52, после чего нумерация идёт с 1. При преобразовании объекта в строку должны выводиться через пробел имена полей в следующем порядке: сначала то, которое при создании было равно 1, затем то, которое было равно 2 и т. д. Если значение пола впоследствии изменилось, порядок сохраняется.
Pairs(1) задаёт следующий порядок:
a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O 13 36