Статьи

Python 3 Type Hints и статический анализ

В Python 3.5 появился новый модуль ввода, который обеспечивает стандартную поддержку библиотеки для использования аннотаций функций для необязательных подсказок типов. Это открывает двери для новых и интересных инструментов для статической проверки типов, таких как mypy, и в будущем, возможно, автоматизированной оптимизации на основе типов. Тип подсказок указан в PEP-483 и PEP-484 .

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

Типовые подсказки строятся поверх аннотаций функций. Вкратце, аннотации функций позволяют вам аннотировать аргументы и возвращать значение функции или метода с произвольными метаданными. Подсказки типов представляют собой особый случай аннотаций функций, которые специально аннотируют аргументы функций и возвращаемое значение стандартными сведениями о типах. Аннотации функций в целом и подсказки типов в частности не являются обязательными. Давайте посмотрим на быстрый пример:

« `python def reverse_slice (text: str, start: int, end: int) -> str: возвращаемый текст [start: end] [:: — 1]

reverse_slice (‘abcdef’, 3, 5) ‘ed’ « `

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

python reverse_slice.__annotations {'end': int, 'return': str, 'start': int, 'text': str}

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

« `python def reverse_slice (текст: float, start: str, end: bool) -> dict: вернуть текст [start: end] [:: — 1]

reverse_slice (‘abcdef’, 3, 5) ‘ed’ « `

Как видите, код ведет себя одинаково, независимо от подсказок типа.

OK. Тип подсказки не являются обязательными. Типовые подсказки полностью игнорируются Python. Какой в ​​этом смысл? Ну, есть несколько веских причин:

  • статический анализ
  • Поддержка IDE
  • стандартная документация

Я углублюсь в статический анализ с Mypy позже. Поддержка IDE уже началась с поддержки PyCharm 5 для подсказок типов. Стандартная документация отлично подходит для разработчиков, которые могут легко определить тип аргументов и возвращаемое значение, просто взглянув на сигнатуру функции, а также на автоматические генераторы документации, которые могут извлечь информацию о типе из подсказок.

Модуль ввода содержит типы, предназначенные для поддержки подсказок типов. Почему бы просто не использовать существующие типы Python, такие как int, str, list и dict? Вы можете определенно использовать эти типы, но из-за динамической типизации Python, помимо базовых типов, вы не получаете много информации. Например, если вы хотите указать, что аргумент может быть отображением между строкой и целым числом, нет способа сделать это со стандартными типами Python. С помощью модуля ввода это так же просто, как:

python Mapping[str, int]

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

« `python из набора импорта списка, Dict, Union

def foo (a: List [Dict [str, int]], b: Union [str, int]) -> int: «» »Вывести список словарей и вернуть количество словарей« »», если isinstance (b, str): b = int (b) для i в диапазоне (b): print (a)

x = [dict (a = 1, b = 2), dict (c = 3, d = 4)] foo (x, ‘3’)

[{‘b’: 2, ‘a’: 1}, {‘d’: 4, ‘c’: 3}] [{‘b’: 2, ‘a’: 1}, {‘d’: 4 , ‘c’: 3}] [{‘b’: 2, ‘a’: 1}, {‘d’: 4, ‘c’: 3}] « `

Давайте посмотрим на некоторые из наиболее интересных типов из модуля ввода.

Тип Callable позволяет вам указывать функцию, которая может быть передана в качестве аргумента или возвращена в результате, так как Python рассматривает функции как первоклассных граждан. Синтаксис для вызываемых элементов заключается в предоставлении массива типов аргументов (снова из модуля ввода), за которым следует возвращаемое значение. Если это сбивает с толку, вот пример:

« `python def do_something_fancy (данные: Set [float], on_error: Callable [[Exception, int], None]):…

« `

Функция обратного вызова on_error указывается как функция, которая принимает в качестве аргументов исключение и целое число и ничего не возвращает.

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

Тип Union, который вы видели ранее, полезен, когда аргумент может иметь несколько типов, что очень часто встречается в Python. В следующем примере функция verify_config () принимает аргумент config, который может быть либо объектом Config, либо именем файла. Если это имя файла, он вызывает другую функцию, чтобы проанализировать файл в объект Config и вернуть его.

