Различия между версиями 3 и 10 (по 7 версиям)
Версия 3 от 2020-11-17 14:12:52
Размер: 12214
Редактор: FrBrGeorge
Комментарий:
Версия 10 от 2020-11-21 20:30:16
Размер: 12883
Редактор: FrBrGeorge
Комментарий:
Удаления помечены так. Добавления помечены так.
Строка 132: Строка 132:
 * [[py3how:descriptor.html|подробрая статья]]  * [[py3how:descriptor.html|подробрая статья]] (рекомендуется)
Строка 137: Строка 137:
 * ''Протокол дескриптора'' — объект с методами `.__get__()`, `.__set__()` и `.__delete__()` (если без `__set__()`, значит, это не данные, а, скажем, функция)  * ''Протокол дескриптора'' — объект с методами `.__get__()`, `.__set__()` и `.__delete__()`
 * если без `__set__()`, значит, это не данные, а, скажем, метов (т. н. ''non-data descriptor'')
Строка 140: Строка 141:
  * конкретный экземпляр передаётся вторым параметром (`None`, если вызывается как метод класса), так что при желании можно различить
 * Имеет ''преимущество'' перед полем экземпляра (в отличие от обычных полей класса)
{{{#!highlight python3
  * конкретный экземпляр передаётся вторым параметром
  * ''тип'' (класс) экземпляра передаётся третьим параметром в `.__get__()`
   * Например, если пытаться прочесть поле класса `класс.дескриптор`, второй параметр будет равен `None`
 * Если задан `.__set__()`, Имеет ''преимущество'' перед полем экземпляра (в отличие от обычных полей класса)
  * если не задан, т. е. для non-data, то, конечно, первое же связывание заведёт на этом месте обычное поле экземпляра
 {{{#!highlight python
Строка 144: Строка 148:
    value = None
Строка 147: Строка 150:
        return self.value

    def __set__(self, cls, val):
        print(f"Set {cls} to {val}")
        self.value = val

    def __delete__(self, cls):
        print("Delete {cls}")
        self.value = None
        return obj._value

    def __set__(self, obj, val):
        print(f"Set in {obj} to {val}")
        obj._value = val

    def __delete__(self, obj):
        print("Delete from {obj}")
        obj._value = None
Строка 165: Строка 168:

d = C("De")
print(d.data)
d.data = 1
print(d.data)
del d.data
}}}

Обратите внимание: дескриптор — поле ''класса'', так что
{{{#!highlight python3
e = C("Ye")
d.data = 100500
print(d.data)
print(e.data)
}}}
даст
{{{
Set <De> to 100500
Get from <class '__main__.C'>:<De>
}}}
  →
 {{{#!highlight pycon
>>> c = C("Obj")
>>> c.data = 100500
Set in <Obj> to 100500
>>> c.data
Get from <class '__main__.C'>:<Obj>
Строка 185: Строка 177:
Get from <class '__main__.C'>:<Ye>
100500
}}}
В методах дескриптора всегда известно, через какой объект идёт доступ, поэтому при желании можно брать `id(объект)` и на этом основании делать что-то разное.

Если указать только `__get__`, получится т. н. non-data descriptor, основное отличие которого — возможность «загородить» его полем из `__dict__`.
>>> del c.data
Delete from {obj}
>>> print(c.data)
Get from <class '__main__.C'>:<Obj>
None
}}}
  * Обратите внимание на то, что `._value` — это поле ''конкретного объекта'', в которое ходит дескриптор
Строка 263: Строка 256:
  * (!) Важное отличие: `property` — это поле `объекта`, а не класса, т. е. именно реализация шаблона [[RW:Геттер_(программирование)|getter/setter]] в чистом виде
Строка 282: Строка 276:
  * Обратите внимание, что в примере `_var`, а не `__var` (на самом деле могло быть что угодно)
Строка 284: Строка 277:

'''Вот отсюда точно не успеем'''

