С появлением подсказок типов (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!
И наконец…
Лучше поздно, чем никогда
По-моему, аннотации типов полностью поменяли правила игры:
Благодаря им мой код стал лучше.
Они предоставляют стандартный способ документирования ввода и вывода, помогая вам и другим людям гораздо лучше понимать код и увереннее о нем рассуждать.
Также они открывают новые способы писать код более чисто и лаконично.
Если вы пока еще не пользуетесь аннотациями типов – попробуйте! О них есть много отличной информации - изучайте для начала.
Если вы уже пользуетесь аннотациями типов — и, надеюсь, хорошо закопались в них, помогите с их популяризацией!
Комментарии (21)
Myxach
25.02.2022 10:35-1А Шаблоны или generic есть в пютоне? То-есть, в первой версии я вижу код, который складывает str значение двух любых типов.
Может и 1+2=12, может и [1,2] + [2,3]=[1,2][2,3], может и 1.2+2.3=1.22.3, а после добавление аннотации, код только с int начал работать, то-есть для float нужно писать второй раз такую функцию
~~~
А, дочитал, есть, ну ок
lxsmkv
25.02.2022 11:07В свое время, до третьей версии питона, я отмечал ожидаемый тип переменной в имени переменной: temp__int, urls__list_str, updated_data__list_any, response__sparse_dict_int_str. Тип переменной именно суффиксом, а не префиксом, для удобства работы с автодополнением в редакторе. Также и для функций, тип возвратного значения заносится в имя.
Эти соглашения в readme потом пишутся одним абзацем. Вообще не о чем говорить, а польза большая. Хотя бы то, что ты явно задумываешься о типах переменных и можешь избежать оплошностей по неосторожности, которые всплыли бы потом, во время исполненияpfffffffffffff
25.02.2022 12:02+7С использованием аннотации ide тебе подскажет если ты накосячил, в вашем подходе нет такой фичи
Andrey_Solomatin
25.02.2022 11:29а мы теперь вводим в него статическую типизацию
Аннотации типов это просто фича языка, которая позволяет вам более точно выразить ваши ожидания от типов.
nerudo
25.02.2022 11:35+2def concat(a: int, b: int) -> str: return a+b
Как придумать еще один вариант записи комментариев, который может не отражать содержимого.santjagocorkez
25.02.2022 20:52+2Так можно вообще на любой язык набросить. Отстрелить себе намеренно ногу и публично предать язык анафеме: вот, ногу мне повредил.
Murtagy
25.02.2022 22:52ну так на базу нужно натравить тайп чекер. просто текст кода понятно что можно что угодно написать.
Andrey_Solomatin
25.02.2022 11:43+5Я стараюсь документировать код, а аннотиции типов это самая легкая форма документации.
Есть поддержка со стороны языка и инструментов.
Забыли сказать про главную проблему типизации.
Если объект на тип который вы ссылаетесь, еще не определён, можно его тип взять в кавычки.class Foo: def foo(self) -> "Foo": return self
Иногда аннотации позволяют выразить то, что сложно сделать в коде.
Mapping это неизменяемый словарь. Родной реализации такого словаря в Питоне нет, писать самому или устанавливать зависимости не всегода хочется.
В данном случае я явно указываю свои ожидания, что возвращаемое значние менять не надо.from typing import Mapping def get_config() -> Mapping: config = {} ... return config
yesworldd
25.02.2022 13:43+6Забыли сказать про главную проблему типизации.
Если объект на тип который вы ссылаетесь, еще не определён, можно его тип взять в кавычки.Теперь в 3.10 можно указывать в методе класса, сам класс.
class Foo: def do_something() -> Foo: return Foo()
poofeg
26.02.2022 22:19А в 3.11 нам завезут тип Self, который решает проблему типизации при наследовании, когда метод возвращает именно self, и у наследника это по сути будет уже другой тип.
ivanych
27.02.2022 00:08А как в аналогичном случае предлагается аннотировать аргументы?
Допустим, аргумент - экземпляр класса, который в текущей функции и вообще в текущем модуле не определён. Как аннотировать такой аргумент?
WASD1
25.02.2022 16:55+3Когда говорят о типах - примеры это всегда самое сложное.
Потому, что вы вводите типы, чтобы сделать код более хм... разумным.
Но когда вы приводите плохой пример - вы портите всё впечатление от разумных доводов.
Скажем вместо того, чтобы типизировать вот эту функцию - её просто (ровно в таком виде) не надо писать никогда:def concat(a: int, b: int) -> str: return str(a) + str(b)
Т.е. может существовать либо "библиотечная" ф-ия:def concat(a: str, b: str) -> str: return a + b ..... x = concat(str(a), str(b))
Либо прикладная функция:
def getForm15Result(a: int, b: int) -> str: return str(a) + str(b)
Vindicar
25.02.2022 17:34+1Аннотации плюс рефлексия позволяют делать чертовски изящные штуки со стороны библиотек. Пример — discord.py, либа для создания чат-ботов под Discord.
Там аннотации типов используются для прозрачного разбора и преобразования аргументов команды. Например:@bot.command(pass_context=True) def plus(context, x: int, y: int): context.send(f'{x} + {y} = {x+y}')
При вводе команды "!plus 2 3", библиотека попытается разбить строку на правильное число параметров и конвертировать их в нужные типы. Если получится — то вызовет обработчик plus() с правильными аргументами.
Для пользователя библиотеки это дико удобно, по-моему.
SadOcean
Типы, как и читаемый код в целом, нужны не просто для других людей.
Через пару месяцев ты сам становишься "другим человеком"
Поражает, что питон, декларируя, что явное лучше неявного, полагается на неясность в выводе типов.