« `python def verify_config (config: Union [str, Config]): если isinstance (config, str): config = parse_config_file (config)…

def parse_config_file (имя файла: str) -> Config:…

« `

Тип Optional означает, что аргумент также может быть None. Optional[T] эквивалентен Union[T, None]

Есть еще много типов, которые обозначают различные возможности, такие как Iterable, Iterator, Reversible, SupportsInt, SupportsFloat, Sequence, MutableSequence и IO. Посмотрите документацию по модулю набора для полного списка.

Главное, что вы можете задавать тип аргументов очень детальным образом, который поддерживает систему типов Python с высокой точностью, а также допускает обобщенные и абстрактные базовые классы.

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

« `класс Python A: def merge (другое: A) -> A:…

NameError: имя ‘A’ не определено « `

Что произошло? Класс A еще не определен, когда подсказка типа для его метода merge () проверяется Python, поэтому класс A нельзя использовать в этой точке (напрямую). Решение довольно простое, и я видел его раньше в SQLAlchemy. Вы просто указываете подсказку типа в виде строки. Python поймет, что это прямая ссылка, и поступит правильно:

python class A: def merge(other: 'A' = None) -> 'A': ...

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

« `Python Data = Dict [int, Sequence [Dict [str, Необязательный [List [float]]]]

def foo (данные: данные) -> bool:… « `

Модуль ввода предоставляет функцию get_type_hints () , которая предоставляет информацию о типах аргументов и возвращаемом значении. Хотя атрибут annotations возвращает подсказки типа, поскольку они являются просто аннотациями, я все же рекомендую использовать функцию get_type_hints (), поскольку она разрешает прямые ссылки. Кроме того, если вы укажете значение по умолчанию None для одного из аргументов, функция get_type_hints () автоматически вернет свой тип как Union [T, NoneType], если вы только что указали T. Давайте посмотрим на разницу, используя метод A.merge () определено ранее:

« `python print (A.merge. аннотации )

{‘other’: ‘A’, ‘return’: ‘A’} « `

Атрибут annotations просто возвращает значение аннотации как есть. В данном случае это просто строка «A», а не объект класса A, на который «A» представляет собой прямую ссылку.

« `python print (get_type_hints (A.merge))

{‘return’: <class ‘ main .A’>, ‘other’: typing.Union [ main .A, NoneType]} « `

Функция get_type_hints () преобразовала тип другого аргумента в объединение A (класса) и NoneType из-за аргумента None по умолчанию. Тип возвращаемого значения также был преобразован в класс А.

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

Для этого модуль ввода обеспечивает два декоратора: @no_type_check и @no_type_check_decorator . Декоратор @no_type_check может применяться либо к классу, либо к функции. Он добавляет атрибут no_type_check к функции (или к каждому методу класса). Таким образом, средства проверки типов будут игнорировать аннотации, которые не являются подсказками типов.

Это немного громоздко, потому что если вы пишете библиотеку, которая будет широко использоваться, вы должны предполагать, что будет использоваться средство проверки типов, и если вы хотите аннотировать свои функции не типичными подсказками, вы также должны украсить их с помощью @no_type_check ,

Распространенным сценарием при использовании обычных аннотаций функций также является наличие декоратора, который работает над ними. В этом случае вы также хотите отключить проверку типов. Один из вариантов — использовать декоратор @no_type_check в дополнение к вашему декоратору, но он устареет. Вместо этого @no_Type_check_decorator можно использовать для украшения вашего декоратора, чтобы он также вел себя как @no_type_check (добавляет атрибут no_type_check ).

Позвольте мне проиллюстрировать все эти концепции. Если вы попытаетесь get_type_hint () (как это делает любая проверка типов) для функции, аннотированной обычной строковой аннотацией, get_type_hints () будет интерпретировать ее как прямую ссылку:

« `python def f (a: ‘некоторая аннотация’): pass

печати (get_type_hints (е))

SyntaxError: ForwardRef должен быть выражением — получил «некоторую аннотацию» « `

Чтобы избежать этого, добавьте декоратор @no_type_check, и get_type_hints просто возвращает пустой dict, а атрибут __annotations__ возвращает аннотации:

« `python @no_type_check def f (a: ‘некоторая аннотация’): pass

print (get_type_hints (f)) {}

print (f. annotations ) {‘a’: ‘некоторая аннотация’} « `

