Перейти к содержимому

Typer python как пользоваться

  • автор:

Поддержка аннотации типов в Python

Внимание. Интерпретатор Python не проверяет и не принимает во внимание аннотации типов функций и переменных. Их могут использовать сторонние инструменты, такие как средства проверки типов, IDE, линтеры и т. д.

Модуль typing обеспечивает поддержку выполнения аннотации типов. Наиболее фундаментальная поддержка состоит из типов typing.Any , typing.Union , typing.Tuple , typing.Callable , typing.TypeVar и typing.Generic .

Функция ниже принимает и возвращает строку и аннотируется следующим образом:

def greeting(name: str) -> str: return 'Hello ' + name 

В функции greeting() ожидается, что имя аргумента будет иметь тип str и возвращаемый тип ожидается str . Подтипы принимаются в качестве аргументов типов. Например переменная списка lst , состоящий из значений int будет аннотироваться как lst: list[int] .

Примечание. Модуль typing определяет несколько типов, которые являются подклассами уже существующих классов стандартной библиотеки, и которые также расширяют typing.Generic для поддержки типов переменных внутри [] скобок. С версии Python 3.9, классы стандартной библиотеки были расширены для поддержки синтаксиса [] и эти типы стали избыточными.

Избыточные типы устарели в Python 3.9, но интерпретатор не будет выдавать предупреждений DEPRECATED . Ожидается, что средства проверки типов будут отмечать устаревшие типы, когда проверяемый код нацелен на версию Python 3.9 или новее.

Устаревшие типы будут удалены из модуля typing в первой версии Python, выпущенной через 5 лет после выпуска Python 3.9.0.

