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

Расширения объектной модели 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: FixFloat 'Фиксированная точность'

    Написать функцию-параметрический декоратор fix(n), с помощью которой все вещественные (как позиционные, так и именные) параметры произвольной декорируемой функции, а также её возвращаемое значение, округляются до n-го знака после запятой с использованием функции round(). Если какие-то параметры функции оказались не вещественными, или не вещественно возвращаемое значение, эти объекты не меняются.

    Input:

    @fix(4)
    def aver(*args, sign=1):
        return sum(args)*sign
    
    print(aver(2.45675901, 3.22656321, 3.432654345, 4.075463224, sign=-1))
    Output:

    -13.1916
  3. EJudge: StatCounter 'Статистика вызовов'

    Написать, держитесь крепче, генератор-декоратор statcounter(), который конструирует объекты (назовём один из них stat) со следующим поведением. Первый вызов next(stat) (он же stat.send(None)) возвращает словарь, в котором stat будет хранить информацию вида функция: количество вызовов, где функция — это исходный (не обёрнутый) объект-функция (да, так тоже можно!). Словарь заполняется в порядке вызовов соответствующих декораторов. Все последующие вызовы stat.send(function) оборачивают вызов произвольной функции function увеличением на 1 соответствующего элемента словаря. Глобальными именами пользоваться нельзя.

    Input:

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

    21
    ('f1', 5) ('f2', 2)
  4. EJudge: DataClass 'Хранилище объектов'

    Написать функцию sloter(fields, default), которой передаётся последовательность полей fields, и значение по умолчанию default, а возвращает она класс, в экземпляре которого все эти поля есть, равны указанному значению и способны хранить произвольные объекты. Удаление существующего поля должно сбрасывать его в в исходное значение. Попытки создать другие поля в этом экземпляре должны приводить к исключению AttributeError. При проходе циклом экземпляр возвращает поля в порядке их объявления.

    Input:

       1 s = sloter(("A", "b", "CC"), 100500)()
       2 print(*s)
       3 s.A, s.b, s.CC = 3, 2, 1
       4 del s.b
       5 print(*s)
       6 try:
       7     s.E = 123
       8 except AttributeError:
       9     print("No .E")
    
    Output:

    100500 100500 100500
    3 100500 1
    No .E

LecturesCMC/PythonIntro2022/10_SlotsDescriptorsDecorators (последним исправлял пользователь FrBrGeorge 2022-11-17 03:07:41)