Как явно указать тип в python
Перейти к содержимому

Как явно указать тип в python

  • автор:

Python. Урок 18. Аннотация типов в Python

Follow us on Google Plus Follow us on rss

Этот урок посвящен аннотациям типов в Python. Рассмотрен вопрос контроля типов переменных и функций с использованием комментариев и аннотаций. Приведено описание PEP‘ов, регламентирующих работу с аннотациями, и представлены примеры работы с инструментом mypy для анализа Python кода.

  • Зачем нужны аннотации?
  • Контроль типов в Python
  • Обзор PEP’ов регламентирующий работу с аннотациями
  • Использование аннотаций в функциях
    • Указание типа аргументов и возвращаемого значения
    • Доступ к аннотациям функции
    • Создание аннотированных переменных
    • Контроль типов с использованием аннотаций

    Зачем нужны аннотации?

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

    Согласно PEP 3107 могут быть следующие варианты использования аннотаций:

    • проверка типов;
    • расширение функционала IDE в части предоставления информации об ожидаемых типах аргументов и типе возвращаемого значения у функций;
    • перегрузка функций и работа с дженериками;
    • взаимодействие с другими языками;
    • использование в предикатных логических функциях;
    • маппинг запросов в базах данных;
    • маршалинг параметров в RPC (удаленный вызов процедур).

    Контроль типов в Python

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

    Выглядит это так:

    name = "John" # type: str

    Мы создали переменную с именем name и предполагаем, что ее тип – str . Естественно, для самого интерпретатора Python это не имеет значения, мы, без труда можем продолжить нашу мини программу таким образом.

    name = "John" # type: str print(name) name = 10 print(name)

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

    Установить его можно с помощью pip :

    python -m pip install mypy

    Если мы сохраним приведенный выше код в файле type_tester.py и выполним следующую команду:

    python -m mypy test_type.py

    Получим такое сообщение:

    test_type.py:3: error: Incompatible types in assignment (expression has type “int”, variable has type “str”)

    Оно говорит о том, что обнаружено несоответствие типов в операции присваивание: переменная имеет тип “ str “, а ей присвоено значение типа “ int “.

    Обзор PEP’ов регламентирующий работу с аннотациями

    Начнем наше введение в тему аннотаций в Python с краткого обзора четырех ключевых документов:

    Первый из них PEP 3107 — Function Annotations , является исторически первым из перечисленных выше документов. В нем описывается синтаксис использования аннотаций в функциях Python . Важным является то, что аннотации не имеют никакого семантического значения для интерпретатора Python и предназначены только для анализа сторонними приложениями. Аннотировать можно аргументы функции и возвращаемое ей значение.

    Следующий документ – PEP 484 — Type Hints . В нем представлены рекомендации по использованию аннотаций типов. Аннотация типов упрощает статический анализ кода, рефакторинг, контроль типов в рантайме и кодогенерацию, использующую информацию о типах. В рамках данного документа, определены следующие варианты работы с аннотациями: использование аннотаций в функциях согласно PEP 3107, аннотация типов переменных через комментарии в формате # type: type_name и использование stub -файлов (см. разделы Использование аннотаций в функциях и Аннотация переменных).

    В PEP 526 — Syntax for Variable Annotations приводится описание синтаксиса для аннотации типов переменных (базируется на PEP 484), использующего языковые конструкции, встроенные в Python (см. раздел Аннотация переменных).

    PEP 563 — Postponed Evaluation of Annotations . Данный PEP вступил в силу с выходом Python 3.7. У подхода работы с аннотация до этого PEP’а был ряд проблем связанных с тем, что определение типов переменных (в функциях, классах и т.п.) происходит во время импорта модуля, и может сложится такая ситуация, что тип переменной объявлен, но информации об этом типе ещё нет, в таком случае тип указывают в виде строки – в кавычках. В PEP 563 предлагается использовать отложенную обработку аннотаций, это позволяет определять переменные до получения информации об их типах и ускоряет выполнение программы, т.к. при загрузке модулей не будет тратится время на проверку типов – это будет сделано перед работой с переменными.

    Теперь более подробно остановимся на использовании аннотаций, опираясь на перечисленные выше PEP ’ы.

    Использование аннотаций в функциях

    Указание типов аргументов и возвращаемого значения

    В функциях мы можем аннотировать аргументы и возвращаемое значение. Выглядеть это может так.

    def repeater(s: str, n: int) -> str: return s * n

    Аннотация для аргумента определяется через двоеточие после его имени.

    имя_аргумента: аннотация

    Аннотация, определяющая тип возвращаемого функцией значения, указывается после ее имени с использованием символов ->

    def имя_функции() -> тип

    Для лямбд аннотации не поддерживаются.

    Доступ к аннотациям функции

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

    Содержимое repeater. __annotations__

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

    Создание аннотированных переменных

    Можно использовать один из трех способов создания аннотированных переменных.

    var = value # type: annotation var: annotation; var = value var: annotation = value

    Рассмотрим это на примере работы со строковой переменной с именем name.

    name = “John” # type: str name:str; name = “John” name: str = “John”

    Приведем еще несколько примеров.

    # список scores: List[int] = [] scores.append(1) # кортеж pack: Tuple[int, …] = (1, 2, 3) # логическая переменная flag: bool flag = True # класс class Point: x: int y: int def __init__(self, x: int, y: int): self.x = x self.y = y

    Контроль типов с использованием аннотаций

    Для проверки можно использовать уже знакомый нам инструмент mypy . Напишем вот такой код.

    a:int = 10 b:int = 15 def sq_sum(v1:int, v2:int) -> int: return v1**2 + v2**2 print(sq_sum(a, b))

    Сохраним его в файле с именем work.py и запустим mypy для анализа.

    python -m mypy work.py

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

    Но если заменить первую строку

    a: int = 10
    a = 10.3

    и вновь запустить mypy , то увидим вот такое сообщение:

    work.py:7: error: Argument 1 to “sq_sum” has incompatible type “float”; expected “int”

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

    Отложенная проверка аннотаций

    До выхода Python 3.7 определение типов в аннотациях происходило во время импорта модуля, что приводило к проблеме. Например, если выполнить следующий код:

    class Rectangle: def __init__(self, height: int, width: int, color: Color) -> None: self.height = height self.width = width self.color = color

    То возникнет ошибка NameError: name ‘Color’ is not defined. Она связана с тем, что переменная color имеет тип Color, который пока ещё не объявлен.

    В таком случае, мы можем указать тип Color в кавычках, но это не очень удобно.

    class Rectangle: def __init__(self, height: int, width: int, color: "Color") -> None: self.height = height self.width = width self.color = color

    Эту проблему можно решить воспользовавшись отложенной обработкой аннотаций из Python 3.7.

    from __future__ import annotations class Rectangle: def __init__(self, height: int, width: int, color: Color) -> None: self.height = height self.width = width self.color = color class Color: def __init__(self, r: int, g: int, b: int) -> None: self.R = r self.G = g self.B = b rect = Rectangle(1, 2, Color(255, 255, 255))

    Можете проверить, что без строки from __future__ import annotations эта программа выполняться не будет.

    P.S.

    Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas. На нашем сайте вы можете найти вводные уроки по этой теме. Все уроки по библиотеке Pandas собраны в книге “Pandas. Работа с данными”.

    Раздел: Python Уроки по Python Метки: Python, аннотации в Python, Уроки Python

    Python. Урок 18. Аннотация типов в Python : 2 комментария

    1. Алексей 08.11.2020 Благодарю, хорошее объяснение.

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

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

    Обложка поста Аннотации переменных в Python

    В Python 3.6 появилась такая интересная особенность, как синтаксис для аннотаций переменных. Подробнее про это нововведение можно прочитать в PEP 526. Суть этого PEP заключается в том, чтобы перевести идею аннотаций типов (PEP 484) на её следующую логическую ступень, т.е. сделать возможным указание типов переменных, включая поля классов и объектов.

    Следует принять во внимание, что это нововведение не делает Python статически типизированным языком. Интерпретатору всё равно, какой тип указан у переменной. Тем не менее в среде разработки на Python или другом инструменте вроде pylint может быть включена функция проверки аннотаций, которая сообщит вам, если вы сначала указали один тип переменной, а затем попытались использовать её в качестве другого типа далее в программе.

    Давайте взглянем на простой пример, чтобы посмотреть, как это работает:

    # annotate.py name: str = 'Mike' 

    В нашем файле annotate.py мы создали переменную name и добавили аннотацию, говорящую о том, что это строка. Аннотацию можно добавить, поставив двоеточие после имени переменной и затем указав нужный тип. Присваивать значение переменной вовсе не обязательно:

    # annotate.py name: str 

    Прим.перев. Указать тип в Python 3.5 подобным образом не выйдет. Вместо этого есть возможность указывать тип в комментарии:

    # annotate.py name = 'Mike' # type: str 

    Все переменные с аннотациями добавляются в атрибут __annotations__ модуля или класса. Давайте импортируем первую версию нашего модуля annotate и получим доступ к этому атрибуту:

    >>> import annotate >>> annotate.__annotations__ > >>> annotate.name 'Mike' 

    Как вы видите, атрибут __annotations__ возвращает словарь с аннотацией в качестве значения. Давайте добавим ещё несколько аннотаций в наш модуль и посмотрим, как изменится атрибут __annotations__ :

    # annotate.py name: str = 'Mike' ages: list = [12, 20, 32] class Car: variable: dict 

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

    >>> import annotate >>> annotate.__annotations__ , 'ages': > >>> annotate.Car.__annotations__ > >>> car = annotate.Car() >>> car.__annotations__ > 

    В этот раз в словаре находится два объекта. Как вы могли заметить, в нём нет поля класса. Чтобы получить к нему доступ, мы должны обратиться к классу Car напрямую или создать экземпляр класса и получить атрибут таким образом.

    Заключение

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

    Аннотации типов на уровне классов

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

    Если вы пока еще не знакомы с классами языка Python, то советую прежде изучить курс по ООП на Python, а потом вернуться к этому занятию. Этот материал будет интересен тем, кто уже знает, что такое классы и объекты.

    Итак, давайте рассмотрим простую аннотацию, например, типом dict:

    tr: dict = {'car': 'машина'}

    В действительности, здесь dict – это класс, который определяет работу со словарем. А имя класса воспринимается, как название типа. Значит, при аннотации мы можем указывать любые классы в том числе и object – класс, от которого неявно наследуются все классы в Python 3. Давайте это сделаем и посмотрим к чему приведет:

    x: object = None x = "123" x = 123

    Смотрите, мы совершенно спокойно можем прописывать любые типы данных, унаследованные от object, и интегрированная среда нам не выдаст никаких предупреждений. Почему так произошло? Дело в том, что к базовому типу object можно привести любой другой тип, который от него наследуется. А это практически все типы данных в Python. Поэтому строки, числа и любые другие стандартные объекты допускается присваивать переменной x без нарушения данной аннотации.

    Или такой пример. Объявим два класса:

    class Geom: pass class Line(Geom): pass

    и аннотируем переменную типом Geom:

    g: Geom

    Далее, присвоим этой переменной объект класса Line:

    g = Line()

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

    class Line: pass

    то сразу появляется подсветка, т.к. теперь типы Line и Geom независимы.

    Все эти примеры показывают, как именно аннотация типов учитывает иерархию наследования классов при проверке корректности присваиваемых данных.

    Отличие между object и typing.Any

    Учитывая все сказанное, внимательный слушатель может спросить: а в чем отличия между типом object и типом Any из модуля typing? Если все типы данных в Python наследуются от object, значит это эквивалентно использованию типа Any? Почти. Но есть один нюанс. Его можно сформулировать так:

    Тип Any совместим с любым другим типом, а тип object – ни с каким другим.

    Давайте я поясню смысл этой фразы на конкретном примере. Допустим, у нас имеется некая переменная аннотированная типом Any:

    a: Any = None

    А, затем, мы определим еще одну переменную с аннотацией типа str:

    s: str

    После этого можно совершенно спокойно присвоить первую переменную второй:

    s = a

    Подсветки кода, т.е. каких-либо предупреждений не будет. Но, если вместо Any прописать object:

    a: object = None

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

    a: str = '123' s: object s = a

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

    a: Any = '123' s: object s = a

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

    Модуль mypy

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

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

    Чтобы воспользоваться этим модулем его нужно вначале установить. Делается это уже известной вам командой:

    pip install mypy

    После установки в терминале достаточно записать команду:

    В моем случае программа находится в файле main.py, а сам файл хранится в текущем рабочем каталоге. Поэтому команда примет вид:

    Если нужно анализировать сразу несколько файлов, то они прописываются через пробел.

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

    Аннотация с помощью Type и TypeVar

    Но давайте вернемся непосредственно к теме «аннотация типов» и предположим, что у нас объявлены два класса:

    class Geom: pass class Point2D(Geom): pass

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

    def factory_point(cls_geom): return cls_geom()

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

    def factory_point(cls_geom: Geom) -> Geom: return cls_geom()

    Но нам здесь интегрированная среда сразу подсвечивает фрагмент cls_geom(). Почему это произошло? Как раз по той причине, что аннотация :Geom подразумевает, что параметр cls_geom будет ссылаться на объекты класса Geom, а не на сам класс Geom. Вот это очень важно понимать, когда вы прописываете аннотации типов. Везде подразумеваются объекты тех типов, которые указываются. Но как тогда поправить эту ситуацию? Очень просто. Для этого существует специальный тип Type из модуля typing. Если мы перепишем аннотацию в виде:

    def factory_point(cls_geom: Type[Geom]) -> Geom: return cls_geom()

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

    Давайте теперь воспользуемся этой функцией. Если ее вызвать так:

    geom = factory_point(Geom) point = factory_point(Point2D)

    то с аннотациями никаких конфликтов не будет. Но, если мы дополнительно аннотируем и переменные geom и point соответствующими типами:

    geom: Geom = factory_point(Geom) point: Point2D = factory_point(Point2D)

    то во второй строчке появится подсветка кода. Очевидно это из-за того, что мы явно указываем ожидаемый тип Point2D, а в определении функции прописан тип Geom. И, так как Geom – базовый класса для Point2D, то возникает конфликт аннотаций.

    Для исправления таких ситуаций в Python можно описывать некие общие типы с помощью класса TypeVar. Например:

    T = TypeVar("T", bound=Geom)

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

    def factory_point(cls_geom: Type[T]) -> T: return cls_geom()

    и он будет автоматически вычисляться при вызове функции. Когда передается класс Geom, то T будет соответствовать этому типу, а когда передается Point2D – то тип T будет Point2D. И так далее. Вот смысл универсальных типов при формировании аннотаций.

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

    T = TypeVar("T") # T – произвольный тип без ограничений T = TypeVar("T", int, float) # T – тип связанный только с типами int и float

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

    Аннотация типов в классах

    В заключение этой темы добавлю пару слов об аннотации типов внутри классов. В целом все делается практически также как и в случае с переменными и функциями. Давайте распишем класс Point2D следующим образом:

    class Point2D: def __init__(self, x: int, y: int) -> None: self.x = x self.y = y

    Обратите внимание, что параметр self не принято аннотировать. Также метод __init__ всегда возвращает значение None.

    Воспользоваться этим классом можно так:

    p = Point2D(10.5, 20)

    В этом случае первый аргумент будет подсвечен, т.к. не соответствует целому типу. Но программа отработает без ошибок. Главный вопрос здесь: существует ли аннотация типов у локальных атрибутов x, y объекта класса Point2D? Давайте проверим. Запишем команду:

    p.x = '10'

    и интегрированная среда нам не выдает никакой подсветки. Но если перейти в терминал и выполнить команду:

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

    class Point2D: x: int y: int def __init__(self, x: int, y: int) -> None: self.x = x self.y = y

    В заключение этого занятия покажу еще одну особенность аннотации типов в классах. Объявим метод с именем copy():

    class Point2D: x: int y: int def __init__(self, x: int, y: int) -> None: self.x = x self.y = y def copy(self) -> Point2D: return Point2D(self.x, self.y)

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

    def copy(self) -> 'Point2D': return Point2D(self.x, self.y)

    Но можно сделать лучше. Если импортировать из модуля __future__ объект annotations:

    from __future__ import annotations

    то после этого можно убрать кавычки у имени класса:

    def copy(self) -> Point2D: return Point2D(self.x, self.y)

    Вы спросите почему это явно не внедрили в новых версиях языка Python? Зачем требуется что то дополнительно импортировать? Ответ прост. Это сделано специально для обратной совместимости с более ранними версиями. Разработчики языка решили, что это важно. По крайней мере пока. Поэтому просто запомните этот обстоятельство.

    Следует ли использовать аннотацию типов

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

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

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

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

    Видео по теме

    #1. Первое знакомство с Python Установка на компьютер

    Как явно указать тип переменной передаваемой в конструктор класса?

    Никак. Python не умеет в статическую типизацию. Но можно сделать некоторые приближения:
    1. mypy + typing позволит сделать из питона подобие TypeScript
    2. typing. Твоя IDE (если она нормальная) подскажет тебе, что ты передаёшь не float. Но в рантайме таких проверок нет, как и в пункте №1
    3. Рейзить ошибку, если if not isinstance(height, float). Проверка работает только в рантайме, IDE ничего не подстветит

    Ответ написан более трёх лет назад
    Комментировать
    Нравится 2 Комментировать

    fox_12

    Владимир Куц @fox_12 Куратор тега Python
    Расставляю биты, управляю заряженными частицами
    Сделать свой костылик:

    class A: def __init__(self, height: float): if not isinstance(height, float): raise Exception('height is not float type!')

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

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