Прочтите оригинальный блог на dev.to

Mypy - это средство проверки статического типа для Python. Он действует как линтер, который позволяет вам писать статически типизированный код и проверять правильность ваших типов.

Все, что делает mypy, это проверяет подсказки вашего типа. Это не похоже на TypeScript, который нужно скомпилировать, чтобы он заработал. Весь код mypy является действительным Python.

Это дает нам преимущество наличия типов, поскольку вы можете точно знать, что в вашем коде нет несоответствия типов, как и в типизированных, скомпилированных языках, таких как C ++ и Java, но вы также получаете преимущество быть Python 🐍 ✨ (вы также получаете другие преимущества, например нулевую безопасность!)

Для более подробного объяснения того, для чего нужны типы, перейдите в блог, который я написал ранее: Нужны ли Python типы?

Эта статья станет глубоким погружением для всех, кто хочет узнать о mypy и всех его возможностях.

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

Показатель

Настройка mypy

Все, что вам действительно нужно сделать, чтобы его настроить, - это pip install mypy.

Использование mypy в терминале

Давайте создадим обычный файл Python и назовем его test.py:

Здесь пока нет определений типов, но давайте пробежимся по нему mypy, чтобы увидеть, что там написано.

$ mypy test.py
Success: no issues found in 1 source file

🤨

Не волнуйтесь, в этом нет ничего неожиданного. Как объяснялось в моей предыдущей статье, mypy не заставляет вас добавлять типы в ваш код. Но, если он найдет типы, он их оценит.

Это определенно может привести к тому, что mypy будет упускать целые части вашего кода только потому, что вы случайно забыли добавить типы.

К счастью, есть способы настроить mypy, чтобы он всегда проверял наличие чего-либо:

$ mypy --disallow-untyped-defs test.py
test.py:1: error: Function is missing a return type annotation
Found 1 error in 1 file (checked 1 source file)

И теперь это дало нам желаемую ошибку.

Есть множество этих --disallow- аргументов, которые мы должны использовать, если мы начинаем новый проект, чтобы предотвратить такие неудачи, но mypy дает нам дополнительный мощный аргумент, который делает все это: --strict

$ mypy --strict test.py
test.py:1: error: Function is missing a return type annotation
test.py:4: error: Call to untyped function "give_number" in typed context Found 2 errors in 1 file (checked 1 source file)

Это дало нам еще больше информации: тот факт, что мы используем give_number в нашем коде, который не имеет определенного типа возврата, поэтому этот фрагмент кода также может иметь непредвиденные проблемы.

TL; DR: для начала используйте mypy --strict filename.py

Использование mypy в VSCode

VSCode имеет неплохую интеграцию с mypy. Все, что вам нужно для работы mypy, - это добавить это в свой settings.json:

...
  "python.linting.mypyEnabled": true,
  "python.linting.mypyArgs": [
    "--ignore-missing-imports",
    "--follow-imports=silent",
    "--show-column-numbers",
    "--strict"
  ],
...

Теперь открытие папки с кодом в python должно показать вам те же самые ошибки на панели проблем:

Кроме того, если вы используете VSCode, я настоятельно рекомендую установить Pylance из панели расширений, это очень поможет с заполнением табуляции и более глубоким пониманием ваших типов.

Хорошо, теперь переходим к исправлению этих проблем.

Примитивные типы

Самые фундаментальные типы, существующие в mypy, - это примитивные типы. Назвать несколько:

  • int
  • str
  • float
  • bool ...

Заметили узор?

Ага. Это те же самые примитивные типы данных Python, с которыми вы знакомы.

И это на самом деле все, что нам нужно для исправления наших ошибок:

Все, что мы изменили, - это определение функции в def:

Здесь говорится, что «функция double принимает аргумент n, который является int, а функция возвращает int.

И теперь запускаем mypy:

$ mypy --strict test.py
Success: no issues found in 1 source file

Поздравляем, вы только что написали свою первую программу на Python с проверкой типов 🎉

Мы можем запустить код, чтобы убедиться, что он действительно работает:

$ python test.py
42

Я должен пояснить, что mypy выполняет всю проверку типов, даже не запуская код. Это то, что называется инструментом статического анализа (эта статика отличается от статики в «статической типизации»), и, по сути, это означает, что он работает не путем запуска вашего кода Python, а с помощью оценка структуры вашей программы. Это означает, что если ваша программа выполняет интересные действия, такие как вызовы API или удаление файлов в вашей системе, вы все равно можете запускать mypy для своих файлов, и это не будет иметь никакого реального эффекта.

Интересно отметить, что мы также объявили num в программе, но мы никогда не сообщали mypy, какой это будет тип, и все же он все еще работал нормально.

Мы могли бы сказать mypy, что это за тип, вот так:

И mypy тоже будет этому доволен. Но нам не нужно указывать этот тип, потому что mypy уже знает его тип. Поскольку double должен возвращать только int, mypy вывел его:

