Слоты, дескрипторы, декораторы
Расширения объектной модели 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.
Дескрипторы
Вместо .__dict__ — механизм с getter-ами и setter-ами.
Если в .__dict__ тоже есть такой ключ, полноценный дескриптор «главнее»
Протокол дескриптора — объект с методами .__get__(), .__set__() и .__delete__() (если без __set__(), значит, это не данные, а, скажем, функция)
Это поле класса
⇒ одно на все экземпляры класса
конкретный экземпляр передаётся вторым параметром (None, если вызывается как метод класса), так что при желании можно различить
Имеет преимущество перед полем экземпляра (в отличие от обычных полей класса)
1 class Dsc:
2 value = None
3 def __get__(self, obj, cls):
4 print(f"Get from {cls}:{obj}")
5 return self.value
6
7 def __set__(self, cls, val):
8 print(f"Set {cls} to {val}")
9 self.value = val
10
11 def __delete__(self, cls):
12 print("Delete {cls}")
13 self.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}>"
23
24 d = C("De")
25 print(d.data)
26 d.data = 1
27 print(d.data)
28 del d.data
Обратите внимание: дескриптор — поле класса, так что
даст
Set <De> to 100500 Get from <class '__main__.C'>:<De> 100500 Get from <class '__main__.C'>:<Ye> 100500
В методах дескриптора всегда известно, через какой объект идёт доступ, поэтому при желании можно брать id(объект) и на этом основании делать что-то разное.
Если указать только __get__, получится т. н. non-data descriptor, основное отличие которого — возможность «загородить» его полем из __dict__.
Слоты
Про слоты в документации
- Зачем использовать классы/объекты как динамический 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 — обёртка вокруг дескриптора
Обратите внимание на троекратное def x( — не надо придумывать ненужные имена (нельзя, actually ☺)
Обратите внимание, что в примере _var, а не __var (на самом деле могло быть что угодно)
Вот отсюда точно не успеем
Метаклассы (одним глазом)
Про метаклассы в документации
- Метакласс — конструктор классов
Никто не запрещает делать конструктор классов так:
Все заметили, что это почти декоратор? Что ему не хватает до того, чтобы быть настоящим декоратором?
В любом случае есть несколько вариантов, когда такой механизм не работает. Есть более правильный способ — метаклассы.
Создание класса с помощью type(name, bases, dict)
- это
1 C = type("C", (), {})
type — это просто класс такой ⇒
ну так вот, «T = overtype(…)» == «class T(metaclass=overtype): …»
определённые правила для __init__() и __new__()
Не путать с наследованием!
TODO про .__new_() подробнее
Д/З
TODO