Слоты, дескрипторы, декораторы

Расширения объектной модели Python

Декораторы

Что, если мы хотим «обмазать» все вызовы некоторой функции отладочной информацией?

   1 def fun(a,b):
   2     return a*2+b
   3 
   4 def dfun(f, *args):
   5     print(">", *args)
   6     res = f(*args)
   7     print("<", res)
   8     return res
   9 
  10 
  11 print(fun(2,3))
  12 print(dfun(fun,2,3))

Неудобно! Поиск с заменой fun(a,b) на dfun(fun,a,b).

Создадим обёрнутую функцию вместо старой:

   1 # ...
   2 def genf(f):
   3     def newfun(*args):
   4         print(">", *args)
   5         res = f(*args)
   6         print("<", res)
   7         return res
   8     return newfun
   9 
  10 newf = genf(fun)
  11 print(newf(2,3))

Всё равно поиск с заменой, хотя и попроще. Тогда просто перебьём имя fun!

   1 # ...
   2 fun = genf(fun)
   3 print(fun(2,3))

Вот это и есть декоратор, записывается так:

   1 def genf(f):
   2     def newfun(*args):
   3         print(">", *args)
   4         res = f(*args)
   5         print("<", res)
   6         return res
   7     return newfun
   8 
   9 @genf
  10 def fun(a,b):
  11     return a*2+b
  12 
  13 print(fun(2,3))

Закомментировали @genf — убрали декоратор!

статья на хабре

BTW, Запись вида

   1 @декоратор2
   2 @декоратор1
   3 def функция(…)
   4 

означает то, что вы подумали: функцию функция(), обмазанную сначала декоратором декоратор1(), а затем — декоратор2().

Параметрические декораторы

Конструкторы декораторов!

вторая часть статьи (+декораторы методов) примеры

Декораторы методов и классов

Методы в классах тоже можно декорировать. И сами классы.

Дескрипторы

Механизм getter/setter

Слоты

Недостатки реализации объектной модели в Python с помощью __dict__:

Слоты:

   1 class slo:
   2 
   3     __slots__ = "field", "schmield"
   4     readonly = 100500
   5 
   6     def __init__(self, f, s):
   7         self.field, self.schmield = f, s

А теперь попробуем:

   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. Прочитать про всё, упомянутое выше. Пощёлкать примеры по каждой теме.
  2. EJudge: ClassOnly 'Только поля класса'

    Написать класс Struct, элементы которого будут содержать поля, соответствующие всем возможным четырёхбуквенным последовательностям из букв a, b, c и d (от aaaa до dddd). Значение этих полей совпадают с их именами. Попытка обращения к полям с любыми другими именами (кроме начинающихся на «_») должна приводить к исключению AttributeError. Записывать что-либо в объекты этого класса не предполагается, но предполагается делать много его экземпляров.

    • <!> Необязательное упражнение: уложить описание класса в одну строку (а с помощью itertools — вполне компактную). Или хотя бы в две.

    Input:

    print(Struct().abba)
    Output:

    abba
  3. EJudge: DefArgs 'Параметры по умолчанию'

    Написать параметрический декоратор (функцию или класс) DefArgs(*константы), которым можно декорировать функции с фиксированным числом позиционных параметров. Возвращаемая им функция должна принимать произвольное количество позиционных параметров, не большее чем у исходной функции. Все опущенные параметры должны получать соответствующие их позиции значения из кортежа константы. Если констант меньше, чем параметров декорируемой функции, декоратор инициирует исключение TypeError. Это же исключение инициируется при вызове функции со слишком большим количеством параметров. Дополнительно декоратор должен проверять, что тип параметров при вызове соответствует типу констант, в противном случае также инициировать исключение TypeError.

    Input:

       1 @DefArgs(2, 3, 4)
       2 def mult(a, b):
       3     return a * b
       4 
       5 
       6 for args in (), (4,), (7, 8), (7, 8, 9), ("q", "w"):
       7     try:
       8         print(mult(*args))
       9     except TypeError:
      10         print("Nope")
      11 
      12 try:
      13     @DefArgs(2)
      14     def mult(a, b):
      15         return a * b
      16 except TypeError:
      17     print("Nope")
    
    Output:

    6
    12
    56
    Nope
    Nope
    Nope
  4. EJudge: RecordPlus 'Структура плюс'

    Напишите параметрический декоратор Record(строка, **именные_параметры) произвольного класса, использующего __slots__ в качестве объектной модели. Декоратор должен добавлять в возвращаемый класс слоты, имена которых перечислены через пробел в строке, и поля только для чтения, имена и значения которых перечислены в именных_параметрах. Имена не могут начинаться на "_". Слоты возвращаемого класса перечисляются в алфавитном порядке. Имена полей могут перекрывать имена слотов родительского класса.

    • Дополнительно должна поддерживаться итерация по объекту — она возвращает имена слотов и полей (имя которых не начинается на "_") в алфавитном порядке

    • Дополнительно должно поддерживаться преобразование в строку в таком формате (поля берутся также в алфавитном порядке): для неопределённых слотов выводится только имя, для определённых — имя=значение, для переменных — имя:значение, разделитель — «|» (см. пример).

    Input:

       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))
    
    Output:

    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
  5. EJudge: SemClass 'Семафор'

    Написать класс Lock, который реализует абстракцию «двоичный семафор», а также декоратор класса @Lock.locked, который добавляет поле-семафор .lock в класс. Протокол работы семафора:

    1. obj.lock = "имя" — задаём имя семафора, который собираемся захватывать

      • Если при этом какой-то семафор был уже захвачен (в том числе с тем же именем), он освобождается
    2. obj.lock — атомарная операция проверки доступности и одновременного захвата семафора. Совпадает с именем семафора, если его захватить удалось, если нет — равна None.

      • Если семафор уже захвачен именно этим lock-ом, результат — имя семафора

      • Если семафор не задан, результат — None

    3. del obj.lock — освобождение семафора, если он захвачен именно этим lock-ом

      • В противном случае не происходит ничего
    4. При удалении объекта, содержащего lock (например, в результате уменьшения счётчика ссылок до 0), захваченный семафор необходимо освободить.

    Input:

       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
    
    Output:

    a S
    a S
    b None
    b S
    b T
    a T

LecturesCMC/PythonIntro2023/10_SlotsDescriptorsDecorators (последним исправлял пользователь FrBrGeorge 2023-11-15 21:49:20)