Pull to refresh

Почему я начал использовать аннотации типов в Python – и вам тоже советую

Reading time7 min
Views19K
Original author: Florimond Manca

С появлением подсказок типов (type hints) в Python 3.5+ добавилась опциональная статическая типизация – поэтому эти подсказки так мне нравятся. Теперь я аннотирую ими все мои проекты.

Фрагмент кода, carbon.now.sh
Фрагмент кода, carbon.now.sh

Когда еще в 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. Что этот фрагмент принимает в качестве ввода?

  2. Как он обрабатывает ввод?

  3. Что этот код выдает в качестве вывода?

По мере того, как я все больше читал код коллег – в том числе, сложный унаследованный код — я осознал, что аннотации типов на самом деле крайне полезны. Аннотации типов позволили мне ответить на вопросы 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 – и найдете следующий документ:

PEP 20 - The Zen of Python

«Дзен 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!

И наконец…

Лучше поздно, чем никогда

По-моему, аннотации типов полностью поменяли правила игры:

  • Благодаря им мой код стал лучше.

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

  • Также они открывают новые способы писать код более чисто и лаконично.

Если вы пока еще не пользуетесь аннотациями типов – попробуйте! О них есть много отличной информации - изучайте для начала.

Если вы уже пользуетесь аннотациями типов — и, надеюсь, хорошо закопались в них, помогите с их популяризацией! 

Tags:
Hubs:
Total votes 29: ↑28 and ↓1+32
Comments25

Articles

Information

Website
piter.com
Registered
Founded
Employees
201–500 employees
Location
Россия