Differences between revisions 8 and 9
Revision 8 as of 2020-11-18 22:25:30
Size: 16897
Editor: FrBrGeorge
Comment:
Revision 9 as of 2020-11-20 07:53:49
Size: 17217
Editor: FrBrGeorge
Comment:
Deletions are marked like this. Additions are marked like this.
Line 353: Line 353:
  * Сначала проверяются параметры в порядке описания в функции, затем вызывается функция, после чего проеряется результат. Ислкючение возникает при первом несовпадении типа.

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

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

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

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

  • вместо объекта-функции @декоратор мы пишем вызов этого объекта @п_декоратор(параметры), значит, в этом месте произойдёт вызов п_декоратор(параметры), а вот то, что оно вернёт, и послужит декоратором:

       1 def multicall(times):
       2     def decorator(fun):
       3         def newfun(*args):
       4             return [fun(*args) for i in range(times)]
       5         return newfun
       6     return decorator
       7 
       8 @multicall(5)
       9 def simplefun(N):
      10     return N*2+1
      11 
      12 print(*simplefun(4))
    

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

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

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

  • Декоратор метода — это то же самое, что декоратор функции
  • Класс — это 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__(), значит, это не данные, а, скажем, метов (т. н. non-data descriptor)

  • Это поле класса

    • одно на все экземпляры класса

    • конкретный экземпляр передаётся вторым параметром
    • тип (класс) экземпляра передаётся третьим параметром в .__get__()

      • Например, если пытаться прочесть поле класса класс.дескриптор, второй параметр будет равен None

  • Если задан .__set__(), Имеет преимущество перед полем экземпляра (в отличие от обычных полей класса)

    • если не задан, т. е. для non-data, то, конечно, первое же связывание заведёт на этом месте обычное поле экземпляра
       1 class Dsc:
       2     def __get__(self, obj, cls):
       3         print(f"Get from {cls}:{obj}")
       4         return obj._value
       5 
       6     def __set__(self, obj, val):
       7         print(f"Set in {obj} to {val}")
       8         obj._value = val
       9 
      10     def __delete__(self, obj):
      11         print("Delete from {obj}")
      12         obj._value = None
      13 
      14 class C:
      15         data = Dsc()
      16 
      17         def __init__(self, name):
      18             self.name = name
      19 
      20         def __str__(self):
      21             return f"<{self.name}>"
    
       1 >>> c = C("Obj")
       2 >>> c.data = 100500
       3 Set in <Obj> to 100500
       4 >>> c.data
       5 Get from <class '__main__.C'>:<Obj>
       6 100500
       7 >>> del c.data
       8 Delete from {obj}
       9 >>> print(c.data)
      10 Get from <class '__main__.C'>:<Obj>
      11 None
      12 
    
    • Обратите внимание на то, что ._value — это поле конкретного объекта, в которое ходит дескриптор

Слоты

   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 

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

  • @classmethod и @staticmethod:

       1 class C:
       2     def fun(*args):
       3         print("Normal:", args)
       4         
       5     @classmethod
       6     def cfun(*args):
       7         print("Class:", args)
       8 
       9     @staticmethod
      10     def sfun(*args):
      11         print("Static:", args)
    
       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 — обёртка вокруг дескриптора

    • (!) Важное отличие: property — это поле объекта, а не класса, т. е. именно реализация шаблона getter/setter в чистом виде

       1 class C:
       2     def __init__(self):
       3         self._var = None
       4 
       5     @property
       6     def x(self):
       7         """I'm the 'x' property."""
       8         return self._var
       9 
      10     @x.setter
      11     def x(self, value):
      12         self._var = value
      13 
      14     @x.deleter
      15     def x(self):
      16         del self._var
    
    • Обратите внимание на троекратное def x( — не надо придумывать ненужные имена (нельзя, actually ☺)

  • dataclasses, functools, contextlib

    • В частности, @functools.wraps, который помогает сохранить исходное имя и строку документации функции

Д/З

  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 
      
  2. Написать функцию,Stat() которая умеет выполнять две роли:

    • С одним параметром Stat(класс) — работает как декоратор класса

      • Добавляет в объекты класса сбор статистики по всем полям данных
        • упомянутым в самом классе (т. е. встречающимся в vars(класс))

        • имя которых не начинается с "_"

        • по всем остальным атрибутам (методам, спецметодам и т. п., а так же атрибутам, динамически добавленным в __init__()) статистика не ведётся

    • С двумя параметрами Stat(объект, "поле") — выводит статистику использования поля поле: два целых числа (количество чтений, количество записей)

      • объект — экземпляр класса, декорированного с помощью Stat

      • поле — поле этого класс

    • Класс не является потомком встроенного класса, не переопределяет __new__(), __getattribute__() и т. п.

         1 @Stat
         2 class C:
         3     A, B = 3, 4
         4     def __init__(self, a=None):
         5         if a:
         6             self.A = a
         7 
         8 c, d = C(), C(123)
         9 print(Stat(c, "A"), Stat(d, "A"))
        10 d.A = c.A * 2 + c.B
        11 c.B = d.A - 1 - len([d.B, d.B, d.B])
        12 print(Stat(c, "A"), Stat(c, "B"))
        13 print(Stat(d, "A"), Stat(d, "B"))
      
      (0, 0) (0, 1)
      (1, 0) (1, 1)
      (1, 2) (3, 0)

LecturesCMC/PythonIntro2020/11_MiscOOP (last edited 2020-11-21 17:30:16 by FrBrGeorge)