И вывод - это круто. В 80% случаев вы будете писать только типы для определений функций и методов, как мы это сделали в первом примере. Заметным исключением из этого правила являются «типы пустых коллекций», которые мы сейчас обсудим.

Типы коллекций

Типы коллекций - это то, как вы можете добавлять типы в коллекции, такие как «список строк» ​​или «словарь со строковыми ключами и логическими значениями» и т. Д.

Некоторые типы коллекций включают:

  • List
  • Dict
  • Set
  • DefaultDict
  • Deque
  • Counter

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

Все они определены в модуле typing, который встроен в Python, и есть одна общая черта: они общие.

У меня есть целый раздел, посвященный обобщениям ниже, но все сводится к тому, что «с универсальными типами вы можете передавать типы внутри других типов». Вот как можно использовать типы коллекций:

Это сообщает mypy, что nums должен быть списком целых чисел (List[int]) и что average возвращает float.

Вот еще пара примеров:

Помните, я сказал, что пустые коллекции - это один из редких случаев, когда нужно вводить данные? Это потому, что mypy не может определить типы в этом случае:

Поскольку в наборе нет элементов, mypy не может статически определить, какой он должен быть тип.

Небольшое примечание: если вы попытаетесь запустить mypy на приведенном выше фрагменте кода, это действительно удастся. Это потому, что разработчики mypy умны и добавили простые примеры упреждающего вывода. То есть новые версии mypy могут определять такие типы в простых случаях. Имейте в виду, что это не всегда работает.

Чтобы исправить это, вы можете вручную добавить требуемый тип:

Примечание. Начиная с Python 3.7, вы можете добавить будущий импорт from __future__ import annotations вверху ваших файлов, что позволит вам использовать встроенные типы в качестве универсальных, т. е. вместо этого вы можете использовать list[int] из List[int]. Если вы используете Python 3.9 или выше, вы можете использовать этот синтаксис без необходимости импорта __future__. Однако в некоторых крайних случаях это может не сработать, поэтому пока я предлагаю использовать typing.List варианты. Это подробно описано в PEP 585.

Отладка типов - Часть 1

Допустим, вы читаете чужой код или код своего прошлого, и не совсем понятно, какой тип переменной. В коде много логических выводов, и он использует некоторые встроенные методы, которые вы точно не помните, как они работают, бла-бла.

К счастью, mypy позволяет узнать тип любой переменной с помощью reveal_type:

Запуск mypy на этом фрагменте кода дает нам:

$ mypy --strict test.py 
test.py:12: note: Revealed type is 'builtins.int'

Пока не обращайте внимания на встроенные команды, они могут сказать нам, что counts здесь int.

Круто, правда? Вам не нужно полагаться на IDE или VSCode, чтобы использовать наведение для проверки типов переменной. Простой терминал и mypy - это все, что вам нужно. (хотя VSCode внутренне использует аналогичный процесс для получения всей информации о типе)

Однако некоторым из вас может быть интересно, откуда взялся reveal_type. Мы не импортировали его из _52 _... это новая встроенная функция? Это вообще действительно в питоне?

И, конечно же, если вы попытаетесь запустить код:

py test.py    
Traceback (most recent call last):
  File "/home/tushar/code/test/test.py", line 12, in <module>
    reveal_type(counts)
NameError: name 'reveal_type' is not defined

reveal_type - это специальная «функция mypy». Поскольку python не знает о типах (аннотации типов игнорируются во время выполнения), только mypy знает о типах переменных при выполнении проверки типов. Итак, только mypy может работать с reveal_type.

Все это означает, что вы должны использовать reveal_type только для отладки кода и удалять его, когда закончите отладку.

Союз и необязательный

До сих пор мы видели только переменные и коллекции, которые могут содержать только один тип значения. Но как насчет этого фрагмента кода?

Какой тип fav_color в этом коде?

Попробуем сделать reveal_type:

Кстати, поскольку эта функция не имеет оператора возврата, ее тип возврата - None.

Запуск mypy на этом:

$ mypy test.py 
test.py:5: note: Revealed type is 'Union[builtins.str*, None]'

И мы получаем один из двух наших новых типов: Union. В частности, Union[str, None].

Все это означает, что fav_color может быть одного из двух разных типов: либо str, либо None.

А союзы на самом деле очень важны для Python из-за того, как Python выполняет полиморфизм. Вот более простой пример:

$ python test.py
Hi!
This is a test
of polymorphism

Теперь давайте добавим к нему типы и узнаем кое-что, используя нашего друга reveal_type:

Вы можете угадать результат reveal_types?

$ mypy test.py 
test.py:4: note: Revealed type is 'Union[builtins.str, builtins.list[builtins.str]]'
test.py:8: note: Revealed type is 'builtins.list[builtins.str]'
test.py:11: note: Revealed type is 'builtins.str'