Содержание:

  • Использование псевдонимов типов;
  • Аннотация отдельных подтипов typing.NewType ;
  • Аннотирование вызываемых объектов;
  • Аннотация универсальных типов;
    • Аннотация универсальных типов, определяемые пользователем;

    Использование псевдонимов типов в модуле typing .

    Начиная с версии Python 3.12, псевдоним типа определяется с помощью оператора type (добавлен в Python 3.12), который создает экземпляр TypeAliasType . В этом примере Vector и list[float] будут обрабатываться средствами проверки статического типа одинаково:

    # синтаксис ДО Python 3.12 # Vector = list[float] # синтаксис начиная с Python 3.12 type Vector = list[float] def scale(scalar: float, vector: Vector) -> Vector: return [scalar * num for num in vector] # Тип будет проверяться; # список значений `float` квалифицируется как вектор. new_vector = scale(2.0, [1.0, -4.2, 5.4]) 

    Псевдонимы типов полезны для упрощения сигнатур сложных типов. Например:

    from collections.abc import Sequence # синтаксис ДО Python 3.12 # ConnectionOptions = dict[str, str] # Address = tuple[str, int] # Server = tuple[Address, ConnectionOptions] # синтаксис начиная с Python 3.12 type ConnectionOptions = dict[str, str] type Address = tuple[str, int] type Server = tuple[Address, ConnectionOptions] def broadcast_message(message: str, servers: Sequence[Server]) -> None: . # Средство проверки статического типа будет рассматривать # подпись предыдущего типа как точно эквивалентную этой. def broadcast_message( message: str, servers: Sequence[tuple[tuple[str, int], dict[str, str]]]) -> None: 

    Для обратной совместимости псевдонимы типов также можно создавать с помощью простого присваивания:

    Vector = list[float] 

    Или пометить typing.TypeAlias , чтобы было ясно, что это псевдоним типа, а не обычное присвоение переменной:

    from typing import TypeAlias Vector: TypeAlias = list[float] 

    Аннотация отдельных типов typing.NewType .

    Используйте вспомогательный класс typing.NewType() для создания отдельных типов:

    from typing import NewType UserId = NewType('UserId', int) some_id = UserId(524313) 

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

    def get_user_name(user_id: UserId) -> str: . # Тип будет проверяться user_a = get_user_name(UserId(42351)) # тип не проверяется; `int` не является UserId user_b = get_user_name(-1) 

    По-прежнему можно выполнять все операции с int с переменной типа UserId , но результат всегда будет иметь тип int . Это позволяет передать UserId везде, где можно ожидать int , но предотвратит случайное создание UserId недопустимым способом:

    # 'output' имеет тип 'int', а не 'UserId' output = UserId(23413) + UserId(54341) 

    Обратите внимание, что эти проверки выполняются только средством проверки типов. Во время выполнения оператор Derived= NewType(‘Derived’, Base) сделает Derived функцией, которая немедленно возвращает любой переданный ей параметр. Это означает, что выражение Derived(some_value) не создает новый класс и не вводит никаких накладных расходов, помимо обычных вызовов функции.

    Точнее, выражение some_value is Derived(some_value) всегда истинно во время выполнения.

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

    from typing import NewType UserId = NewType('UserId', int) # Не работает во время выполнения и не проверяет тип class AdminUserId(UserId): pass 

    Однако можно создать typing.NewType() на основе производного NewType :

    from typing import NewType UserId = NewType('UserId', int) ProUserId = NewType('ProUserId', UserId) 

    и проверка типов для ProUserId будет работать так, как ожидалось.

    Примечание. Напомним, что использование псевдонима типа объявляет, что два типа эквивалентны друг другу. Выполнение Alias ​​= Original заставит средство проверки статического типа обрабатывать псевдоним как полностью эквивалентный оригиналу во всех случаях. Это полезно, когда необходимо упростить сигнатуры сложных типов.

    Напротив, typing.NewType объявляет один тип подтипом другого. Выполнение Derived = NewType(‘Derived’, Original) заставит средство проверки аннотации типов рассматривать Derived как подкласс Original , это означает, что значение типа Original не может использоваться в местах, где ожидается значение типа Derived . Такое поведение полезно, когда необходимо предотвратить логические ошибки с минимальными затратами времени выполнения.

    Изменено в Python 3.10: NewType теперь является классом, а не функцией. При вызове NewType вместо обычной функции возникают некоторые дополнительные затраты времени выполнения. В Python 3.11.0 эти затраты будут снижены.

    Изменено в версии 3.11: Производительность вызова NewType восстановлена ​​до уровня Python 3.9.

    Аннотирование вызываемых объектов.

    Функции или другие вызываемые объекты можно аннотировать с помощью Collections.abc.Callable или typing.Callable . Callable[[int], str] означает функцию, которая принимает один параметр типа int и возвращает строку str .

    from collections.abc import Callable, Awaitable def feeder(get_next_item: Callable[[], str]) -> None: . # тело функции def async_query(on_success: Callable[[int], None], on_error: Callable[[int, Exception], None]) -> None: . # тело функции async def on_update(value: str) -> None: . # тело функции callback: Callable[[str], Awaitable[None]] = on_update 

    Синтаксис подписки Callable() всегда должен использоваться ровно с двумя значениями: списком аргументов и типом возвращаемого значения. Список аргументов должен быть списком типов, ParamSpec , Concatenate или многоточием. Возвращаемый тип должен быть одного типа.

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

    def concat(x: str, y: str) -> str: return x + y x: Callable[. , str] x = str # OK x = concat # Also OK 

    Callable() не может выражать сложные сигнатуры, такие как функции, которые принимают переменное количество аргументов, overloaded functions или функции, которые имеют только ключевые аргументы. Однако эти сигнатуры могут быть выражены путем определения класса typing.Protocol с методом __call__() :

    from collections.abc import Iterable from typing import Protocol class Combiner(Protocol): def __call__(self, *vals: bytes, maxlen: int | None = None) -> list[bytes]: . def batch_proc(data: Iterable[bytes], cb_results: Combiner) -> bytes: for item in data: . def good_cb(*vals: bytes, maxlen: int | None = None) -> list[bytes]: . def bad_cb(*vals: bytes, maxitems: int | None) -> list[bytes]: . batch_proc([], good_cb) # OK # Ошибка! Аргумент 2 имеет несовместимый тип из-за # другого имени и вида в обратном вызове batch_proc([], bad_cb) 

    Вызываемые объекты, которые принимают другие вызываемые объекты в качестве аргументов, могут указывать на то, что их типы параметров зависят друг от друга с помощью ParamSpec . Кроме того, если этот вызываемый объект добавляет или удаляет аргументы из других вызываемых объектов, то может использоваться оператор typing.Concatenate . Они принимают форму Callable[ParamSpecVariable, ReturnType] и Callable[Concatenate[Arg1Type, Arg2Type, . ParamSpecVariable], ReturnType] соответственно.

    Изменено в версии Python 3.10: Callable теперь поддерживает ParamSpec и Concatenate .

    Также смотрите документацию для typing.ParamSpec и typing.Concatenate ]typing.Concatenate, в которой приведены примеры использования в Callable .

    Аннотация универсальных типов.

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

    from collections.abc import Mapping, Sequence class Employee: . # `Sequence[Employee]` указывает, что все элементы в последовательности # должны быть экземплярами `Employee`. # `Mapping[str, str]` указывает, что все ключи и все значения в сопоставлении # должны быть строками. def notify_by_email(employees: Sequence[Employee], overrides: Mapping[str, str]) -> None: . 

    Универсальные функции и классы могут быть аннотированы с помощью синтаксиса списка параметров типа:

    from collections.abc import Sequence # Функция является универсальной для TypeVar "T" def first[T](l: Sequence[T]) -> T: return l[0] 

    Или с помощью фабрики TypeVar напрямую:

    from collections.abc import Sequence from typing import TypeVar # Объявляем переменную типа "U" U = TypeVar('U') # Функция является универсальной по сравнению с TypeVar "U", def second(l: Sequence[U]) -> U: return l[1] 

    Определяемые пользователем универсальные типы.

    В Python 3.12 пользовательский класс можно определить как универсальный класс.

    from logging import Logger class LoggedVar[T]: def __init__(self, value: T, name: str, logger: Logger) -> None: self.name = name self.logger = logger self.value = value def set(self, new: T) -> None: self.log('Set ' + repr(self.value)) self.value = new def get(self) -> T: self.log('Get ' + repr(self.value)) return self.value def log(self, message: str) -> None: self.logger.info('%s: %s', self.name, message) 

    Этот синтаксис указывает, что класс LoggedVar аннотирован вокруг одного type variable T . Это также делает T допустимым типом в теле класса.

    from typing import TypeVar, Generic from logging import Logger T = TypeVar('T') class LoggedVar(Generic[T]): def __init__(self, value: T, name: str, logger: Logger) -> None: self.name = name self.logger = logger self.value = value def set(self, new: T) -> None: self.log('Set ' + repr(self.value)) self.value = new def get(self) -> T: self.log('Get ' + repr(self.value)) return self.value def log(self, message: str) -> None: self.logger.info('%s: %s', self.name, message) 

    Generic[T] как базовый класс определяет, что класс LoggedVar принимает единственный параметр типа T . Это также делает T действительным как тип в теле класса.

    Базовый класс typing.Generic определяет __class_getitem__() , так что LoggedVar[t] действителен как тип:

    from collections.abc import Iterable def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None: for var in vars: var.set(0) 

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

    from typing import TypeVar, Generic, Sequence class WeirdTrio[T, B: Sequence[bytes], S: (int, str)]: . OldT = TypeVar('OldT', contravariant=True) OldB = TypeVar('OldB', bound=Sequence[bytes], covariant=True) OldS = TypeVar('OldS', int, str) class OldWeirdTrio(Generic[OldT, OldB, OldS]): . 

    Все аргументы переменной типа для typing.Generic должны быть разными. Таким образом, следующее неверно:

    from typing import TypeVar, Generic . T = TypeVar('T') class Pair(Generic[T, T]): # INVALID . 

    Вы можете использовать множественное наследование с typing.Generic :

    from collections.abc import Sized from typing import TypeVar, Generic T = TypeVar('T') class LinkedList(Sized, Generic[T]): . 

    При наследовании от универсальных классов можно исправить некоторые переменные типа:

    from collections.abc import Mapping from typing import TypeVar T = TypeVar('T') class MyDict(Mapping[str, T]): . 

    В этом случае MyDict имеет единственный параметр T .

    Использование универсального класса без указания параметров типа предполагает typing.Any для каждой позиции. В следующем примере MyIterable не является универсальным, а неявно наследуется от Iterable[Any] :

    from collections.abc import Iterable # Такой же как `Iterable[Any]` class MyIterable(Iterable): 

    В Python 3.12 поддерживаются определенные пользователем псевдонимы универсального типа. Примеры:

    from collections.abc import Iterable type Response[S] = Iterable[S] | int # Возвращаемый тип здесь такой же, как Iterable[str] | int def response(query: str) -> Response[str]: . type Vec[T] = Iterable[tuple[T, T]] # То же, что и Iterable[tuple[T, T]] def inproduct[T: (int, float, complex)](v: Vec[T]) -> T: return sum(x*y for x, y in v) 

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

    from collections.abc import Iterable from typing import TypeVar S = TypeVar("S") Response = Iterable[S] | int 

    Изменено в Python 3.7: typing.Generic больше не имеет собственного метакласса.

    Изменено в Python 3.12: Синтаксическая поддержка универсальных шаблонов и псевдонимов типов появилась в Python 3.12. Раньше универсальные классы должны были явно наследоваться от Generic или содержать переменную типа в одной из своих баз.

    Определяемые пользователем универсальные типы для аннотирования параметров также поддерживаются с помощью переменных спецификации параметров в форме [**P] . Поведение согласуется с описанными выше переменными типа, поскольку переменные спецификации параметров обрабатываются модулем ввода как специализированная переменная типа. Единственным исключением из этого является то, что список типов может быть использован для замены параметра SPEC :

    # T - это переменная типа; # P - это ParamSpec >>> class Z[T, **P]: . . >>> Z[int, [dict, float]] # __main__.Z[int, [dict, float]] 

    Классы, универсальные для ParamSpec , также можно создавать с использованием явного наследования от Generic . В этом случае ** не используется:

    >>> from typing import Generic, ParamSpec, TypeVar >>> T = TypeVar('T') >>> P = ParamSpec('P') >>> class Z(Generic[T, P]): . . >>> Z[int, [dict, float]] # __main__.Z[int, (, )] 

    Еще одно различие между TypeVar и ParamSpec заключается в том, что универсальный вариант только с одной переменной спецификации параметра будет принимать списки параметров в формах X[[Type1, Type2, . ]] , а также X[Type1, Type2, . ] по эстетическим соображениям. Внутри последние преобразуются в первые и, таким образом, эквивалентны:

    >>> class X[**P]: . . >>> X[int, str] # __main__.X[[int, str]] >>> X[[int, str]] # __main__.X[[int, str]] 

    Обратите внимание, что универсальные типы с typing.ParamSpec , в некоторых случаях, могут не иметь правильных __parameters__ после подстановки, потому что они предназначены в первую очередь для проверки статического типа.

    Изменено в Python 3.10: Generic теперь можно параметризовать с помощью выражений ParamSpec .

    Определенный пользователем универсальный класс может иметь ABC в качестве базовых классов без конфликта метаклассов. Универсальные метаклассы не поддерживаются. Результат параметризации универсальных шаблонов кэшируется, и большинство типов в модуле типизации являются хэшируемыми и сопоставимыми по равенству.

    Применение аннотации typing.Any .

    Особый тип аннотации typing.Any . Средство проверки статического типа будет рассматривать каждый тип как совместимый с Any и Any как совместимый с каждым типом.

    Это означает, что можно выполнить любую операцию или вызов метода для значения типа Any и присвоить его любой переменной:

    from typing import Any a = None # type: Any a = [] # OK a = 2 # OK s = '' # type: str s = a # OK def foo(item: Any) -> int: # Проверка типов; 'item' может быть # любого типа, и этот тип может # иметь метод 'bar' item.bar() . 

    Обратите внимание, что при присвоении значения типа Any более точному типу, проверка типов выполняться не будет. Например, средство проверки аннотации не сообщило об ошибке при присвоении a параметру s , даже если s был объявлен как имеющий тип str и получил значение int во время выполнения!

    Кроме того, все функции без возвращаемого типа или типов параметров неявно по умолчанию будут использовать Any:

    def legacy_parser(text): . return data # Статическая проверка типов будет # рассматривать вышеприведенное # как имеющее ту же сигнатуру, что и: def legacy_parser(text: Any) -> Any: . return data 

    Такое поведение позволяет использовать typing.Any в качестве аварийного выхода, когда необходимо смешивать динамически и статически типизированный код.

    Сравните поведение typing.Any с поведением object : Подобно Any , каждый тип является подтипом object . Однако, в отличие от Any , обратное неверно: object не является подтипом любого другого типа.

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

    def hash_a(item: object) -> int: # Не проходит; у `object` нет `magic` метода. item.magic() . def hash_b(item: Any) -> int: # Проверка типа прошла успешно item.magic() . # Проверка прошла, поскольку `ints` и # `strs` являются подклассами объекта hash_a(42) hash_a("foo") # Проверка прошла, т.к. `Any` # совместим со всеми типами hash_b(42) hash_b("foo") 

    Используйте object , для указания значения любого типа безопасным способом, а typing.Any , для динамически типизируемых значений.

    Номинальные и структурные подтипы.

    Первоначально определи систему статических типов Python, как использование номинальных подтипов. Это означает, что класс A разрешен там, где ожидается класс B тогда и только тогда, когда A является подклассом B .

    Это требование ранее также применялось к абстрактным базовым классам, таким как Iterable . Проблема с этим подходом заключается в том, что класс должен быть явно помечен для их поддержки, что не является питоническим и отличается от того, что обычно делают в идиоматическом динамически типизированном коде Python. Например:

    from collections.abc import Sized, Iterable, Iterator class Bucket(Sized, Iterable[int]): . def __len__(self) -> int: . def __iter__(self) -> Iterator[int]: . 

    Однако пользователи могут писать приведенный выше код без явных базовых классов в определении класса, что позволяет средствам проверки статических типов неявно рассматривать Bucket как подтип Sized и Iterable[int] . Это называется структурным подтипом или статической утиной типизацией:

    from collections.abc import Iterator, Iterable # Примечание: нет базовых классов class Bucket: . def __len__(self) -> int: . def __iter__(self) -> Iterator[int]: . def collect(items: Iterable[int]) -> int: . result = collect(Bucket()) # Проходит проверку типа 

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

    • КРАТКИЙ ОБЗОР МАТЕРИАЛА.
    • Аннотация Any модуля typing
    • Аннотации Never и NoReturn модуля typing
    • Тип аннотации TypeAlias модуля typing
    • Аннотация LiteralString модуля typing
    • Аннотация Self модуля typing
    • Аннотации Required и NotRequired модуля typing
    • Тип аннотации Union модуля typing
    • Тип аннотации Optional модуля typing
    • Тип аннотации Tuple() модуля typing
    • Тип аннотации Callable() модуля typing
    • Тип аннотации Concatenate модуля typing
    • Класс ParamSpec модуля typing
    • Тип аннотации TypeGuard модуля typing
    • Класс Type() модуля typing
    • Тип аннотации Literal модуля typing
    • Тип аннотации ClassVar модуля typing
    • Тип аннотации Final() модуля typing
    • Тип аннотации Annotated модуля typing
    • Тип аннотации Generic модуля typing
    • Тип аннотации TypeVar модуля typing
    • Аннотация TypeVarTuple модуля typing
    • Тип аннотации Unpack модуля typing
    • Тип аннотации AnyStr модуля typing
    • Тип аннотации Protocol() модуля typing
    • Декоратор @runtime_checkable модуля typing
    • Тип аннотации NamedTuple модуля typing
    • Класс NewType модуля typing
    • Тип аннотации TypedDict() модуля typing
    • Типы аннотаций коллекций модуля typing
    • Аннотация абстрактных базовых классов
    • Функции и декораторы модуля typing
    • Помощники самоанализа модуля typing
    • ParamSpecArgs и ParamSpecKwargs модуля typing
    • Класс TypeAliasType модуля typing

    Введение в аннотации типов Python

    Автор иллюстрации — Magdalena Tomczyk

    Python — язык с динамической типизацией и позволяет нам довольно вольно оперировать переменными разных типов. Однако при написании кода мы так или иначе предполагаем переменные каких типов будут использоваться (это может быть вызвано ограничением алгоритма или бизнес логики). И для корректной работы программы нам важно как можно раньше найти ошибки, связанные с передачей данных неверного типа.

    Сохраняя идею динамической утиной типизации в современных версиях Python (3.6+) поддерживает аннотации типов переменных, полей класса, аргументов и возвращаемых значений функций:

    • PEP 3107 — Function Annotations
    • PEP 484 — Type Hints
    • PEP 526 — Syntax for Variable Annotations
    • Пакет typing

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

    Меня зовут Тихонов Андрей и я занимаюсь backend-разработкой в Lamoda.

    В этой статье я хочу объяснить основы использования аннотаций типов и рассмотреть типичные примеры, реализуемые аннотациями из пакета typing .

    Инструменты, поддерживающие аннотации

    Аннотации типов поддерживаются многими IDE для Python, которые выделяют некорректный код или выдают подсказки в процессе набора текста.

    Например, так это выглядит в Pycharm:

    Так же аннотации типов обрабатываются и консольными линтерами.

    Вот вывод pylint:

    $ pylint example.py ************* Module example example.py:7:6: E1101: Instance of 'int' has no 'startswith' member (no-member)

    А вот для того же файла что нашел mypy:

    $ mypy example.py example.py:7: error: "int" has no attribute "startswith" example.py:10: error: Unsupported operand types for // ("str" and "int")

    Поведение разных анализаторов может отличаться. Например, mypy и pycharm по разному обрабатывают смену типа переменной. Далее в примерах я буду ориентироваться на вывод mypy.

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

    Основы

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

    В простейшем случае аннотация содержит непосредственно ожидаемый тип. Более сложные кейсы будут рассмотрены ниже. Если в качестве аннотации указан базовый класс, допустимо передача экземпляров его наследников в качестве значений. Однако использовать можно только те возможности, что реализованы в базовом классе.

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

    price: int = 5 title: str

    Параметры функции аннотируются так же как переменные, а возвращаемое значение указывается после стрелки -> и до завершающего двоеточия. Например,

    def indent_right(s: str, width: int) -> str: return " " * (max(0, width - len(s))) + s

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

    class Book: title: str author: str def __init__(self, title: str, author: str) -> None: self.title = title self.author = author b: Book = Book(title='Fahrenheit 451', author='Bradbury')

    Кстати, при использовании dataclass типы полей необходимо указывать именно в классе. Подробнее про dataclass

    Встроенные типы

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

    Optional

    Если вы пометите переменную типом int и попытаетесь присвоить ей None , будет ошибка:

    Incompatible types in assignment (expression has type «None», variable has type «int»)

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

    from typing import Optional amount: int amount = None # Incompatible types in assignment (expression has type "None", variable has type "int") price: Optional[int] price = None

    Any

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

    unknown_item: Any = 1 print(unknown_item) print(unknown_item.startswith("hello")) print(unknown_item // 0)

    Может возникнуть вопрос, почему не использовать object ? Однако в этом случае предполагается, что хоть передан может быть любой объект, обращаться с ним можно только как с экземпляром object .

    unknown_object: object print(unknown_object) print(unknown_object.startswith("hello")) # error: "object" has no attribute "startswith" print(unknown_object // 0) # error: Unsupported operand types for // ("object" and "int")

    Union

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

    def hundreds(x: Union[int, float]) -> int: return (int(x) // 100) % 10 hundreds(100.0) hundreds(100) hundreds("100") # Argument 1 to "hundreds" has incompatible type "str"; expected "Union[int, float]"

    Кстати, аннотация Optional[T] эквивалентна Union[T, None] , хотя такая запись и не рекомендуется.

    Коллекции

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

    Списки

    Для того, чтобы указать, что переменная содержит список можно использовать тип list в качестве аннотации. Однако если хочется конкретизировать, какие элементы содержит список, он такая аннотация уже не подойдёт. Для этого есть typing.List . Аналогично тому, как мы указывали тип опциональной переменной, мы указываем тип элементов списка в квадратных скобках.

    titles: List[str] = ["hello", "world"] titles.append(100500) # Argument 1 to "append" of "list" has incompatible type "int"; expected "str" titles = ["hello", 1] # List item 1 has incompatible type "int"; expected "str" items: List = ["hello", 1]

    Предполагается, что список содержит неопределенное количество однотипных элементов. Но при этом нет ограничений на аннотацию элемента: можно использовать Any , Optional , List и другие. Если тип элемента не указан, предполагается, что это Any .

    Кроме списка аналогичные аннотации есть для множеств: typing.Set и typing.FrozenSet .

    Кортежи

    Кортежи в отличие от списков часто используются для разнотипных элементов. Синтаксис похож с одним отличием: в квадратных скобках указывается тип каждого элемента кортежа по отдельности.

    Если же планируется использовать кортеж аналогично списку: хранить неизвестное количество однотипных элементов, можно воспользоваться многоточием ( . ).

    Аннотация Tuple без указания типов элементов работает аналогично Tuple[Any, . ]

    price_container: Tuple[int] = (1,) price_container = ("hello") # Incompatible types in assignment (expression has type "str", variable has type "Tuple[int]") price_container = (1, 2) # Incompatible types in assignment (expression has type "Tuple[int, int]", variable has type "Tuple[int]") price_with_title: Tuple[int, str] = (1, "hello") prices: Tuple[int, . ] = (1, 2) prices = (1, ) prices = (1, "str") # Incompatible types in assignment (expression has type "Tuple[int, str]", variable has type "Tuple[int, . ]") something: Tuple = (1, 2, "hello")
    Словари

    Для словарей используется typing.Dict . Отдельно аннотируется тип ключа и тип значений:

    book_authors: Dict[str, str] = book_authors["1984"] = 0 # Incompatible types in assignment (expression has type "int", target has type "str") book_authors[1984] = "Orwell" # Invalid index type "int" for "Dict[str, str]"; expected type "str" 

    Аналогично используются typing.DefaultDict и typing.OrderedDict

    Результат выполнения функции

    Для указания типа результата функции можно использовать любую аннотацию. Но есть несколько особенных случаев.

    Если функция ничего не возвращает (например, как print ), её результат всегда равен None . Для аннотации так же используем None .

    Корректными вариантами завершения такой функции будут: явный возврат None , возврат без указания значения и завершение без вызова return .

    def nothing(a: int) -> None: if a == 1: return elif a == 2: return None elif a == 3: return "" # No return value expected else: pass

    Если же функция никогда не возвращает управление (например, как sys.exit ), следует использовать аннотацию NoReturn :

    def forever() -> NoReturn: while True: pass

    Если это генераторная функция, то есть её тело содержит оператор yield , для возвращаемого можно воспользоватьтся аннотацией Iterable[T] , либо Generator[YT, ST, RT] :

    def generate_two() -> Iterable[int]: yield 1 yield "2" # Incompatible types in "yield" (actual type "str", expected type "int") 

    Вместо заключения

    Для многих ситуаций в модуле typing есть подходящие типы, однако я не буду рассматривать все, так как поведение аналогично рассмотренным.
    Например, есть Iterator как generic-версия для collections.abc.Iterator , typing.SupportsInt для того, чтобы указать что объект поддерживает метод __int__ , или Callable для функций и объектов, поддерживающих метод __call__

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

    В следующей статье я бы хотел остановиться на механизме работы дженериков и обработке аннотаций в рантайме.

    Как работать с типизацией в Python

    Разбор основ типизации кода в Python и её роли в динамически-типизированном языке, который будет наиболее полезен новичкам в Python.

    Первые упоминания о подсказках типов в языке программирования Python появились в базе Python Enhancement Proposals (PEP-483). Такие подсказки нужны для улучшения статического анализа кода и автодополнения редакторами, что помогает снизить риски появления багов в коде.

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

    Типизация в Python

    Для обозначения базовых типов переменных используются сами типы:

    Пример использования базовых типов в python-функции:

    def func(a: int, b: float) -> str: a: str = f", " return a 

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

    from typing import List def func(n: int) -> List[int]: return list(range(n)) 

    Кроме List , существуют и другие типы из модуля typing, которые можно параметризировать. Такие типы называются Generic-типами. Такого рода типа определены для многих встроенных в Python структур данных:

    • Set[x]
    • FrozenSet[x]
    • ByteString[x]
    • Dict[x, y]
    • DefaultDict[x, y]
    • OrderedDict[x, y]
    • ChainMap[x,y]
    • Counter[x, int]
    • Deque[x]
    • и т.д.

    Как можно заметить, некоторые типы имеют несколько параметров, которые можно описать. Например, Dict[x, y] означает, что это будет словарь, где ключи будут иметь тип x , а значения – тип y .

    Также есть более абстрактные типы, например:

    • Mapping[x, y] – объект имеет реализации метода __getitem__ ;
    • Iterable[x] – объект имеет реализацию метода __iter__ .

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

    from typing import Callable def func(f: Callable[[int, int], bool]) -> bool: return f(1,2) func(lambda x, y: x == y) >>> False 
    • говорит о том, что у объекта реализован метод __call__ ;
    • описывает типы параметров к этому методу.

    На первом месте стоит массив типов входных параметров, на втором — тип возвращаемого значения.

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

    Также есть более конкретные типы, например Literal[x] , где x указывает не тип, а конкретное значение. Например Literal[3] означает цифру 3. Используют такой тип крайне редко.

    Также Python позволяет определять свои Generic-типы.

    from typing import TypeVar, Generic T = TypeVar('T') class Stack(Generic[T]): def __init__(self) -> None: # Create an empty list with items of type T self.items: List[T] = [] def push(self, item: T) -> None: self.items.append(item) def pop(self) -> T: return self.items.pop() def empty(self) -> bool: return not self.items 

    В данном примере TypeVar означает переменную любого типа, которую можно подставить при указании. Например:

    def func(stack: Stack[int]) -> None: stack.push(11) stack.push(-2) s = Stack[int]() func(s) s.empty() >>> False s.items >>> [11, -2] 

    Для определения собственных типов наследование возможно не только от Generic , но и от других абстрактных типов, например, таких, как Mapping , Iterable .

    from typing import Generic, TypeVar, Mapping, Iterator, Dict KeyType = TypeVar('KeyType') ValueType = TypeVar('ValueType') class MyMap(Mapping[KeyType, ValueType]): # This is a generic subclass of Mapping def __getitem__(self, k: KeyType) -> ValueType: . # Implementations omitted def __iter__(self) -> Iterator[KeyType]: . def __len__(self) -> int: . 

    На месте KeyType или ValueType могут быть конкретные типы.

    Также есть специальные конструкции, которые позволяют комбинировать типы. Например, Union[x, y, . ] — один из типов. Если переменной может быть как int , так и float , то как тип следует указать Union[int, float] . Если переменной может быть как int , так и None , то в качестве типа можно указать Union[int,None] или, что предпочтительно, Optional[int] .

    Зачем это нужно

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

    Допустим, у вас есть класс юзера и функция, которая преобразует json в User .

    from typing import Dict, Union, Optional from dataclasses import dataclass @dataclass class User: name: str surname: str age: int def get_user_from_json(json_dict: Dict[str, Optional[Union[int, str]]]) -> User: name = json_dict.get("name") surname = json_dict.get("surname") age = json_dict.get("age") if (age is None or name is None or surname is None): raise ValueError("Not enough information") return User(age=age, name=name, surname=surname) 

    Конечно, можно написать и проще:

    def get_user_from_json(json_dict: Dict[str, Optional[Union[int, str]]]) -> User: return User(age=json_dict["age"], name=json_dict["name"], surname=json_dict["surname"]) 

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

    Использование Pydantic помогает корректно валидировать данные, при этом тип автоматически поменяется на требуемый.

    from pydantic import BaseModel class User(BaseModel): name: str surname: str age: int def get_user_from_json(json_dict: Dict[str, Optional[Union[int, str]]]) -> User: return User(**json_dict) get_user_from_json(< "name": "ssa", "surname": "ddd", "age": 10 >) >>> User(name='ssa', surname='ddd', age=10) get_user_from_json(< "name": "ssa", "surname": "ddd", "age": "10" >) >>> User(name='ssa', surname='ddd', age=10) get_user_from_json(< "name": "ssa", "surname": "ddd", "age": "d" >) -------------------------------------- ValidationError: 1 validation error for User age value is not a valid integer (type=type_error.integer) 

    Как можно заметить, более строгая типизация кода помогает сделать его проще и безопаснее. Однако, использование некоторых возможностей Pydantic может нежелательно повлиять на код. Так, мутация данных при валидации способна привести к тому, что тип значения модели будет непонятен. Например:

    from pydantic import BaseModel, validator class User(BaseModel): name: str age: int @validator('age') def validate_age(cls, value): if int(value) < 10: raise ValueError("too low") return str(value) User(name='Brian', age=33) >>> User(name='Brian', age='33') 

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

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

    from fastapi import FastAPI from typing import Optional from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str price: float is_offer: Optional[bool] = None @app.put("/item") async def put_item(item: Item): return

    В данном примере эндпоинт /item автоматически валидирует входящий json и передает его в функцию как требуемую модель.

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

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

    Нововведения Python 3.9.0

    Начиная с недавно вышедшей версии Python 3.9, у разработчиков больше нет необходимости импортировать абстрактные коллекции для описания типов. Теперь вместо typing.Dict[x, y] можно использовать dict[x,y] , то же самое происходит с Deque , List , Counter и т.д. Полное описание этого нововведения можно прочитать тут: PEP-585.

    Также добавили аннотации типов, которые в дальнейшем могут быть использованы инструментами статического анализа. variable: Annotated[T, x] где T — тип переменной variable , а x — некоторые метаданные для переменной. По оценкам некоторых авторов, эти метаданные могут быть использованы также и во время выполнения (подробности смотрите в PEP-593).

    Заключение

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

    Модуль typing и аннотация типов в Python

    Модуль typing, который добавлен в Python 3.5, предоставляет способ указания типов, что позволяет программам проверки статических типов и линтерам точно предсказывать ошибки.

    Иногда разработчикам бывает сложно выяснить, что именно происходит в коде из-за того, что Python должен определять тип объектов во время выполнения.

    Внешние средства проверки типов, например PyCharm IDE, не дают нужных результатов. Он правильно прогнозирует ошибки всего в 50% случаев, согласно доступной статистике.

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

    Код Python становится читабельным и понятным для других пользователей!

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

    Требования

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

    Для этого используйте команду:

    pip3 install mypy

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

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

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

    Подсказки типа, или типовая аннотация

    Типовая аннотация в функциях

    Мы можем аннотировать функцию, чтобы указать ее возвращаемый тип и типы ее параметров.

    def print_list(a: list) -> None: print(a)

    Этот код сообщает средству проверки типов (у нас это mypy), что у нас есть функция print_list(), которая принимает list в качестве аргумента и возвращает None.

    def print_list(a: list) -> None: print(a) print_list([1, 2, 3]) print_list(1)

    Сначала давайте запустим это в mypy:

    vijay@JournalDev:~ $ mypy printlist.py printlist.py:5: error: Argument 1 to "print_list" has incompatible type "int"; expected "List[Any]" Found 1 error in 1 file (checked 1 source file)

    Поскольку строка № 5 имеет аргумент int, а не list, мы получаем ошибку.

    Аннотация типов в переменных

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

    # Annotates 'radius' to be a float radius: float = 1.5 # We can annotate a variable without assigning a value! sample: int # Annotates 'area' to return a float def area(r: float) -> float: return 3.1415 * r * r print(area(radius)) # Print all annotations of the function using # the '__annotations__' dictionary print('Dictionary of Annotations for area():', area.__annotations__)
    vijay@JournalDev: ~ $ mypy find_area.py && python find_area.py Success: no issues found in 1 source file 7.068375 Dictionary of Annotations for area(): , 'return': >

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

    Псевдонимы типов

    Модуль typing предоставляет нам псевдонимы типов. Чтобы определить такой псевдоним, присвойте его типу.

    from typing import List # Vector is a list of float values Vector = List[float] def scale(scalar: float, vector: Vector) -> Vector: return [scalar * num for num in vector]

    В результате получится:

    vijay@JournalDev: ~ $ mypy vector_scale.py && python vector_scale.py Success: no issues found in 1 source file [2.0, 4.0, 6.0]

    В фрагменте выше Vector — это псевдоним, обозначающий список значений с плавающей запятой. Приведенная выше программа может вывести подсказку для псевдонима.

    Полный список допустимых псевдонимов приведен здесь .

    Теперь давайте рассмотрим еще один пример, который проверяет каждую пару “ключ:значение” на соответствие формату name:email.

    from typing import Dict import re # Create an alias called 'ContactDict' ContactDict = Dict[str, str] def check_if_valid(contacts: ContactDict) -> bool: for name, email in contacts.items(): # Check if name and email are strings if (not isinstance(name, str)) or (not isinstance(email, str)): return False # Check for email xxx@yyy.zzz if not re.match(r"[a-zA-Z0-9\._\+-]+@[a-zA-Z0-9\._-]+\.[a-zA-Z]+$", email): return False return True print(check_if_valid()) print(check_if_valid())

    Вывод mypy будет выглядеть так:

    vijay@JournalDev:~ $ mypy validcontacts.py validcontacts.py:19: error: Dict entry 1 has incompatible type "int": "str"; expected "str": "str" Found 1 error in 1 file (checked 1 source file)

    Поскольку параметр name в нашем втором словаре является целым числом (123), то здесь мы получаем статическую ошибку времени компиляции в mypy. Таким образом, псевдонимы — это еще один способ обеспечить точную проверку типов из mypy.

    Создание пользовательских типов данных с помощью NewType()

    Для создания новых пользовательских типов мы можем использовать функцию NewType().

    from typing import NewType # Create a new user type called 'StudentID' that consists of # an integer StudentID = NewType('StudentID', int) sample_id = StudentID(100)

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

    from typing import NewType # Create a new user type called 'StudentID' StudentID = NewType('StudentID', int) def get_student_name(stud_id: StudentID) -> str: return str(input(f'Enter username for ID #:\n')) stud_a = get_student_name(StudentID(100)) print(stud_a) # This is incorrect!! stud_b = get_student_name(-1) print(stud_b)

    Вывод mypy будет иметь следующий вид:

    vijay@JournalDev:~ $ mypy studentnames.py studentnames.py:13: error: Argument 1 to "get_student_name" has incompatible type "int"; expected "StudentID" Found 1 error in 1 file (checked 1 source file)

    Тип Any

    Any – это особый тип, который сообщает другим инструментам проверки типов (в нашем случае mypy), что каждый тип совместим с этим ключевым словом.

    Рассмотрим нашу старую функцию print_list(), которая теперь принимает аргументы любого типа.

    from typing import Any def print_list(a: Any) -> None: print(a) print_list([1, 2, 3]) print_list(1)

    Теперь при запуске mypy ошибок не будет.

    vijay@JournalDev:~ $ mypy printlist.py && python printlist.py Success: no issues found in 1 source file [1, 2, 3] 1

    Все функции без возвращаемого типа значения или параметров будут по умолчанию использовать Any.

    def foo(bar): return bar # A static type checker will treat the above # as having the same signature as: def foo(bar: Any) -> Any: return bar

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

    Заключение

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

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *