Различия между версиями 15 и 16
Версия 15 от 2022-12-01 14:08:50
Размер: 19174
Редактор: FrBrGeorge
Комментарий:
Версия 16 от 2022-12-07 00:00:26
Размер: 19173
Редактор: FrBrGeorge
Комментарий:
Удаления помечены так. Добавления помечены так.
Строка 15: Строка 15:
 * [[py3doc:enum]] (в частности, [[py3doc:enum.html#how-are-enums-different|How are Enums different?]])  * [[py3doc:enum]] (в частности, [[py3how:enum.html#enum-class-differences|How are Enums different?]])

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

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

Метаклассы

Предуведомление: Тим Петерс про метаклассы ☺.

Посылка: в питоне всё — объект. Объекты-экземпляры класса конструируются с помощью вызова самого класса. А кто конструирует класс? Мета-класс!

Хороший пример 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     @classmethod
       4     def __prepare__(metacls, name, bases, **kwds):
       5         print("prepare", name, bases, kwds)
       6         return super().__prepare__(name, bases, **kwds)
       7 
       8     @staticmethod
       9     def __new__(metacls, name, parents, ns, **kwds):
      10         print("new", metacls, name, parents, ns, kwds)
      11         return super().__new__(metacls, name, parents, ns)
      12 
      13     def __init__(cls, name, parents, ns, **kwds):
      14         print("init", cls, parents, ns, kwds)
      15         return super().__init__(name, parents, ns)
      16 
      17     def __call__(cls, *args, **kwargs):
      18         print("call", cls, args, kwargs)
      19         return super().__call__(*args, **kwargs)
      20 
      21 class C(int, metaclass=ctype, parameter="See me"):
      22      field = 42
      23 
      24 c = C("100500", base=16)
    
    prepare C (<class 'int'>,) {'parameter': 'See me'}
    new <class '__main__.ctype'> C (<class 'int'>,) {'__module__': '__main__', '__qualname__': 'C', 'field': 42} {'parameter': 'See me'}
    init <class '__main__.C'> (<class 'int'>,) {'__module__': '__main__', '__qualname__': 'C', 'field': 42} {'parameter': 'See me'}
    call <class '__main__.C'> ('100500',) {'base': 16}
  • Заметим, куда приезжает именной параметр parameter

  • Особенность __new__: это статический метод, при вызове из super() поле cls надо передавать явно

    • при этом @staticmethod можно не писать (FrBrGeorge/MyDict/speech_balloon_question.png это как?)

  • Особенность __prepare__: это метод класса

    • TODO (проверить!) он не вызывается, если написать C = ctype(…)

  • FrBrGeorge/MyDict/speech_balloon_question.png (кажется!) Общая особенность: нельзя написать без наследования от type(), пример отсюда не работает!

Два примера:

  • Ненаследуемый класс
       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: оно тоже не знает о том, какого типа объекты правильные, какого — нет

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

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

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

  • Пример аннотаций полей (переменных), параметров и возвращаемых значений
       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__ в соответствующем пространстве имён

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

  • Типы в аннотациях —
    • это настоящие типы — не всегда возможно, например:

         1 class Widget:
         2     def copy(self) -> Widget:
         3         return deepcopy(self)
      
    • В действительности могут быть чем угодно (например, строками)
    • Можно включить, чтобы вообще всегда были строками (pep-0563):

         1 >>> from __future__ import annotations
         2 >>> class Widget:
         3 ...     def copy(self) -> Widget:
         4 ...         return deepcopy(self)
         5 >>> Widget.copy.__annotations__
         6 {'return': 'Widget'}
         7 >>> import inspect
         8 >>> inspect.get_annotations(Widget.copy, eval_str=True)
         9 {'return': <class '__main__.Widget'>}
      

<!> Рекомендуется (на момент Python 3.11) использовать именно inspect.get_annotations().

  • Однако eval_str=True — это прямой вызов eval() >:>

  • …как и typing.get_type_hints(C)

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

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

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

       1 >>> def fun(lst: list[int]): pass
       2 >>> inspect.get_annotations(fun)
       3 {'lst': list[int]}
       4 >>> inspect.get_annotations(fun)['lst']
       5 list[int]
       6 >>> type(inspect.get_annotations(fun)['lst'])
       7 <class 'types.GenericAlias'>
       8 >>> ann = inspect.get_annotations(fun)['lst']
       9 >>> typing.get_args(ann)
      10 (<class 'int'>,)
      11 >>> typing.get_origin(ann)
      12 <class 'list'>
      13 
    
    • .get_args() возвращает кортеж с аннотациями элемента, .get_origin() — тип контейнера

    • Again, на семантику работы аннотация не влияет

Модуль typing

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

  • Внезапно полезное: collections.abc

    • например, как узнать, что нечто — это последовательность:
         1 >>> import collections.abc
         2 >>> isinstance([1,2,3], collections.abc.Iterable)
         3 True
         4 >>> isinstance("wer", collections.abc.Iterable)
         5 True
         6 >>> isinstance((i for i in range(10)), collections.abc.Iterable)
         7 True
         8 >>> isinstance(1+3j, collections.abc.Iterable)
         9 False
        10 >>> isinstance("wer", collections.abc.Sequence)
        11 True
        12 >>> isinstance((i for i in range(10)), collections.abc.Sequence)
        13 False
      
  • Инструменты: NoReturn, Union, Optional, Type (если сама переменная — класс), Literal, Final

  • Дженерики (Дженерики, Карл!)

  • Перегрузка функций FrBrGeorge/MyDict/speech_balloon_question.png Шhат

  • 3.11: больше сиплюплюса богу сиплюплюса

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

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

⇒ В язык не входит, делайте сами.

MyPy

А вот чем помимо прочего занимается Гвидо в M$

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

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

  • Прагматика, включенная в синтаксис языка
  • Преобразование 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 def fib(n: int) -> int:
   3     if n <= 1:
   4         return n
   5     else:
   6         return fib(n-2) + fib(n-1)

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

   1 $ python -m timeit -s 'import fib' 'fib.fib(30)'
   2 2 loops, best of 5: 178 msec per loop
   3 $ mypyc fib.p
   4 
   5 $ python -m timeit -s 'import fib' 'fib.fib(30)'
   6 20 loops, best of 5: 13.2 msec per loop
   7 

Д/З

  1. Прочитать про:
  2. EJudge: FloatFix 'Фиксированная точка'

    Написать метакласс fixed с параметром ndigits (по умолчанию 3), в котором все возвращаемые обычными (не статическими и не методами класса) методами значения округляются с помощью round() до ndigits знаков после запятой, если они вещественные по определению модуля numbers.

    Input:

       1 from fractions import Fraction
       2 from decimal import Decimal
       3 
       4 class C(metaclass=fixed, ndigits=4):
       5     def div(self, a, b):
       6         return a / b
       7 
       8 print(C().div(6, 7))
       9 print(C().div(Fraction(6), Fraction(7)))
      10 print(C().div(Decimal(6), Decimal(7)))
    
    Output:

    0.8571
    8571/10000
    0.8571428571428571428571428571
  3. EJudge: InitParam 'Параметры по умолчанию'

    Написать метакласс init, который рассчитывает на то, что методы создаваемого им класса полностью аннотированы. Для каждого позиционного параметра обычного метода в этом классе предусматривается значение по умолчанию (если оно не было задано) на основании типа в аннотации.

    • Если в аннотации тип параметра простой, значение по умолчанию — это тип_пареметра()

    • Если в аннотации тип параметра составной (тип_контейнера[ещё типы], например, list[int]), значение по умолчанию — это тип_контейнера()

      • Будем считать что тип самой аннотации при этом всегда types.GenericAlias

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

    Input:

       1 class C(metaclass=init):
       2     def __init__(self, var: int, rng: range, lst: list[int], defined: str = "defined"):
       3         self.data = f"{var}/{rng}/{lst}/{defined}"
       4 
       5 for c in (C(), C(1, range(3)), C(rng=range(4, 7)), C(lst=[1, 2, 3], defined=3)):
       6     print(c.data)
    
    Output:

    0/None/[]/defined
    1/range(0, 3)/[]/defined
    0/range(4, 7)/[]/defined
    0/None/[1, 2, 3]/3

LecturesCMC/PythonIntro2022/12_MetaclassAnnotations (последним исправлял пользователь FrBrGeorge 2022-12-07 00:00:26)