Mypy достаточно умен: если вы добавите isinstance(...) проверку к переменной, он будет правильно предполагать, что тип внутри этого блока сужен до этого типа.

В нашем случае item был правильно идентифицирован как List[str] внутри блока isinstance и str в блоке else.

Это чрезвычайно мощная функция mypy, называемая сужением типа.

А теперь рассмотрим более надуманный пример - аннотированную Python-реализацию встроенной функции abs:

И это все, что вам нужно знать о Union.

… Так что Optional спросите вы?

Что ж, Union[X, None], похоже, так часто встречается в Python, что они решили, что для этого нужно сокращение. Optional[str] - это просто более короткий способ записи Union[str, None].

Любой тип

Если вы когда-нибудь попытаетесь запустить reveal_type внутри нетипизированной функции, произойдет следующее:

$ mypy test.py
test.py:6: note: Revealed type is 'Any'
test.py:6: note: 'reveal_type' always outputs 'Any' in unchecked functions

Сообщается, что обнаруженный тип - Any.

Any просто означает, что здесь можно передать что угодно. Что бы ни было передано, mypy должен просто принять это. Другими словами, Any отключает проверку типа.

Конечно, это означает, что если вы хотите воспользоваться преимуществами mypy, вам следует избегать использования Any в максимально возможной степени.

Но поскольку Python по своей сути является языком с динамической типизацией, в некоторых случаях невозможно узнать, какой тип чего-либо будет. В таких случаях можно использовать Any. Например:

Вы также можете использовать Any в качестве значения-заполнителя для чего-то, пока вы выясняете, каким оно должно быть, тем временем, чтобы порадовать mypy. Но не забудьте избавиться от Any, если сможете.

Разные типы

Кортеж

Вы можете думать о кортежах как о неизменяемом списке, но Python думает об этом совсем по-другому.

Кортежи отличаются от других коллекций, поскольку они, по сути, представляют собой способ представления набора точек данных, связанных с сущностью, что-то вроде того, как C struct хранится в памяти. В то время как другие коллекции обычно представляют собой группу объектов, кортежи обычно представляют один объект.

Хороший пример - sqlite:

Кортежи также пригодятся, когда вы хотите вернуть несколько значений из функции, например:

По этим причинам кортежи, как правило, имеют фиксированную длину, а каждый индекс имеет определенный тип. (В нашем примере sqlite был массив длиной 3 и типы int, str и int соответственно.

Вот как вы набираете кортеж:

Однако иногда вам нужно создавать кортежи переменной длины. Для этого можно использовать синтаксис Tuple[X, ...].

... в этом случае просто означает, что в массиве есть переменное количество элементов, но их тип - X. Например:

TypedDict

TypedDict - это словарь, ключи которого всегда являются строками, а значения имеют указанный тип. Во время выполнения он ведет себя точно так же, как обычный словарь.

По умолчанию все ключи должны присутствовать в TypedDict. Это можно изменить, указав total=False.

Буквальный

Literal представляет тип буквального значения. Вы можете использовать его, чтобы ограничить уже существующие типы, такие как str и int, только некоторыми конкретными их значениями. Вот так:

$ mypy test.py 
test.py:7: error: Argument 1 to "i_only_take_5" has incompatible type "Literal[6]"; expected "Literal[5]"

У этого есть несколько интересных вариантов использования. Примечательным является использование его вместо простых перечислений:

$ mypy test.py
test.py:8: error: Argument 1 to "make_request" has incompatible type "Literal['DLETE']"; expected "Union[Literal['GET'], Literal['POST'], Literal['DELETE']]"

Ой, вы допустили опечатку в 'DELETE'! Не волнуйтесь, mypy сэкономил вам час отладки.

Финал

Final - это аннотация, объявляющая переменную окончательной. Это означает, что переменную нельзя переназначить. Это похоже на final в Java и const в JavaScript.

Без возврата

NoReturn - интересный тип. Он редко когда используется, но он все еще должен существовать на тот момент, когда вам, возможно, придется его использовать.

Бывают случаи, когда у вас может быть функция, которая никогда не вернется. Я могу придумать для этого две возможные причины:

  • Функция всегда вызывает исключение или
  • Функция представляет собой бесконечный цикл.

Вот пример того и другого:

Обратите внимание, что в обоих этих случаях также будет работать ввод функции как -> None. Но если вы хотите, чтобы функция никогда ничего не возвращала, вы должны ввести ее как NoReturn, потому что тогда mypy покажет ошибку, если функция когда-либо будет иметь условие, при котором она действительно вернется.

Например, если вы измените while True: на while False: или while some_condition() в первом примере, mypy выдаст ошибку:

$ mypy test.py
test.py:6: error: Implicit return in function which does not return

Классы набора текста

Все методы класса по сути типизированы так же, как и обычные функции, за исключением self, который остается нетипизированным. Вот простой класс Stack:

Если вы никогда не видели {x!r} синтаксис внутри f-строк, это способ использовать repr() значения. Для получения дополнительной информации, pyformat.info - очень хороший ресурс для изучения функций форматирования строк Python.

Однако есть одно предостережение при вводе классов: обычно вы не можете получить доступ к самому классу внутри объявлений функций класса (потому что класс еще не завершил объявление себя, потому что вы все еще объявляете его методы).

Так что что-то вроде этого недопустимый Python:

$ mypy --strict test.py
Success: no issues found in 1 source file

$ python test.py
Traceback (most recent call last):
  File "/home/tushar/code/test/test.py", line 11, in <module>
    class MyClass:
  File "/home/tushar/code/test/test.py", line 15, in MyClass
    def copy(self) -> MyClass:
NameError: name 'MyClass' is not defined

Есть два способа исправить это:

  • Превратите имя класса в строку: создатели PEP 484 и Mypy знали, что существуют такие случаи, когда вам может потребоваться определить тип возврата, который еще не существует. Итак, mypy может проверять типы, если они заключены в строки.
  • Используйте from __future__ import annotations. Это включает новую функцию в Python, которая называется отложенная оценка аннотаций типов. По сути, это заставляет Python обрабатывать все аннотации типов как строки, сохраняя их во внутреннем атрибуте __annotations__. Подробности описаны в PEP 563.

Начиная с Python 3.11, поведение отложенной оценки станет по умолчанию, и вам больше не потребуется импорт __future__.

Ввод namedtuples

namedtuples очень похожи на кортежи, за исключением того, что каждый индекс их полей назван, и у них есть некоторый синтаксический сахар, который позволяет вам получить доступ к его свойствам, таким как атрибуты объекта:

Поскольку базовая структура данных представляет собой кортеж, и нет реального способа предоставить какую-либо информацию о типе именованным кортежам, по умолчанию это будет тип Tuple[Any, Any, Any].

Чтобы бороться с этим, Python добавил класс NamedTuple, который вы можете расширить до его типизированного эквивалента:

Внутренняя работа NamedTuple:

Если вам интересно, как NamedTuple работает под капотом: age: int - это объявление типа без каких-либо назначений (например, age : int = 5).

Объявления типа внутри функции или класса на самом деле не определяют переменную, но они добавляют аннотацию типа к метаданным этой функции или класса в форме словарной статьи в x.__annotations__.

Выполнение print(ishan.__annotations__) в приведенном выше коде дает нам {'name': <class 'str'>, 'age': <class 'int'>, 'bio': <class 'str'>}.

typing.NamedTuple использует эти аннотации для создания необходимого кортежа.

Декораторы набора текста

Декораторы - довольно продвинутая, но действительно мощная функция Python. Если вы ничего не знаете о декораторах, я бы порекомендовал вам посмотреть Энтони объясняет декораторов, но я также кратко объясню это здесь.

Декоратор - это, по сути, функция, которая является оболочкой для другой функции. Декораторы могут расширять функциональные возможности уже существующих функций, выполняя другие побочные эффекты всякий раз, когда вызывается исходная функция. Декоратор украшает функцию, добавляя новые функции.

Простым примером может быть отслеживание того, сколько времени требуется для выполнения функции:

Чтобы иметь возможность набирать это, нам нужен способ определения типа функции. Этот способ называется Callable.

Callable - это универсальный тип со следующим синтаксисом:

Callable[[<list of argument types>], <return type>]

Типы аргументов функции попадают в первый список внутри Callable, а тип возвращаемого значения следует за ним. Несколько примеров:

Вот как можно реализовать ранее показанный time_it декоратор:

Примечание: Callable - это так называемый тип утки. Это означает, что вы можете создать свой собственный объект и сделать его действительным Callable, реализовав волшебный метод под названием __call__. У меня есть специальный раздел, в котором я подробно расскажу о типах уток.

Генераторы набора текста

Генераторы - также довольно сложная тема, которую следует полностью осветить в этой статье, и вы можете посмотреть
Энтони объясняет генераторы, если никогда о них не слышали. Краткое объяснение таково:

Генераторы чем-то похожи на вечные функции. Вместо того, чтобы возвращать значение один раз, они извлекают из них yield значений, которые вы можете перебирать. Когда вы yield значение из итератора, его выполнение приостанавливается. Но когда у генератора запрашивается другое значение, он возобновляет выполнение с того места, где оно было остановлено в последний раз. Когда функция генератора возвращается, итератор останавливается.

Вот пример:

Чтобы добавить аннотации типов к генераторам, вам понадобится typing.Generator. Синтаксис следующий:

Generator[yield_type, throw_type, return_type]

С такими знаниями набрать это довольно просто:

Поскольку мы не генерируем ошибок в генераторе, throw_type равно None. И хотя тип возвращаемого значения - int, что правильно, мы все равно не используем возвращаемое значение, поэтому вы также можете использовать Generator[str, None, None] и вообще пропустить часть return.

Печатать *args и **kwargs

*args и **kwargs - это функция python, которая позволяет передавать любое количество аргументов и аргументов ключевого слова функции (это то, что обозначают имена args и kwargs, но эти имена являются просто соглашением, вы можете называть переменные как угодно) . Энтони объясняет арги и варги

Все дополнительные аргументы, переданные в *args, превращаются в кортеж, а аргументы kewyord превращаются в словарь, где ключи являются строковыми ключевыми словами:

Поскольку *args всегда будет иметь тип Tuple[X], а **kwargs всегда будет иметь тип Dict[str, X], для их ввода нам нужно предоставить только одно значение типа X. Вот практический пример:

Типы уток

Типы уток - довольно фундаментальная концепция Python: вся объектная модель Python построена на идее типов уток.

Цитата Алекса Мартелли:

«На самом деле вас не интересует IS-A - вы действительно заботитесь только о BEHAVES-LIKE-A- (в этом-конкретном-контексте), поэтому, если вы проводите тестирование, это поведение - то, на что вы должны тестировать. ”

Это означает, что Python не волнует, какой тип объекта, а скорее как он себя ведет.

У меня была небольшая заметка выше в декораторах набора текста, в которой упоминалось, что утка печатает функцию с __call__, теперь вот фактическая реализация:

PS: Запуск mypy поверх приведенного выше кода приведет к загадочной ошибке Особые формы, не беспокойтесь об этом прямо сейчас, мы исправим это в разделе Протокол. Все, что я сейчас показываю, это то, что код Python работает.

Вы можете видеть, что Python согласен с тем, что обе эти функции «доступны для вызова», т.е. вы можете вызывать их, используя синтаксис x(). (поэтому тип называется Callable, а не что-то вроде Function)

Какие типы уток предоставляют вам, так это возможность определять параметры ваших функций и возвращаемые типы не в терминах конкретных классов, а в терминах того, как ведет себя ваш объект, давая вам гораздо больше гибкости в том, какие вещи вы можете использовать в своем коде сейчас, а также позволяет значительно упростить расширяемость в будущем без внесения «критических изменений».

Вот простой пример:

Запуск этого кода с Python работает нормально. Но запуск mypy по этому поводу дает нам следующую ошибку:

$ mypy test.py 
test.py:12: error: Argument 1 to "count_non_empty_strings" has incompatible type "ValuesView[str]"; expected "List[str]"

ValuesView - это тип, когда вы выполняете dict.values(), и хотя в данном случае вы можете представить его как список строк, это не совсем тот тип List.

Фактически, ни один из других типов последовательностей, таких как tuple или set, не будет работать с этим кодом. Вы можете исправить это для некоторых встроенных типов, выполнив strings: Union[List[str], Set[str], ...] и так далее, но сколько типов вы добавите? А как насчет сторонних / нестандартных типов?

Правильное решение здесь - использовать Тип утки (да, мы наконец дошли до сути). Единственное, что мы хотим гарантировать в этом случае, - это возможность итерации объекта (что в терминах Python означает, что он реализует __iter__ магический метод), и правильный тип для этого - Iterable:

И теперь mypy доволен нашим кодом.

Многие, многие из этих типов уток поставляются в модуле Python typing, и некоторые из них включают:

  • Sequence для определения вещей, которые можно индексировать и перевернуть, например List и Tuple.
  • MutableMapping, когда у вас есть структура данных типа пары ключ-значение, например dict, а также другие, такие как defaultdict, OrderedDict и Counter из модуля коллекций.
  • Collection, если все, что вас волнует, - это наличие конечного числа элементов в вашей структуре данных, например. set, list, dict или что-нибудь из модуля коллекций.

Если вы еще этого не сделали, вам действительно следует изучить, как синтаксис Python и функции верхнего уровня подключаются к объектной модели Python через __magic_methods__ для практически всего поведения Python. Документация к нему находится прямо здесь, и есть отличная беседа Джеймса Пауэлла, которая с самого начала действительно глубоко погружается в эту концепцию.

Перегрузка функций с @overload

Напишем простую add функцию, которая поддерживает int и float:

Реализация кажется прекрасной ... но mypy это не нравится:

$ test.py:15: error: No overload variant of "__getitem__" of "list" matches argument type "float"
test.py:15: note: Possible overload variants:
test.py:15: note:     def __getitem__(self, int) -> int
test.py:15: note:     def __getitem__(self, slice) -> List[int]

Mypy пытается нам сказать вот что:

print(joined_list[last_index])

last_index может иметь тип float. И проверяя с помощью show_type, это определенно так:

И поскольку это может, mypy не позволит вам использовать возможное значение с плавающей запятой для индексации списка, потому что это приведет к ошибке.

Единственное, что мы могли бы сделать, - это сделать isinstance утверждение на нашей стороне, чтобы убедить mypy:

Но это будет довольно сложно сделать в каждом месте нашего кода, где мы используем add с int. Также мы, как программисты, знаем, что передача двух int всегда вернет только int. Но как нам сказать об этом mypy?

Ответ: используйте @overload. Синтаксис в основном повторяет то, что мы хотели сказать в предыдущем абзаце:

И теперь mypy знает, что add(3, 4) возвращает int.

Обратите внимание, что Python не может гарантировать, что код действительно всегда возвращает int, когда получает int значения. Ваша задача как программиста, предоставляющего эти перегрузки, проверять их правильность. Вот почему в некоторых случаях использование assert isinstance(...) может быть лучше, чем это, но в большинстве случаев @overload работает нормально.

Кроме того, в определениях перегрузки -> int: ... ... в конце - это соглашение о том, когда вы предоставляете заглушки типов для функций и классов, но вы можете технически написать что угодно в качестве тела функции: pass, 42 и т. д. в любом случае будут проигнорированы.

Еще один хороший пример перегрузки:

Type тип

Type - это тип, используемый для ввода классов. Он основан на способе Python определять тип объекта во время выполнения:

Обычно вы используете issubclass(x, int) вместо type(x) == int для проверки поведения, но иногда знание точного типа может помочь, например. в оптимизации.

Поскольку type(x) возвращает класс x, тип класса C Type[C]:

Здесь нам пришлось использовать Any в трех местах, и два из них можно устранить с помощью дженериков, и мы поговорим об этом позже.

Ввод уже существующих проектов

Если вам это нужно, mypy дает вам возможность добавлять типы в ваш проект без изменения исходного исходного кода. Это делается с помощью так называемых «заглушек».

Файлы-заглушки - это файлы, похожие на файлы Python, которые содержат только определения переменных, функций и классов с проверкой типа. Это что-то вроде mypy заголовочного файла.

Вы можете создавать свои собственные заглушки типов, создав файл .pyi:

Теперь запустите mypy в текущей папке (убедитесь, что у вас есть файл __init__.py в папке, если нет, создайте пустой).

$ ls
__init__.py  test.py  test.pyi
$ mypy --strict .
Success: no issues found in 2 source files

Отладка типов - Часть 2

Поскольку мы говорим о проектах и ​​папках, давайте обсудим еще одну ловушку, в которой вы можете оказаться при использовании mypy.

Первый - ПЭП 420.

Мне потребовалось некоторое время, чтобы осознать, что для mypy, чтобы иметь возможность проверять тип папки, папка должна быть модулем.

Допустим, вы попали в такую ​​ситуацию:

$ tree
.
├── test.py
└── utils
    └── foo.py
1 directory, 2 files
$ cat test.py               
from utils.foo import average
print(average(3, 4))
$ cat utils/foo.py 
def average(x: int, y: int) -> float:
    return float(x + y) / 2
$ py test.py      
3.5
$ mypy test.py 
test.py:1: error: Cannot find implementation or library stub for module named 'utils.foo'
test.py:1: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
Found 1 error in 1 file (checked 1 source file)

В чем проблема? Python может найти utils.foo без проблем, почему не может mypy?

Ошибка очень загадочная, но нужно сосредоточить внимание на слове «модуль» в ней. utils.foo должен быть модулем, и для этого в папке utils должен быть __init__.py, даже если он пуст.

$ tree              
.
├── test.py
└── utils
    ├── foo.py
    └── __init__.py
1 directory, 3 files
$ mypy test.py
Success: no issues found in 1 source file

Теперь та же проблема появляется снова, если вы устанавливаете свой пакет через pip, по совершенно другой причине:

$ tree ..
..
├── setup.py
├── src
│   └── mypackage
│       ├── __init__.py
│       └── utils
│           ├── foo.py
│           └── __init__.py
└── test
    └── test.py
4 directories, 5 files
$ cat ../setup.py
from setuptools import setup, find_packages
setup(
    name="mypackage",
    packages = find_packages('src'),
    package_dir = {"":"src"}
)
$ pip install ..
[...]
successfully installed mypackage-0.0.0
$ cat test.py
from mypackage.utils.foo import average
print(average(3, 4))
$ python test.py
3.5
$ mypy test.py
test.py:1: error: Cannot find implementation or library stub for module named 'mypackage.utils.foo'
test.py:1: note: See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports
Found 1 error in 1 file (checked 1 source file)

Что теперь? В каждой папке есть __init__.py, он даже установлен как пакет pip, и код запускается, поэтому мы знаем, что структура модуля правильная. Что дает?

Что ж, оказывается, что пакеты pip не проверяются типом mypy по умолчанию. Такое поведение существует, потому что определения типов по умолчанию не требуются. Пакеты Python не должны проверяться на тип, потому что типы mypy не являются обязательными. Если бы mypy предположил, что каждый пакет имеет подсказки типа, он мог бы показать, возможно, десятки ошибок, потому что пакет не имеет правильных типов или использовал подсказки типа для чего-то другого и т. Д.

Чтобы подписаться на проверку типа вашего пакета, вам необходимо добавить пустой файл py.typed в корневой каталог вашего пакета, а также включить его в качестве метаданных в ваш setup.py:

$ tree ..
..
├── setup.py
├── src
│   └── mypackage
│       ├── __init__.py
│       ├── py.typed
│       └── utils
│           ├── foo.py
│           └── __init__.py
└── test
    └── test.py
4 directories, 6 files
$ cat ../setup.py
from setuptools import setup, find_packages
setup(
    name="mypackage",
    packages = find_packages(
        where = 'src',
    ),
    package_dir = {"":"src"},
    package_data={
        "mypackage": ["py.typed"],
    }
)
$ mypy test.py
Success: no issues found in 1 source file

Есть еще одна третья ловушка, с которой вы иногда можете столкнуться, а именно, если a.py объявляет класс MyClass и импортирует материал из файла b.py, который требует импорта MyClass из a.py для проверки типов.

Это создает цикл импорта, и Python дает вам ImportError. Чтобы избежать этого, просто добавьте блок if typing.TYPE_CHECKING: к оператору импорта в b.py, поскольку ему требуется только MyClass для проверки типа. Кроме того, везде, где вы используете MyClass, добавляйте кавычки: 'MyClass', чтобы Python был доволен.

Менеджеры контекста набора текста

Менеджеры контекста - это способ добавления общей логики настройки и разрыва к частям вашего кода, таких как открытие и закрытие соединений с базой данных, установка веб-сокета и т. Д. На первый взгляд это может показаться простым, но это довольно обширная тема, и если вы никогда не слышали о ней раньше, Энтони освещает ее здесь.

Чтобы определить диспетчер контекста, вам необходимо предоставить два волшебных метода в вашем классе, а именно __enter__ и __exit__. Затем они автоматически вызываются в начале и в конце, если ваш with блок.

Возможно, вы раньше использовали диспетчер контекста: with open(filename) as file: - внизу используется диспетчер контекста. Кстати, давайте напишем нашу собственную реализацию open:

Ввод асинхронных функций

Модуль typing имеет тип утки для всех типов, которых можно ожидать: Awaitable.

Так же, как обычная функция - это Callable, асинхронная функция - это Callable, которая возвращает Awaitable:

Дженерики

Generics (или общие типы) - это языковая функция, которая позволяет вам «передавать типы внутри других типов».

Я лично считаю, что лучше всего это объяснить на примере:

Допустим, у вас есть функция, которая возвращает первый элемент в массиве. Чтобы определить это, нам нужно это поведение:

«Учитывая список типа List[X], мы вернем элемент типа X».

Именно это и есть универсальные типы: определение вашего возвращаемого типа на основе типа ввода.

Общие функции

Мы уже видели make_object из раздела Тип типа, но нам пришлось использовать Any, чтобы иметь возможность поддерживать возврат любого типа объекта, созданного с помощью вызова cls(*args). Но на самом деле нам не нужно этого делать, потому что мы можем использовать дженерики. Вот как бы вы это сделали:

T = TypeVar('T') - это то, как вы объявляете универсальный тип в Python. В определении функции теперь говорится: «Если я дам вам класс, который создает T, вы вернете объект T».

И действительно, reveal_type внизу показывает, что mypy знает, что c является объектом MyClass.

Имя универсального типа T - это еще одно соглашение, вы можете называть его как угодно.

Другой пример: largest, который возвращает самый большой элемент в списке:

Кажется, это хорошо, но mypy недовольна:

$ mypy --strict test.py
test.py:10: error: Unsupported left operand type for > ("T")
Found 1 error in 1 file (checked 1 source file)

Это потому, что вам нужно убедиться, что вы можете делать a < b с объектами, чтобы сравнивать их друг с другом, что не всегда так:

>>> {} < {}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'dict' and 'dict'

Для этого нам нужен Тип утки, который определяет это поведение меньше b.

И хотя в настоящее время Python не имеет такой встроенной функции, существует «виртуальный модуль», поставляемый с mypy, который называется _typeshed. У него много дополнительных типов уток, а также другие особенности, характерные для mypy.

Теперь mypy позволит передавать в эту функцию только списки объектов, которые можно сравнивать друг с другом.

Если вам интересно, почему проверки на < было достаточно, когда в нашем коде используется >, вот как python выполняет сравнения. Я планирую написать об этом статью позже

Обратите внимание, что _typeshed не является фактическим модулем в Python, поэтому вам придется импортировать его, установив флажок if TYPE_CHECKING, чтобы убедиться, что python не дает ModuleNotFoundError. А поскольку SupportsLessThan не будет определен при запуске Python, нам пришлось использовать его как строку при передаче в TypeVar.

На этом этапе вас может заинтересовать, как можно реализовать один из ваших собственных SupportsX типов. Для этого у нас есть еще один раздел ниже: Протоколы.

Общие классы

мы реализовали простой класс Stack в классах набора текста, но он работал только с целыми числами. Но мы можем очень просто заставить его работать для любого типа.

Для этого нам нужно, чтобы mypy понимал, что T означает внутри класса. И для этого нам нужно, чтобы класс расширил Generic[T], а затем предоставил конкретный тип для Stack:

Вы можете передать столько TypeVar на Generic[...], сколько вам нужно, например. чтобы сделать общий словарь, вы можете использовать class Dict(Generic[KT, VT]): ...

Универсальные типы

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

Mypy позволяет нам сделать это очень легко: буквально с помощью задания. Общие части типа выводятся автоматически.

В ближайшее время появится синтаксис, который проясняет, что мы определяем псевдоним типа: Vector: TypeAlias = Tuple[int, int]. Это доступно, начиная с Python 3.10

Точно так же, как мы могли сказать TypeVar T раньше, чтобы он поддерживал только типы, SupportLessThan, мы также можем сделать это

AnyStr - это встроенная ограниченная TypeVar, используемая для определения объединяющего типа для функций, которые принимают str и bytes:

Это отличается от Union[str, bytes], потому что AnyStr представляет любой из этих двух типов одновременно и, следовательно, не concat не принимает первый аргумент как str, а второй как bytes.

Расширенная / рекурсивная проверка типов с Protocol

Мы реализовали FakeFuncs в разделе Типы утки выше и использовали isinstance(FakeFuncs, Callable), чтобы убедиться, что объект действительно был распознан как вызываемый.

Но что, если нам нужны методы типа «утка», отличные от __call__?

Если мы хотим сделать это со всем классом: это становится труднее. Скажем, нам нужен «класс с утиным типом», у которого «есть метод get, который возвращает int» и так далее. По какой-то причине у нас фактически нет доступа к самому классу, например, возможно, мы пишем вспомогательные функции для библиотеки API.

Для этого нам нужно определить Protocol:

Используя это, мы смогли ввести код проверки, даже не нуждаясь в завершенной реализации Api.

Это чрезвычайно мощный. По сути, мы определяем структуру объекта, который нам нужен, а не класс, от которого он принадлежит или от которого наследуется. Это дает нам гибкость набора текста, но в масштабе всего класса.

Помните SupportsLessThan? если вы проверите его реализацию в _typeshed, вот оно:

Да, это вся реализация.

Это также позволяет нам определять определения рекурсивных типов. Самый простой пример - Дерево:

Обратите внимание, что в этом простом примере использование Protocol не обязательно, поскольку mypy может понимать простые рекурсивные структуры. Но для чего-то более сложного, например N-арного дерева, вам нужно использовать Protocol.

Структурные подтипы и все их особенности очень хорошо определены в PEP 544.

Дальнейшее обучение

Если вам интересно узнать больше о типах, у mypy есть отличная документация, и вам обязательно стоит прочитать ее для дальнейшего изучения, особенно раздел Generics.

Я сослался на множество видео Anthony Sottile в этой статье по темам, недоступным для этой статьи. У него есть канал на YouTube, где он выкладывает короткие и очень информативные видеоролики о Python.

Вы можете найти исходный код модуля набора текста здесь, всех типов утки набора внутри модуля _collections_abc и дополнительных в _typeshed в типизированном репо.

Тема, которую я пропустил, говоря о TypeVar и универсальных шаблонах, - это Дисперсия. Это тема теории типов, которая определяет, как подтипы и обобщения соотносятся друг с другом. Если вы хотите узнать об этом подробно, конечно, есть документация в mypy docs, и есть еще два блога, которые я нашел, которые помогают понять эту концепцию, здесь и здесь.

Часть этого материала была перепроверена с использованием официальной документации Python, и, честно говоря, их документация всегда великолепна. Кроме того, на удивление хорошо работает функция Быстрый поиск.

Вы также можете прочитать довольно много PEP, начиная с ключевого слова: PEP 484 и сопровождающего его PEP 526. Другие PEP, упомянутые в статье выше, - это PEP 585, PEP 563, PEP 420 и PEP 544.

И это все!

Я довольно много поработал над этой статьей, сведя все, что я узнал о mypy за последний год, в единый источник знаний. Если у вас есть какие-либо сомнения, мысли или предложения, обязательно оставьте комментарий ниже, и я свяжусь с вами.

Кроме того, если вы дочитали всю статью до сих пор, спасибо! И поздравляю, теперь вы знаете почти все, что вам понадобится, чтобы в будущем писать полностью типизированный код Python. Надеюсь, вам понравилось ✨