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

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

Декораторы

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

   1 def fun(a,b):G
   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().

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

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

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

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

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

Декоратор метода — это то же самое, что декоратор функции, не забываем только про self в первом параметре.

Класс — это callable, так что ему ничто не мешает быть декоратором

Декоратор класса — проще, чем кажется ☺! Это функция, которой передаётся класс, она его жуёт (например, подсовывает или даже перебивает поля), и возвращает новый, пережёванный класc.

Дескрипторы

Механизм getter/setter:

Реализация в Python:

Будучи полем класса, дескриптор отсутствует в .__dict__[] объекта, но имеет перед ни приоритет:

Вот такой дескриптор может быть в классе только один, потому что он изменяет одно конкретное поле:

(!) Мы могли бы хранить значение дескрипторов d = Descr() и e = Descr() непосредственно в obj.__dict__["d"] и obj.__dict__["e"]… но для этого нам нужно как-то узнать, что экземпляры Descr() назывались «d» и «e».

Метод __set_name__() (pep-0487) вызывается при формировании пространства имён класса, и решает ровно эту задачу!

   1 class Descr:
   2     def __get__(self, obj, cls):
   3         return obj.__dict__[self.key]
   4     def __set__(self, obj, val):
   5         obj.__dict__[self.key] = abs(val)
   6     def __set_name__(self, owner, name):
   7         self.key = name
   8 
   9 class C:
  10     d = Descr()
  11     e = Descr()

Воспользуемся:

   1 >>> a = C()
   2 >>> dir(a)
   3 ['__class__', …, '__weakref__', 'd', 'e']
   4 >>> a.__dict__
   5 {}
   6 >>> a.d, a.e = 1, 42
   7 >>> a.__dict__
   8 {'d': 1, 'e': 42}
   9 >>> a.d, a.e = -2, -100500
  10 >>> a.__dict__
  11 {'d': 2, 'e': 100500}
  12 >>> a.e
  13 100500
  14 

Проблема первоначального заполнения всё ещё не решена: имя поля становится известно строго после создания экземпляра.

Слоты

Гвидо про дескрипторы и слоты

Недостатки реализации объектной модели в 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 

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

В стандартной библиотеке Python полно декораторов:

Модификаторы методов

Примеры адского использования неклассической объектной модели в стандартной библиотеке Python

Д/З

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

    Написать класс Sire, у всех экземпляров наследников которого будет поле .parent, содержащее имя непосредственного класса-родителя (более точно — первого из классов в списке наследования). Например, для дочерних классов это поле должно быть равно "Sire".

    Input:

       1 class C(Sire):
       2         pass
       3 
       4 class B(C):
       5         pass
       6 
       7 print(C().parent, B().parent)
    
    Output:

    Sire C
  3. EJudge: UniSize 'Унисайз'

    Написать декоратор класса под названием sizer, который будет добавлять в класс поле size. При обращении к этому полю возвращается len() объекта, если объект имеет длину, иначе же abs() объекта, если от него вычисляется модуль, и 0 в противном случае. Если в объекте присвоить этому полю некоторое значение, будет возвращаться это значение до тех пор, пока поле не удалят.

    Input:

       1 @sizer
       2 class S(list):
       3     pass
       4 
       5 @sizer
       6 class N(complex):
       7     pass
       8 
       9 @sizer
      10 class E(Exception):
      11     pass
      12 
      13 for obj in S("QWER"), N(3+4j), E("Exceptions know no lengths!"):
      14     print(obj, obj.size)
      15 p = S(range(10, 15))
      16 print(p.size)
      17 p.size = p.pop()
      18 print(p.size)
      19 del p.size
      20 print(p.size)
    
    Output:

    ['Q', 'W', 'E', 'R'] 4
    (3+4j) 5.0
    Exceptions know no lengths! 0
    5
    14
    4
  4. EJudge: CorrectFloat 'Фиксированная точность'

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

    • <!> Требуется использовать @wraps

    Input:

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

    -13.1916
  5. EJudge: CompactPairs 'Двухбуквенные поля'

    Написать класс Pairs, в экземпляре которого присутствуют поля с именами всех латинских букв (и строчных, и прописных). При инициализации экземпляру передаётся единственный параметр 1 ⩽ N ⩽ 52 — значение поля .a. Поле .b должно быть на 1 больше, и так далее до 52, после чего нумерация идёт с 1. При преобразовании объекта в строку должны выводиться через пробел имена полей в следующем порядке: сначала то, которое при создании было равно 1, затем то, которое было равно 2 и т. д. Если значение пола впоследствии изменилось, порядок сохраняется.

    • Pairs(1) задаёт следующий порядок:

      • a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

    Input:

       1 p = Pairs(12)
       2 print(p, p.b, p.y)
    
    Output:

    P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O 13 36

LecturesCMC/PythonIntro2024/10_SlotsDescriptorsDecorators (последним исправлял пользователь FrBrGeorge 2024-11-20 19:44:41)