Слоты, дескрипторы, декораторы
Расширения объектной модели 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() и споткнёмся о рекурсию: - 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: FixFloat 'Фиксированная точность' Input:- Написать функцию-параметрический декоратор fix(n), с помощью которой все вещественные (как позиционные, так и именные) параметры произвольной декорируемой функции, а также её возвращаемое значение, округляются до n-го знака после запятой с использованием функции round(). Если какие-то параметры функции оказались не вещественными, или не вещественно возвращаемое значение, эти объекты не меняются. Output:- @fix(4) def aver(*args, sign=1): return sum(args)*sign print(aver(2.45675901, 3.22656321, 3.432654345, 4.075463224, sign=-1))- -13.1916 
- EJudge: StatCounter 'Статистика вызовов' Input:- Написать, держитесь крепче, генератор-декоратор statcounter(), который конструирует объекты (назовём один из них stat) со следующим поведением. Первый вызов next(stat) (он же stat.send(None)) возвращает словарь, в котором stat будет хранить информацию вида функция: количество вызовов, где функция — это исходный (не обёрнутый) объект-функция (да, так тоже можно!). Словарь заполняется в порядке вызовов соответствующих декораторов. Все последующие вызовы stat.send(function) оборачивают вызов произвольной функции function увеличением на 1 соответствующего элемента словаря. Глобальными именами пользоваться нельзя. Output:- stat = statcounter() stats = next(stat) @stat.send def f1(a): return a+1 @stat.send def f2(a, b): return f1(a)+f1(b) print(f1(f2(2,3)+f2(5,6))) print(*((f.__name__, c) for f, c in stats.items())) - 21 ('f1', 5) ('f2', 2)
- EJudge: DataClass 'Хранилище объектов' Input:- Написать функцию sloter(fields, default), которой передаётся последовательность полей fields, и значение по умолчанию default, а возвращает она класс, в экземпляре которого все эти поля есть, равны указанному значению и способны хранить произвольные объекты. Удаление существующего поля должно сбрасывать его в в исходное значение. Попытки создать другие поля в этом экземпляре должны приводить к исключению AttributeError. При проходе циклом экземпляр возвращает поля в порядке их объявления. Output:- 100500 100500 100500 3 100500 1 No .E 
