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

Расширения объектной модели 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().

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

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

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

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

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

Дескрипторы

Вместо .__dict__ — механизм с getter-ами и setter-ами.

   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

Обратите внимание: дескриптор — поле класса, так что

   1 e = C("Ye")
   2 d.data = 100500
   3 print(d.data)
   4 print(e.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__.

Слоты

   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 >>>
  18 

Стандартные декораторы

Д/З

  1. Прочитать про всё, упомянутое выше. Пощёлкать примеры по каждой теме.

TODO

  1. Написать параметрический декоратор TypeCheck(последовательность_типов, тип_результата), который бросает исключение TypeError при вызове функции со следующим сообщением:

    • "Type of argument Номер is not Тип", если не совпадает тип позиционного параметра функции и соответствующий ему по порядку тип в последовательности_типов

    • "Type of argument Имя is not Тип", если не совпадает тип именного параметра функции и соответствующий ему тип в последовательности_типов. Типы именованных параметров перечислены в конце последовательности_типов в порядке их описания в def …

    • "Type of result is not Тип", если тип возвращённого функцией значения не совпадает с типом_результата

    • "Function функция must have число arguments" — если количество переданных функции параметров (включая переданные по умолчанию) не соответствует длине последовательности_типов

         1 @TypeCheck((int, str, int), int)
         2 def valid(a, b, c=0):
         3     return len(b*(a+1))+c
         4 
         5 @TypeCheck([int, int], int)
         6 def semivalid(a, b):
         7     return a/b if a%2 else a*b
         8 
         9 @TypeCheck((int for i in range(4)), int)
        10 def variable(*args, **kwargs):
        11     return len(args)+len(kwargs)
      
         1 >>> valid(3, "--", 10)
         2 18
         3 >>> valid(3, 7, 10)
         4 
         5 TypeError: Type of argument 2 is not <class 'str'>
         6 >>> valid(3, "--", "*")
         7 
         8 TypeError: Type of argument 3 is not <class 'int'>
         9 >>> valid(3, "--", c=1.23)
        10 
        11 TypeError: Type of argument 'c' is not <class 'int'>
        12 >>> semivalid(2, 2)
        13 4
        14 >>> semivalid(1, 2)
        15 
        16 TypeError: Type of result is not <class 'int'>
        17 >>> variable(1,2,3,4)
        18 4
        19 >>> variable(1,2,a=100, b=500)
        20 4
        21 >>> variable(1,2,a=100)
        22 
        23 TypeError: Function variable must have 4 arguments
        24