Теперь предположим, что у нас есть декоратор, который печатает диктанты. Вы можете украсить его с помощью @no_Type_check_decorator, а затем украсить функцию, не беспокоясь о проверке некоторых типов, вызывающей get_type_hints () и сбивающейся с толку. Это, вероятно, лучшая практика для каждого декоратора, который работает с аннотациями. Не забывайте @ functools.wraps , иначе аннотации не будут скопированы в оформленную функцию, и все развалится. Это подробно описано в аннотациях функций Python 3 .

python @no_type_check_decorator def print_annotations(f): @functools.wraps(f) def decorated(*args, **kwargs): print(f.__annotations__) return f(*args, **kwargs) return decorated

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

« `python @print_annotations def f (a: ‘некоторая аннотация’): pass

f (4) {‘a’: ‘некоторая аннотация’} « `

Вызов get_type_hints () также безопасен и возвращает пустой dict.

python print(get_type_hints(f)) {}

Mypy — это статическая программа проверки типов, которая послужила вдохновением для подсказок типов и модуля ввода. Сам Гвидо ван Россум является автором PEP-483 и соавтором PEP-484.

Mypy находится в очень активной разработке, и на момент написания этой статьи пакет на PyPI устарел и не работает с Python 3.5. Чтобы использовать Mypy с Python 3.5, получите последнюю версию из репозитория Mypy на GitHub . Это так просто, как:

bash pip3 install git+git://github.com/JukkaL/mypy.git

После установки Mypy вы можете просто запустить Mypy в своих программах. Следующая программа определяет функцию, которая ожидает список строк. Затем он вызывает функцию со списком целых чисел.

« `python от ввода списка импорта

def case_insensitive_dedupe (data: List [str]): «» »Преобразует все значения в нижний регистр и удаляет дубликаты» »» список возврата (set (x.lower () для x в данных))

print (case_insensitive_dedupe ([1, 2])) « `

При запуске программы она явно завершается с ошибкой во время выполнения:

plain python3 dedupe.py Traceback (most recent call last): File "dedupe.py", line 8, in <module> print(case_insensitive_dedupe([1, 2, 3])) File "dedupe.py", line 5, in case_insensitive_dedupe return list(set(x.lower() for x in data)) File "dedupe.py", line 5, in <genexpr> return list(set(x.lower() for x in data)) AttributeError: 'int' object has no attribute 'lower'

В чем проблема с этим? Проблема в том, что даже в этом очень простом случае неясно, какова основная причина. Это проблема типа входа? Или, возможно, сам код неверен и не должен пытаться вызвать метод lower () для объекта ‘int’. Другая проблема заключается в том, что если у вас нет 100% тестового покрытия (и, честно говоря, никто из нас не делает), то такие проблемы могут скрываться в каком-то непроверенном, редко используемом пути кода и обнаруживаться в худшее время в производстве.

Статическая типизация, поддерживаемая подсказками типов, дает вам дополнительную сеть безопасности, гарантируя, что вы всегда вызываете свои функции (отмеченные подсказками типов) с правильными типами. Вот вывод Mypy:

plain (N) > mypy dedupe.py dedupe.py:8: error: List item 0 has incompatible type "int" dedupe.py:8: error: List item 1 has incompatible type "int" dedupe.py:8: error: List item 2 has incompatible type "int"

Это просто, указывает непосредственно на проблему и не требует выполнения большого количества тестов. Еще одно преимущество статической проверки типов заключается в том, что если вы фиксируете ее, вы можете пропустить динамическую проверку типов, за исключением случаев анализа внешних входных данных (чтения файлов, входящих сетевых запросов или ввода данных пользователем). Это также создает большую уверенность, насколько рефакторинг идет.

Подсказки по типу и модуль ввода являются совершенно необязательными дополнениями к выразительности Python. Хотя они могут и не понравиться всем, для больших проектов и больших команд они могут быть незаменимы. Доказательством тому является то, что большие команды уже используют статическую проверку типов. Теперь, когда информация о типах стандартизирована, вам будет проще делиться кодом, утилитами и инструментами, которые ее используют. Среды IDE, такие как PyCharm, уже используют это для обеспечения лучшего опыта разработчика.