Аннотация типов в python
Аннотации не имеют никакого семантического значения для интерпретатора Python и предназначены только для анализа сторонними приложениями.
У подхода работы с аннотация до этого PEP’а был ряд проблем связанных с тем, что определение типов переменных (в функциях, классах и т.п.) происходит во время импорта модуля, и может сложится такая ситуация, что тип переменной объявлен, но информации об этом типе ещё нет, в таком случае тип указывают в виде строки – в кавычках. В PEP 563 предлагается использовать отложенную обработку аннотаций, это позволяет определять переменные до получения информации об их типах и ускоряет выполнение программы, т.к. при загрузке модулей не будет тратится время на проверку типов – это будет сделано перед работой с переменными.
Аннотации локальных переменных недоступны во время выполнения, но аннотации глобальных переменных, атрибутов классов и функций хранятся в специальном атрибуте __annotations__ модулей, классов и функций соответственно.
Аннотации в функциях
def repeater(s: str, n: int) -> str: return s * n
Тут имя_аргумента: аннотация аннотация аргумента. Аннотация возвращаемого функцией значения def имя_функции() -> тип . Доступ к аннотации осуществляется через repeater.__annotations__ . Мы получим это;
'n': int, 'return': str, 's': str>
Аннотация переменных
Есть три варианта
var = value # type: annotation var: annotation; var = value var: annotation = value # example 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 a:int = 10 b:int = 15 def sq_sum(v1:int, v2:int) -> int: return v1**2 + v2**2 print(sq_sum(a, b))
- [2022-09-14-daily-note] type annotations for class instances and dynamic attributes
Введение в аннотации типов 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. Оно охватывает только минимум, необходимый для их использования с FastAPI. что на самом деле очень мало.
FastAPI целиком основан на аннотациях типов, у них много выгод и преимуществ.
Но даже если вы никогда не используете FastAPI, вам будет полезно немного узнать о них.
Если вы являетесь экспертом в Python и уже знаете всё об аннотациях типов, переходите к следующему разделу.
Мотивация¶
Давайте начнем с простого примера:
def get_full_name(first_name, last_name): full_name = first_name.title() + " " + last_name.title() return full_name print(get_full_name("john", "doe"))
Вызов этой программы выводит:
John Doe
Функция делает следующее:
- Принимает first_name и last_name .
- Преобразует первую букву содержимого каждой переменной в верхний регистр с title() .
- Соединяет их через пробел.
def get_full_name(first_name, last_name): full_name = first_name.title() + " " + last_name.title() return full_name print(get_full_name("john", "doe"))
Отредактируем пример¶
Это очень простая программа.
А теперь представьте, что вы пишете её с нуля.
В какой-то момент вы бы начали определение функции, у вас были бы готовы параметры.
Но затем вы должны вызвать «тот метод, который преобразует первую букву в верхний регистр».
Было это upper ? Или uppercase ? first_uppercase ? capitalize ?
Тогда вы попробуете с давним другом программиста: автодополнением редактора.
Вы вводите первый параметр функции, first_name , затем точку ( . ), а затем нажимаете Ctrl+Space , чтобы запустить дополнение.
Но, к сожалению, ничего полезного не выходит:
Добавим типы¶
Давайте изменим одну строчку в предыдущей версии.
Мы изменим именно этот фрагмент, параметры функции, с:
first_name, last_name
first_name: str, last_name: str
Это аннотации типов:
def get_full_name(first_name: str, last_name: str): full_name = first_name.title() + " " + last_name.title() return full_name print(get_full_name("john", "doe"))
Это не то же самое, что объявление значений по умолчанию, например:
first_name="john", last_name="doe"
Это другая вещь.
Мы используем двоеточия ( : ), а не равно ( = ).
И добавление аннотаций типов обычно не меняет происходящего по сравнению с тем, что произошло бы без неё.
Но теперь представьте, что вы снова находитесь в процессе создания этой функции, но уже с аннотациями типов.
В тот же момент вы пытаетесь запустить автодополнение с помощью Ctrl+Space и вы видите:
При этом вы можете просматривать варианты, пока не найдёте подходящий:
Больше мотивации¶
Проверьте эту функцию, она уже имеет аннотации типов:
def get_name_with_age(name: str, age: int): name_with_age = name + " is this old: " + age return name_with_age
Поскольку редактор знает типы переменных, вы получаете не только дополнение, но и проверки ошибок:
Теперь вы знаете, что вам нужно исправить, преобразовав age в строку с str(age) :
def get_name_with_age(name: str, age: int): name_with_age = name + " is this old: " + str(age) return name_with_age
Объявление типов¶
Вы только что видели основное место для объявления подсказок типов. В качестве параметров функции.
Это также основное место, где вы можете использовать их с FastAPI.
Простые типы¶
Вы можете объявить все стандартные типы Python, а не только str .
Вы можете использовать, к примеру:
def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes): return item_a, item_b, item_c, item_d, item_d, item_e
Generic-типы с параметрами типов¶
Существуют некоторые структуры данных, которые могут содержать другие значения, например, dict , list , set и tuple . И внутренние значения тоже могут иметь свой тип.
Чтобы объявить эти типы и внутренние типы, вы можете использовать стандартный Python-модуль typing .
Он существует специально для поддержки подсказок этих типов.
List ¶
Например, давайте определим переменную как list , состоящий из str .
Импортируйте List из typing (с заглавной L ):
from typing import List def process_items(items: List[str]): for item in items: print(item)
Объявите переменную с тем же синтаксисом двоеточия ( : ).
В качестве типа укажите List .
Поскольку список является типом, содержащим некоторые внутренние типы, вы помещаете их в квадратные скобки:
from typing import List def process_items(items: List[str]): for item in items: print(item)
Эти внутренние типы в квадратных скобках называются «параметрами типов».
В этом случае str является параметром типа, передаваемым в List .
Это означает: «переменная items является list , и каждый из элементов этого списка является str «.
Если вы будете так поступать, редактор может оказывать поддержку даже при обработке элементов списка:
Без типов добиться этого практически невозможно.
Обратите внимание, что переменная item является одним из элементов списка items .
И все же редактор знает, что это str , и поддерживает это.
Tuple и Set ¶
Вы бы сделали то же самое, чтобы объявить tuple и set :
from typing import Set, Tuple def process_items(items_t: Tuple[int, int, str], items_s: Set[bytes]): return items_t, items_s
- Переменная items_t является tuple с 3 элементами: int , другим int и str .
- Переменная items_s является set и каждый элемент имеет тип bytes .
Dict ¶
Чтобы определить dict , вы передаёте 2 параметра типов, разделённых запятыми.
Первый параметр типа предназначен для ключей dict .
Второй параметр типа предназначен для значений dict :
from typing import Dict def process_items(prices: Dict[str, float]): for item_name, item_price in prices.items(): print(item_name) print(item_price)
- Переменная prices является dict :
- Ключи этого dict имеют тип str (скажем, название каждого элемента).
- Значения этого dict имеют тип float (скажем, цена каждой позиции).
Optional ¶
Вы также можете использовать Optional , чтобы объявить, что переменная имеет тип, например, str , но это является «необязательным», что означает, что она также может быть None :
from typing import Optional def say_hi(name: Optional[str] = None): if name is not None: print(f"Hey name>!") else: print("Hello World")
Использование Optional[str] вместо просто str позволит редактору помочь вам в обнаружении ошибок, в которых вы могли бы предположить, что значение всегда является str , хотя на самом деле это может быть и None .
Generic-типы¶
Эти типы принимают параметры в квадратных скобках:
называются Generic-типами или Generics.
Классы как типы¶
Вы также можете объявить класс как тип переменной.
Допустим, у вас есть класс Person с полем name :
class Person: def __init__(self, name: str): self.name = name def get_person_name(one_person: Person): return one_person.name
Тогда вы можете объявить переменную типа Person :
class Person: def __init__(self, name: str): self.name = name def get_person_name(one_person: Person): return one_person.name
И снова вы получаете полную поддержку редактора:
Pydantic-модели¶
Pydantic является Python-библиотекой для выполнения валидации данных.
Вы объявляете «форму» данных как классы с атрибутами.
И каждый атрибут имеет тип.
Затем вы создаете экземпляр этого класса с некоторыми значениями, и он проверяет значения, преобразует их в соответствующий тип (если все верно) и предоставляет вам объект со всеми данными.
И вы получаете полную поддержку редактора для этого итогового объекта.
Взято из официальной документации Pydantic:
from datetime import datetime from typing import List, Union from pydantic import BaseModel class User(BaseModel): id: int name: str = "John Doe" signup_ts: Union[datetime, None] = None friends: List[int] = [] external_data = "id": "123", "signup_ts": "2017-06-01 12:22", "friends": [1, "2", b"3"], > user = User(**external_data) print(user) # > User name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3] print(user.id) # > 123
FastAPI целиком основан на Pydantic.
Вы увидите намного больше всего этого на практике в Руководстве пользователя.
Аннотации типов в FastAPI¶
FastAPI получает преимущества аннотаций типов для выполнения определённых задач.
С FastAPI вы объявляете параметры с аннотациями типов и получаете:
. и FastAPI использует тот же механизм для:
- Определения требований: из параметров пути запроса, параметров запроса, заголовков, зависимостей и т.д.
- Преобразования данных: от запроса к нужному типу.
- Валидации данных: исходя из каждого запроса:
- Генерации автоматических ошибок, возвращаемых клиенту, когда данные не являются корректными.
- который затем используется пользовательскими интерфейсами автоматической интерактивной документации.
Всё это может показаться абстрактным. Не волнуйтесь. Вы увидите всё это в действии в Руководстве пользователя.
Важно то, что при использовании стандартных типов Python в одном месте (вместо добавления дополнительных классов, декораторов и т.д.) FastAPI сделает за вас большую часть работы.
Если вы уже прошли всё руководство и вернулись, чтобы узнать больше о типах, хорошим ресурсом является «шпаргалка» от mypy .
Почему я начал использовать аннотации типов в Python – и вам тоже советую
С появлением подсказок типов (type hints) в Python 3.5+ добавилась опциональная статическая типизация – поэтому эти подсказки так мне нравятся. Теперь я аннотирую ими все мои проекты.
Когда еще в 2016 году вышел Python 3.6, меня восхитили некоторые новые возможности, которые в нем предоставлялись. Среди них меня особенно впечатлили f-строки (если вы ими пока не пользовались, начните, не откладывая).
Спустя некоторое время после того, как я апгрейдил мой стек для работы с Python 3.6, я услышал об аннотациях типов из видео Дэна Бейдера. Он объяснил, что это такое, и для чего они ему пригодились.
Хотя, аннотации типов были введены в Python 3.5 (еще в 2015 году), о них по-прежнему интересно поговорить более подробно.
Думаю, как раз настало время поделиться с вами опытом!
Статическая типизация в Python?! Нет, это не для меня
Впервые услышав об аннотациях типов, я ими не впечатлился. Думал, что аннотации типов – это какой-то костыль на уровне языка Python.
Идея указывать типы в языке с динамической типизацией показалась мне, мягко говоря, странной, учитывая, что динамическая природа Python годами меня устраивала.
На тот момент я считал, что вполне нормально написать такой код:
def set_pos(self, pos): self.x = pos[0] self.y = pos[1]
Что должно содержаться в pos ? Это же очевидно – просто смотрим в код и видим, что там должен находиться кортеж с двумя числами (а какими именно числами? Целыми? С плавающей точкой?).
Я также узнал, что в среде выполнения Python аннотации типов вообще не использовались. Ранее они полностью игнорировались. Поэтому я подумал: зачем их использовать, если они никак не влияют на выполнение кода, который я пишу?
В самом деле, мне было непонятно, откуда весь этот растущий сыр-бор по поводу введения статической типизации в Python.
Пока вы не начали сотрудничать с другими разработчикам
Один из первых фрагментов кода, который я прочитал, был написан моим наставником. Он – из тех, у кого есть большой практический опыт работы с Java, но для выполнения задач ему также потребовалось освоить Python. Поскольку наставник исходно работал со статически типизируемыми языками, он настойчиво выступал за аннотирование типов в Python и в своем коде использовал их повсюду. Когда я спросил, зачем, он просто мне ответил:
Так всем понятнее. Аннотации типов поясняют читателю твоего кода, каков в этом коде ввод и вывод – даже спрашивать об этом не приходится.
Меня поразило, что в этом тезисе такой акцент делается на удобстве других людей.
В принципе, он сказал, что человек использует аннотации типов, чтобы другим было проще понимать его код.
Удобочитаемость важна
Задумайтесь об этом. Когда вы работаете над проектом, тот код, который вы пишете сейчас, может казаться вам вполне осмысленным. Вы не ощущаете нужды подробно его документировать.
Но другие люди (в том числе, вы через полгода) в будущем должны будут читать ваш код и понимать, что он означает. Как я полагаю, при этом читателю кода приходится ответить как минимум на три основных вопроса:
- Что этот фрагмент принимает в качестве ввода?
- Как он обрабатывает ввод?
- Что этот код выдает в качестве вывода?
По мере того, как я все больше читал код коллег – в том числе, сложный унаследованный код — я осознал, что аннотации типов на самом деле крайне полезны. Аннотации типов позволили мне ответить на вопросы 1 и 3 мгновенно. (на вопрос 2 можно ответить не менее просто, если в коде правильно выбраны имена функций.)
Давайте сыграем в игру
Ниже я написал функцию, ее тело скрыто. Можете мне рассказать, что она делает?
def concat(a, b): .
Вот моя версия — судя по имени функции, я бы сказал, что concat() принимает два списка (или кортежа?) и сцепляет их, возвращая в результате единый список, в котором содержатся элементы a и b .
Очевидно, правда? Не вполне.
На самом деле, здесь есть две возможности. Что, если на самом деле concat() просто сцепляет две строки, например?
Вот в чем дело — мы, в принципе, не понимаем, что делает concat() , поскольку мы не можем ответить на все три вопроса, приведенных выше. Можно примерно ответить только на вопрос 2: «делается какое-то сцепление».
А теперь давайте добавим аннотации типов для concat() :
def concat(a: int, b: int) -> str: .
Ага! Значит, мы в обоих вопросах ошиблись. По-видимому, concat() принимает два целых числа и на выходе выдает строку.
Так что, теперь я скажу: она принимает на ввод два целых числа, преобразует их в строковые представления, сцепляет их и возвращает результат.
А вот что именно она делает:
def concat(a: int, b: int) -> str: return str(a) + str(b)
Этот пример показывает, что знание ввода и вывода критически важно, чтобы понять этот фрагмент кода. А аннотации типов позволяют решить эту задачу почти мгновенно.
Здесь обычно был обходной маневр
Возвращаясь к моему опыту, скажу, что уже знал об этом – задолго до того, как начал использовать аннотации типов — и, вероятно, вы тоже знали.
Мне всегда нравился чистый код, который я при этом документировал, насколько мне удавалось. Думаю, это знак дисциплинированности – добавлять строку docstring во все ваши функции и классы, чтобы объяснить, что они делают (функционал) и почему они вообще существуют (цель).
Вот конкретный фрагмент кода из моего личного проекта, над которым я работал несколько лет назад:
def randrange(a, b, size=1): """Return random numbers between a and b. Parameters ---------- a : float Lower bound. b : float Upper bound. size : int, optional Number of numbers to return. Defaults to 1. Returns ------- ns : list of float """ .
Посмотрим-ка… в docstring описываются параметры, а также их типы и выводимое значение к каждому типу…
В каком-то смысле я уже пользовался аннотациями типов — через docstring.
Не поймите меня неправильно: документировать ваш код при помощи docstrings хорошо и полезно, когда в компоненте заложено много логики. Есть стандартные форматы (выше я пользовался форматом документов NumPy) и они полезны тем, что помогают поддерживать соглашения по документации, а также могут интерпретироваться некоторыми IDE.
Однако при работе с простыми функциями использование полноценной строки docstring просто для описания аргументов и возвращения значений иногда кажется обходным маневром — поскольку (как я полагал) в Python вообще не предлагается каких-либо подсказок типов.
Иногда аннотации типов могут полностью заменить docstring, поскольку они — по моему мнению — очень чисто и просто документируют как ввод, так и вывод. В итоге ваш код становится более удобочитаемым как для вас самих, так и для ваших коллег.
Но подождите! Это еще не все.
Аннотации типов были добавлены в Python 3.5 вместе с модулем типизации.
Этот модуль позволяет аннотировать всевозможные типы (например, списки, словари, функции или генераторы) и даже поддерживает вложения, дженерики, а также возможность определять собственные специальные типы.
Не буду сейчас в деталях разбирать модуль typing , но просто поделюсь некоторыми моими недавними наблюдениями: аннотации типов могут использоваться для генерации кода.
Возьмем, к примеру, namedtuple . Это структура данных из модуля collections — точно как ChainMap, рассмотренный в статье A practical usage of ChainMap in Python.
Что делает namedtuple : он генерирует класс, чьи экземпляры действуют как кортежи (они неизменяемые) но допускают доступ к атрибутам через точечное представление.
Как правило, namedtuple используется следующим образом:
from collections import namedtuple Point = namedtuple("Point", "x y") point = Point(x=1, y=5) print(point.x) # 1
Ранее, говоря о важности документирования ввода и вывода, мы сталкивались с подобным случаем: здесь нам чего-то не хватает. Мы не знаем, каковы типы x и y .
Оказывается, в модуле typing есть эквивалент namedtuple , называемый NamedTuple , он позволяет использовать аннотации типов.
Давайте заново определим класс Point с применением NamedTuple :
from typing import NamedTuple class Point(NamedTuple): x: int y: int point = Point(x=4, y=0) print(point.x) # 4
Мне нравится. Вот таким красивым, чистым и выразительным может быть код Python.
Обратите внимание: Point используется точно так, как и ранее, с той оговоркой, что теперь работать удобнее, поскольку код гораздо легче читается – и наши IDE и редакторы помогут нам обнаруживать потенциальные ошибки при написании типов. Также это делается благодаря статическим инструментам проверки, например, MyPy (и различным вариантам их интеграции).
При помощи аннотаций типов можно делать еще множество классных вещей, особенно теперь, когда они входят в ядро языка Python.
Например, в Python 3.7 были введены классы данных, потрясающий новый вариант генерируемых классов для простого, но при этом эффективного хранения данных. Однако, о них стоило бы написать отдельную статью.
А что с философской точки зрения?
Python проектировался как динамический язык программирования, а мы теперь вводим в него статическую типизацию. Здесь уже пора задуматься о следующем:
Как это вписывается в философию языка?
Может быть, разработчики ядра Python наконец осознали, что динамическая типизация была ошибочным выбором?
Не вполне. Попробуйте погуглить python philosophy – и найдете следующий документ:
«Дзен Python» — это документ, направляющий всю философию языка Python.
По-моему, аннотации типов на 100% вписываются в философию Python. Вот некоторые истины, воплощенные в них.
Явное лучше неявного.
В принципе, именно ради этого и изобретались аннотации типов. Просто сравните:
def process(data): do_stuff(data)
from typing import List, Tuple def process(data: List[Tuple[int, str]]): do_stuff(data)
Простое лучше сложного.
В простых случаях подсказки типов гораздо удобнее полноразмерных строк docstring.
Удобочитаемость важна
Об этом мы уже поговорили.
Должен быть один — и желательно всего один — способ это сделать.
Этот момент реализуется при помощи строгого (но при этом простого) синтаксиса аннотаций типов. Они – верное средство, если вы хотите документировать и поддерживать статические типы в Python!
Лучше поздно, чем никогда
По-моему, аннотации типов полностью поменяли правила игры:
- Благодаря им мой код стал лучше.
- Они предоставляют стандартный способ документирования ввода и вывода, помогая вам и другим людям гораздо лучше понимать код и увереннее о нем рассуждать.
- Также они открывают новые способы писать код более чисто и лаконично.
Если вы пока еще не пользуетесь аннотациями типов – попробуйте! О них есть много отличной информации — изучайте для начала.
Если вы уже пользуетесь аннотациями типов — и, надеюсь, хорошо закопались в них, помогите с их популяризацией!
- Блог компании Издательский дом «Питер»
- Python
- Программирование
- Совершенный код
- ООП