Различия между версиями 6 и 7
Версия 6 от 2021-12-24 11:47:19
Размер: 16267
Редактор: FrBrGeorge
Комментарий:
Версия 7 от 2021-12-24 12:10:28
Размер: 16684
Редактор: FrBrGeorge
Комментарий:
Удаления помечены так. Добавления помечены так.
Строка 204: Строка 204:
## * [[https://docs.python.org/3/whatsnew/3.7.html?highlight=postponed evaluation annotations#pep-563-postponed-evaluation-of-annotations|Отложенная аннотация]]: [[pep:pep-0563]]
##
* <!> Вместо типов аннотации стали строковыми
##
* Типы хинтов надо перевычислять с помощью [[py3doc:typing.html#typing.get_type_hints|typing.get_type_hints()]]
==== (Оказалось, что не новости) ====
* [[https://docs.python.org/3/whatsnew/3.7.html?highlight=postponed evaluation annotations#pep-563-postponed-evaluation-of-annotations|Отложенная аннотация]]: [[pep:pep-0563]] (обещали в 3.10, но ещё не включили)
* Аннотации ''могут быть'' строковыми (и вообще какими угодно)
* Типы хинтов можно вычислить с помощью [[py3doc:typing.html#typing.get_type_hints|typing.get_type_hints()]], однако это прямой вызов `eval()` ☹:
  {{{##!highlight pycon
>>> import inspect
>>> import typing
>>> class C:
... a: "D"
>>> class D: pass
>>> inspect.get_annotations(C)
{'a': 'D'}
>>> typing.get_type_hints(C)
{'a': <class '__main__.D'>}
}}}

Метаклассы и аннотации

Это две совсем разные темы, если что).

Метаклассы

Хороший пример real-life кода на Python, эксплуатирующий метаклассы и многое другое:

Итак, что может служить конструктором класса?

  • Класс можно создать просто функцией
  • Декоратором
    • Но не т. н. monkey-patch, когда подправляется уже имеющийся класс (⇒ не мы его создаём)
  • От класса можно унаследоваться и всё модифицировать в потомке
  • Зачем тогда нужны ещё отдельные конструкторы классов?

    1. Чёткого ответа нет.
    2. Чтобы закрыть дурную бесконечность (кто конструирует конструктор?) — но это ответ на вопрос «почему?», а не «зачем?»
    3. Чтобы разделить иерархию классов, которой пользуется програрммист, и то, как конструируется сам базовый класс этой иерархии
      • Важно: MRO не найдёт следов метакласса
    4. ⇒ Чтобы сами метаклассы тоже можно было организовывать в виде дерева наследования

Использование type()

  • Создание класса с помощью type(name, bases, dict)

       1 class C:
       2     pass
    
    • это вырожденный вызов type("имя", (кортеж родителей), {пространство имён})

       1 C = type("C", (), {})
    
  • Например,
       1 C = type('Simple', (), {'val': 42, 'getval': lambda self: self.val})
       2 c = C()
       3 c.val, c.getval()
    
  • Но type — это просто класс такой ⇒ от него можно унаследоваться, например, перебить ему __init__():

       1 class overtype(type):
       2     def __init__(self, Name, Parents, Dict):
       3         print(f" Class definition: {Name}{Parents}: {Dict}")
       4         super().__init__(Name, Parents, Dict)
       5 
       6 Boo = overtype("Boo", (), {"A": 100500})
       7 t = Boo()
       8 print(Boo, t, t.A)
    
  • а вот это Boo = overtype… можно записать так:

       1 
       2 class Boo(metaclass=overtype):
       3     A = 100500
    
  • (по сути, class C: — это class C(metaclass=type):)

Подробности:

  • (__prepare__() для автоматического создания пространства имён, если есть), __new__(), __init__()

    • можно перебить ещё __call__ для внесения правок при создании экземпляра класса

  • __new__()

    • создаёт экземпляр объекта (а __init__() заполняет готовый)

    • это метод класса (такой @classmethod без декоратора)

    • в нём можно поменять всё, что в __init__() приезжает готовое и read-only: __slots__, имя класса (если это метакласс) и т. п.

Общая картина:

  •    1 class ctype(type):
       2 
       3     def __call__(self, *args, **kwargs):
       4         print("call", self, args, kwargs)
       5         return super().__call__(*args, **kwargs)
       6 
       7     def __new__(cls, name, parents, ns):
       8         print("new", cls, name, parents, ns)
       9         return super().__new__(cls, name, parents, ns)
      10 
      11     def __init__(self, name, parents, ns):
      12         print("init", self, parents, ns)
      13         return super().__init__(name, parents, ns)
      14 
      15 class C(int, metaclass=ctype):
      16      field = 42
      17 
      18 c = C("100500", base=16)
    
    new <class '__main__.ctype'> C (<class 'int'>,) {'__module__': '__main__', '__qualname__': 'C', 'field': 42}
    init <class '__main__.C'> (<class 'int'>,) {'__module__': '__main__', '__qualname__': 'C', 'field': 42}
    call <class '__main__.C'> ('100500',) {'base': 16}
  • Особенность: __new__ — это метод класса: при вызове из super() поле cls надо передавать явно

Два примера:

  • Ненаследуемый класс
       1 class final(type):
       2     def __new__(metacls, name, parents, namespace):
       3         for cls in parents:
       4             if isinstance(cls, final):
       5                 raise TypeError(f"{cls.__name__} is final")
       6         return super().__new__(metacls, name, parents, namespace)
       7 class E(metaclass=final): pass
       8 class C: pass
       9 class A(C, E): pass
    
    • Обратите внимание на параметры super()

  • Синглтон (больше синглтонов тут)

       1 class Singleton(type):
       2     _instance = None
       3     def __call__(cls, *args, **kw):
       4         if cls._instance is None:
       5              cls._instance = super().__call__(*args, **kw)
       6         return cls._instance
       7 
       8 class S(metaclass=Singleton):
       9     A = 3
      10 s, t = S(), S()
      11 s.newfield = 100500
      12 print(f"{s.newfield=}, {t.newfield=}")
      13 print(f"{s is t=}")
    
  • Модуль types

Аннотации

Duck typing:

  • Экономия кода на описаниях и объявлениях типа
  • Экономия (несравненно бо́льшая) кода на всех этих ваших полиморфизмах
  • ⇒ Компактный читаемый код, хорошее отношение семантика/синтаксис
  • ⇒ Быстрое решение Д/З ☺

Однако:

  • Практически все ошибки — runtime
  • Много страданий от невнимательности (передал объект не того типа, и не заметил, пока не свалилось)
    • Вашей невнимательности не поможет даже хитрое IDE: оно тоже не знает о том, какого типа объекты правильные, какого — нет

    • (соответственно, о полях вашего объекта тоже)
  • Часть прагматики растворяется в коде (например, вы написали строковую функцию, как об этом узнать?)

  • Большие и сильно разрозненные проекты — ?

Поэтому нужны указания о типе полей классов, параметрах и возвращаемых значений функций/методов и т. п. — Аннотации

  • Пример аннотаций полей (переменных), параметров и возвращаемых значений
       1 class C:
       2     A: int = 2
       3     N: float
       4 
       5     def __init__(self, param: int = None, signed: bool = True):
       6         if param != None:
       7             self.A = param if signed else abs(param)
       8 
       9     def mult(self, mlt: int) -> str:
      10         return self.A * mlt
      11 
      12 a: C = C(3)
      13 b: C = C("QWE")
      14 print(f"{a.mult([2])=}, {b.mult(2)=}")
      15 print(f"{a.__annotations__=}")
      16 print(f"{a.mult.__annotations__=}")
      17 print(f"{C.__annotations__}")
      18 print(f"{C.__init__.__annotations__}")
      19 
      20 print(a.mult(2))
      21 print(b.mult(2))
      22 print(a.mult("Ho! "))
      23 print(a.N)  # an Error!
    
  • Аннотации сами по себе не влияют на семантику кода
    • …в т. ч. не занимаются проверкой типов
  • Аннотации заполняют словарь __annotations__ в соответствующем пространстве имён

    • …но не они заводят там имена

  • Типы в аннотациях —
    • это настоящие типы (Python ⩽ 3.9)

    • это строки (Python 3.10+)

Составные и нечёткие типы

составные типы:

  • pep-0585: Во многих случаях можно писать что-то вроде list[int]

       1 >>> seq : list[int]
       2 >>> __annotations__['seq']
       3 list[int]
       4 >>> typ = __annotations__['seq']
       5 >>> type(typ)
       6 <class 'types.GenericAlias'>
       7 >>> typ.__args__
       8 (<class 'int'>,)
       9 >>> typ('sadfaf')
      10 ['s', 'a', 'd', 'f', 'a', 'f']
      11 
    
    • Again, на семантику работы аннотация не влияет

Модуль typing

  • Алиасы (практически typedef), Any, NewType (категоризация), Callable

  • Дженерики и collections.abc

  • Инструменты: NoReturn, Union, Optional, Type (если сама переменная — класс), Literal, Final, …

Новости о Python 3.10

(Оказалось, что не новости)

  • Отложенная аннотация: pep-0563 (обещали в 3.10, но ещё не включили)

    • Аннотации могут быть строковыми (и вообще какими угодно)

    • Типы хинтов можно вычислить с помощью typing.get_type_hints(), однако это прямой вызов eval() ☹: {{{##!highlight pycon

>>> import inspect >>> import typing >>> class C: ... a: "D" >>> class D: pass >>> inspect.get_annotations(C) {'a': 'D'} >>> typing.get_type_hints(C) {'a': <class 'main.D'>} }}}

Развесистая статья на Хабре (⩽ Python3.8, однако ☺, см pep-0585)

Важно: в Python есть поддержка аннотаций, но практически нет их использования (разве что в dataclasses). В язык не входит, делайте сами.

MyPy

Зачем аннотации?

  • Дисциплина программирования
    • большие, сверхбольшие и «долгие» проекты
  • Потенциально возможные проверки

  • Прагматика, включенная в синтаксис языка
  • Преобразование Python-кода в представления, требующие статической типизации

http://www.mypy-lang.org: статическая типизация в Python (ну, почти… или совсем!)

  • Описание типов переменных, параметров и т. п.-

    • Больше не нужно!
  • Проверка выражений с типизированными данными
    • В т. ч. не-проверка нетипизиварованных

  • Пример:
       1 def fun(a: int, b) -> str:
       2     b *= a
       3     return b
       4 
       5 def fun2(a: int) -> str:
       6     c: int = a + "1"
       7     return c
       8 
       9 res: str
      10 var: int
      11 res = fun(1,"qwe")
      12 res = fun(100, 200)
      13 var = fun(1,2)
    
  • Он запускается! Но проверку на статическую типизацию не проходит:
       1 $ mypy ex1.py
       2 ex1.py:6: error: Unsupported operand types for + ("int" and "str")
       3 ex1.py:7: error: Incompatible return value type (got "int", expected "str")
       4 ex1.py:13: error: Incompatible types in assignment (expression has type "str", variable has type "int")
       5 Found 3 errors in 1 file (checked 1 source file)
       6 $ mypy --strict ex1.py
       7 ex1.py:1: error: Function is missing a type annotation for one or more arguments
       8 ex1.py:3: error: Returning Any from function declared to return "str"
       9 ex1.py:6: error: Unsupported operand types for + ("int" and "str")
      10 ex1.py:7: error: Incompatible return value type (got "int", expected "str")
      11 ex1.py:13: error: Incompatible types in assignment (expression has type "str", variable has type "int")
      12 Found 5 errors in 1 file (checked 1 source file)
      13 
    
  • Компиляция. Если все объекты полностью типизированы, у них имеется эквивалент в виде соответствующих структур PythonAPI. ЧСХ, у байт-кода тоже есть эквивалент в Python API

  • Таинственный mypyc

  • Пока не рекомендуют использовать, но сами все свои модули им компилируют!

Пример для mypyc

   1 #!/usr/bin/env python3
   2 import time
   3 
   4 
   5 def fb(x: int, y: int) -> tuple[int, int]:
   6     return y, x + y
   7 
   8 
   9 def test() -> float:
  10     x: int = 0
  11     y: int = 1
  12     t: float = time.time()
  13     for i in range(1000000):
  14         x = 0
  15         y = 1
  16         for j in range(100):
  17             x, y = fb(x, y)
  18     return time.time() - t

Сравнение производительности:

   1 $ mypyc speed.py
   2 running build_ext
   3 building 'speed' extension
   4 creating build/temp.linux-x86_64-3.9
   5 creating build/temp.linux-x86_64-3.9/build
   6 x86_64-alt-linux-gcc -Wno-unused-result -Wsign-compare -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DNDEBUG -g -fwrapv -O3 -Wall -pipe -frecord-gcc-switches -Wall -g -O3 -flto=auto -ffat-lto-objects -pipe -frecord-gcc-switches -Wall -g -O3 -flto=auto -ffat-lto-objects -fPIC -I/home/george/venv/_mypy/lib64/python3/site-packages/mypyc/lib-rt -I/home/george/venv/_mypy/include -I/usr/include/python3.9 -c build/__native.c -o build/temp.linux-x86_64-3.9/build/__native.o -O3 -Werror -Wno-unused-function -Wno-unused-label -Wno-unreachable-code -Wno-unused-variable -Wno-unused-command-line-argument -Wno-unknown-warning-option -Wno-unused-but-set-variable
   7 x86_64-alt-linux-gcc -shared build/temp.linux-x86_64-3.9/build/__native.o -L/usr/lib64 -o /home/george/venv/_mypy/speed.cpython-39.so
   8 $ mv speed.py speed_.py
   9 $ ls
  10 build  ex1.py  __pycache__  speed.cpython-39.so  speed_.py
  11 $ python3 -c "import speed_; print(speed_.test())"
  12 9.72582745552063
  13 $ python3 -c "import speed; print(speed.test())"
  14 2.2559256553649902
  15 

Д/З

Задача сложная, присутствует исследование документации!

  1. Прочитать про:
    • Метаклассы
    • Модуль inspect (вот тут исследование)

    • Аннотации
  2. EJudge: MyMypy 'Типизация вручную'

    Написать метакласс checked, при использовании которого в порождаемом классе проверяются соответствия типов всех аннотированных параметров и возвращаемых значений всех методов. В случае несовпадения инициируется TypeError: Type mismatch: <параметр>

    • Значения параметров по умолчанию не проверяются (для простоты)
    • Поля не проверяются, только методы
    • Предполагается, что должна быть верна проверка isinstance(объект, тип), в противном случае соответствие считается неуспешным

    • При вызове (по крайней мере, в тестах) не используются конструкции *args и **kwargs

    • Параметры проверяются (если они аннотированы, конечно) строго в следующем порядке

      1. Позиционные (переданные в кортеже и распакованные) параметры, по порядку
      2. Явно заданные именные (переданные в словаре и распакованные) параметры в порядке вызова метода
      3. Нераспакованные позиционные параметры, полученные через *args. В этом случае в строке исключения пишется args (имя, которое при описании функции принимало запакованные позиционные параметры)

      4. Нераспакованные именные параметры, полученные через **kwargs

      5. Возвращаемое значение (имя параметра "return", как в аннотации)

    Input:

       1 class E(metaclass=checked):
       2     def __init__(self, var: int):
       3         self.var = var if var%2 else str(var)
       4 
       5     def mix(self, val: int, opt) -> int:
       6         return self.var*val + opt
       7 
       8     def al(self, c: int, d:int=1, *e:int, f:int=1, **g:int):
       9         return self.var*d
      10 
      11 e1, e2 = E(1), E(2)
      12 code = """
      13 e1.mix("q", "q")
      14 e1.mix(2, 3)
      15 e2.mix(2, "3")
      16 e1.al("q")
      17 e1.al(1, 2, 3, 4, 5, 6, foo=7, bar=8)
      18 e2.al(1, 2, 3, 4, 5, 6, foo=7, bar=8)
      19 e1.al("E", 2, 3, 4, 5, 6, foo=7, bar=8)
      20 e1.al(1, "E", 3, 4, 5, 6, foo=7, bar=8)
      21 e1.al(1, 2, "E", 4, 5, 6, foo=7, bar=8)
      22 e1.al(1, 2, 3, "E", 5, 6, foo="7", bar=8)
      23 e1.al(1, f="E", d=1)
      24 e1.al(1, f=1, d="E")
      25 e1.al(1, f="E", d="1")
      26 e1.al(1, d="E", f="1")
      27 e1.al(1, e="E")
      28 e1.al(1, g="E")
      29 """
      30 
      31 for c in code.strip().split("\n"):
      32     try:
      33         res = eval(c)
      34     except TypeError as E:
      35         res = E
      36     print(f"Run: {c}\nGot: {res}")
    
    Output:

    Run: e1.mix("q", "q")
    Got: Type mismatch: val
    Run: e1.mix(2, 3)
    Got: 5
    Run: e2.mix(2, "3")
    Got: Type mismatch: return
    Run: e1.al("q")
    Got: Type mismatch: c
    Run: e1.al(1, 2, 3, 4, 5, 6, foo=7, bar=8)
    Got: 2
    Run: e2.al(1, 2, 3, 4, 5, 6, foo=7, bar=8)
    Got: 22
    Run: e1.al("E", 2, 3, 4, 5, 6, foo=7, bar=8)
    Got: Type mismatch: c
    Run: e1.al(1, "E", 3, 4, 5, 6, foo=7, bar=8)
    Got: Type mismatch: d
    Run: e1.al(1, 2, "E", 4, 5, 6, foo=7, bar=8)
    Got: Type mismatch: e
    Run: e1.al(1, 2, 3, "E", 5, 6, foo="7", bar=8)
    Got: Type mismatch: e
    Run: e1.al(1, f="E", d=1)
    Got: Type mismatch: f
    Run: e1.al(1, f=1, d="E")
    Got: Type mismatch: d
    Run: e1.al(1, f="E", d="1")
    Got: Type mismatch: f
    Run: e1.al(1, d="E", f="1")
    Got: Type mismatch: d
    Run: e1.al(1, e="E")
    Got: Type mismatch: e
    Run: e1.al(1, g="E")
    Got: Type mismatch: g

LecturesCMC/PythonIntro2021/12_MetaclassAnnotations (последним исправлял пользователь FrBrGeorge 2021-12-24 12:10:28)