== Метаклассы (одним глазом) ==
 * Про метаклассы [[py3ref:datamodel.html#metaclasses|в документации]]
 * [[https://realpython.com/python-metaclasses/|На realpython.com]]
 * Метакласс — конструктор классов
 * <!> Никто не запрещает делать конструктор классов так:
{{{#!python
def fc(st):
    class _bzz:
        def __init__(self):
            self.st = st
    return _bzz

C = fc(10500)
c = C()
print(c.st)
}}}
Все заметили, что это почти декоратор? Что ему не хватает до того, чтобы быть настоящим декоратором?

В любом случае есть несколько вариантов, когда такой механизм не работает. Есть более правильный способ — метаклассы.

 * Создание класса с помощью [[py3doc:functions.html#type|type(name, bases, dict)]]
 {{{#!python
 class C:
    pass
 }}}
это
 {{{#!python
 C = type("C", (), {})
 }}}
* `type` — это просто класс такой ⇒
 {{{#!python
 class overtype(type):
    pass

T = overtype("Boo", (), {})
 }}}
* ну так вот, «`T = overtype(…)`» == «`class T(metaclass=overtype): …`»
 * определённые правила для [[py3ref:datamodel.html#object.__init__|__init__()]] и [[py3ref:datamodel.html#object.__new__|__new__()]]

<!> Не путать с наследованием!

{{{#!python
from time import ctime

class D(type):
    def __str__(self):
        self.__date = ctime()
        return f"<class {self.__class__.__name__} created at {self.__date}>"

class C(metaclass=D):
    pass

class E:
    pass
}}}

{{{#!highlight pycon
>>> c, e = C(), E()
>>> print(C, E)
<class D created at Sun Dec 1 18:17:59 2019> <class '__main__.E'>
}}}

'''TODO''' про `.__new_()` подробнее
  * В частности, [[py3doc:functools#functools.wraps|@functools.wraps]], который помогает сохранить исходное имя и строку документации функции

##
'''Вот отсюда точно не успеем'''

##== Метаклассы (одним глазом) ==
## * Про метаклассы [[py3ref:datamodel.html#metaclasses|в документации]]
## * [[https://realpython.com/python-metaclasses/|На realpython.com]]
## * Метакласс — конструктор классов
## * <!> Никто не запрещает делать конструктор классов так:
##{{{#!python
##def fc(st):
## class _bzz:
## def __init__(self):
## self.st = st
## return _bzz

##C = fc(10500)
##c = C()
##print(c.st)
##}}}
##
Все заметили, что это почти декоратор? Что ему не хватает до того, чтобы быть настоящим декоратором?

##В любом случае есть несколько вариантов, когда такой механизм не работает. Есть более правильный способ — метаклассы.

## * Создание класса с помощью [[py3doc:functions.html#type|type(name, bases, dict)]]
## {{{#!python
## class C:
## pass
## }}}
##
это
## {{{#!python
## C = type("C", (), {})
## }}}
##
* `type` — это просто класс такой ⇒
## {{{#!python
## class overtype(type):
## pass

##
T = overtype("Boo", (), {})
## }}}
##
* ну так вот, «`T = overtype(…)`» == «`class T(metaclass=overtype): …`»
## * определённые правила для [[py3ref:datamodel.html#object.__init__|__init__()]] и [[py3ref:datamodel.html#object.__new__|__new__()]]

##<!> Не путать с наследованием!

##{{{#!python
##from time import ctime

##class D(type):
## def __str__(self):
## self.__date = ctime()
## return f"<class {self.__class__.__name__} created at {self.__date}>"

##class C(metaclass=D):
## pass

##
class E:
## pass
##}}}

##
{{{#!highlight pycon
##>>> c, e = C(), E()
##>>> print(C, E)
##<class D created at Sun Dec 1 18:17:59 2019> <class '__main__.E'>
##}}}

##
'''TODO''' про `.__new_()` подробнее
Строка 352: Строка 346:
 1.#0 Прочитать про всё, упомянутое выше. Пощёлкать примеры по каждой теме.
Строка 353: Строка 348:
 1. <<EJCMC(148, TypeCheck, Проверка типов)>>
 1. <<EJCMC(148, FieldCounter, Статистика с полей)>>

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

Расширения объектной модели 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. EJudge: TypeCheck 'Проверка типов'

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

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

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

      • Типы именованных параметров перечислены в конце последовательности_типов

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

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

    • Сначала проверяются параметры в порядке описания в функции, затем вызывается функция, после чего проеряется результат. Ислкючение возникает при первом несовпадении типа.
    Input:

    18
    Type of argument 2 is not <class 'str'>
    Output:

    18
    Type of argument 2 is not <class 'str'>
  2. EJudge: FieldCounter 'Статистика с полей'

    Написать функцию,Stat() которая умеет выполнять две роли:

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

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

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

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

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

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

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

      • Если статистика по данному полю не ведётся, или поля не существует, обя значения равны 0
    • Экземпляр класса инициализируется с помощью __init__(), т. е. класс не является потомком встроенного класса, не переопределяет __new__(), __getattribute__() и т. п., изначально не содержит слотов/дескрипторов.

    Input:

       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"))
      14 print(Stat(c, "Foo"))
    
    Output:

    (0, 0) (0, 1)
    (1, 0) (1, 1)
    (1, 2) (3, 0)
    (0, 0)

LecturesCMC/PythonIntro2020/11_MiscOOP (последним исправлял пользователь FrBrGeorge 2020-11-21 20:30:16)