Аннотации и статическая типизация

Аннотации

Базовая статья: О дисциплине использования аннотаций

(немного копипасты из ../12_MetaclassMatch)

Duck typing:

Однако:

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

FrBrGeorge/MyDict/speech_balloon_question.png А хорошо ли понятно, почему вообще работает последний пример?

В действительности объект-аннотация не вычисляется по имени,

В действительности аннотации — это дескриптор(ы). Их значения можно получать в разных форматах:

Этот же приём используется в т. н. псевдонимах типов (type alias):

   1 >>> type integer = int
   2 >>> integer
   3 integer
   4 >>> type(integer)
   5 <class 'typing.TypeAliasType'>
   6 >>> integer.__value__
   7 <class 'int'>
   8 >>> integer.evaluate_value()
   9 <class 'int'>
  10 >>> annotationlib.call_evaluate_function(integer.evaluate_value, annotationlib.Format.STRING)
  11 'int'
  12 

Итого, в 3.14 в принципе имеется отложенное вычисление. Оператор type = работает с правой частью не так, как обычное связывание: имя не превращается в объект перед связыванием, а помещается куда-то внутрь TypeAlias.

   1 >>> type C = D
   2 >>> annotationlib.call_evaluate_function(C.evaluate_value, annotationlib.Format.STRING)
   3 'D'
   4 >>> C.evaluate_value()
   5 Traceback (most recent call last):
   6   File "<python-input-3>", line 1, in <module>
   7     C.evaluate_value()
   8     ~~~~~~~~~~~~~~~~^^
   9   File "<python-input-1>", line 1, in C
  10     type C = D
  11              ^
  12 NameError: name 'D' is not defined

<!> Аннотации настолько отвязаны от реализации, что, например, получить доступ к собственным аннотациям из функции, например, довольно трудно, и получается «хрупкий» код (с классами гораздо проще)

В действительности могут быть вообще чем угодно (например, строками)

Статическая модель типизации

Модуль typing

⇒ Это тема для целого курса.

Кратко: в Python есть (никогда не полная, но стремящаяся к полноте) модель статического описания типов в аннотациях.

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

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

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

Собственно, псевдонимы типов нужны для упрощения:

   1 from itertools import pairwise
   2 from math import dist
   3 type Path = list[tuple[int, int]]
   4 def distance(p: Path) -> float:
   5     return sum(dist(a, b) for a, b in pairwise(p))
   6 print(distance([(0, 0), (3, 4), (-1, 1)]))
   7 print(distance(([0, 0], [3, 4], [-1, 1])))  # неправильно задано, но  всё равно работает!

Again, на семантику они не влияют.

Альтернативы вида тип1 | тип2:

   1 def phpsum(a: int | str, b: int | str) -> int | None:
   2     try:
   3         return int(a) + int(b)
   4     except TypeError:
   5         pass

Абстрактные типы

Абстрактные базовые классы — для проверки .isinstance()/issubclass()

Дженерики

Другие инструменты

Пример: dataclasses — типизированные структуры, логика базируется на аннотациях

MyPy

Что показательно:

  1. Статическая типизация в Python очень активно развивается, достаточно посмотреть сводный What's New и поискать там «type» или «typing».

  2. три официальных блога Гвидо: The Mypy Blog, Neopythonic, The History of Python

Совпадение? Не думаю!™

Ещё раз: зачем аннотации?

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

На MyPy основано большинство дисциплин разработки и систем проверки кода в различных IDE.

Компиляция

Пример для mypyc:

Д/З

/!\ Пока что на EJudge Python3.12, в котором нет отложенных аннотаций / отложенных псевдонимов.

  1. Прочитать про

TODO 2 обязательных задачи (match/case и asyncio + задача на аннотации + задача на статическую типизацию + задача на mypy

  1. EJudge: AnnoCalc 'Вычислимые поля'

    Написать метакласс AnnoCalc, который добавляет в конструируемый с его помощью класс такое свойство: если (1) в классе есть аннотация к полю, (2) самого поля у экземпляра/класса нет, и (3) эта аннотация — строка, то при чтении из этого поля строка-аннотация интерпретируется как выражение с участием других полей объекта/класса. Выражение вычисляется и возвращается соответствующее значение. Если какое-то из условий (1), (2), (3) не выполнено, класс ведёт себя стандартно (добывает имеющееся значение поля или вызывает исключение).

    Input:

       1 class C(metaclass=AnnoCalc):
       2     A: int = 123
       3     B: "A * 2 + 1"
       4 
       5     def __init__(self, A):
       6         self.A = A
       7 
       8 c = C(23)
       9 print(c.B)
      10 c.A = 100
      11 print(c.B)
    
    Output:

    47
    201
  2. EJudge: MulSum 'Умносложение'

    Написать функцию anymulsum(a, b, c), которая возвращает a * b + c. Проверка mypy --strict должна проходить без ошибок, только если b — целое, a и c — одного типа, который предусматривает сложение и умножение на целое (обоих случаях результат должен получиться того же типа). В тестах будут проверяться как допустимые, так и недопустимые сочетания.

    • Недопустимые сочетания: anymulsum("QW", 2, 3), anymulsum("QW", 2, (1, 2)).

    Input:

       1 print(anymulsum("QW", 2, "RTY"))
       2 print(anymulsum(1, 2, 3))
    
    Output:

    QWQWRTY
    5
  3. Обязательная задача из основного корпуса:

    EJudge: AbsoluteMeta 'Метакласс c модулем'

    Написать класс Absolute, который можно использовать как метакласс. Absolute добавляет в порождаемый класс дескриптор abs и метод __abs__(). При создании класса ему можно передавать два именных параметра: width — имя поля «ширина» и height — имя поля «высота». По умолчанию width="width" и height="height". Создание полей abs и __abs__ происходит по следующим правилам (правила применяются по принципу «первое подходящее»):

    • Если метод __abs__() существует, он не меняется; если это не метод — поле заменяется на метод

    • Если существует метод abs() и этот метод допускает вызов без параметров, то __abs__() должен делать то же самое

    • Если существует метод __len__() и этот метод допускает вызов без параметров, то __abs__() должен делать то же самое

    • Если существуют методы «ширина»() и «высота»() и они допускают вызов без параметров, то __abs__() возвращает их произведение

    • Если в классе существуют не-callable поля «ширина» и «высота», то __abs__() возвращает их произведение

    • В противном случае __abs__() возвращает сам объект без изменений

    Дескриптор abs создаётся всегда (в том числе вместо любого атрибута abs, если он был): возвращает __abs__().

    Input:

       1 class D(metaclass=Absolute):
       2     def __len__(self):
       3         return 2
       4 
       5 class E(metaclass=Absolute, height="depth"):
       6     width = depth = 0
       7     def __init__(self, sz):
       8         self.width = self.depth = sz
       9 
      10 print(abs(D()), abs(E(8))) 
    
    Output:

    2 64
  4. Обязательная задача из основного корпуса:

    EJudge: AsyncPoly 'Вычисление многочлена'

    Написать класс YesFuture и функцию parse_poly(многочлен, x) со следующими свойствами:

    • YesFuture — это класс, похожий на Future, только проще.

      • obj = YesFuture(значение=None) задаёт awaitable-объект obj, для которого await obj немедленно возвращает значение.

      • obj.set(новое_значение) подменяет значение

    • У parse_poly() два параметра.

      • Второй параметр — объект типа YesFuture.

      • Первый параметр — строка, в которой описан многочлен от x по стандартным правилам: знак * между необязательным коэффициентом и x не ставится, цифры степени — это unicode-цифры верхнего регистра (SUPERSCRIPT).

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

    • При создании корутины вместо операций сложения, умножения и возведения в степень необходимо пользоваться только специальными корутинами Sum(a, b), Mul(a, b) и Pow(a, b), параметры которых — awaitable-объекты (другие корутины или YesFuture)

    Специальные корутины будут входить в каждый тест.

    Input:

       1 async def Sum(a, b):
       2     return await a + await b
       3 
       4 async def Mul(a, b):
       5     return await a * await b
       6 
       7 async def Pow(a, b):
       8     return await a ** await b
       9 
      10 async def Run(poly, *args):
      11     x = YesFuture()
      12     for arg in args:
      13         s = parse_poly(poly, x)
      14         x.set(arg)
      15         print(await s)
      16 
      17 asyncio.run(Run("3x⁵ + x² - 6x + 4", 4, 2))
    
    Output:

    3068
    92

LecturesCMC/PythonIntro2025/33_StaticTyping (последним исправлял пользователь FrBrGeorge 2025-12-11 18:21:33)