Аннотация типов#

Утиная типизация#

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

def f(x):
  return x + 1

Например, приведенная выше функция f принимает один параметр x. Вызов этой функции с аргументом x типа X завершится без ошибок, если X поддерживает сложение с 1. Рассмотрим чуть более сложный пример.

def duck(x):
  x.swim()
  x.quack()
  return 

Такая функция duck отработает корректно, только если

  1. У x есть атрибут swim. Иначе, AttributeError;

  2. Этот атрибут является методом (ведёт себя как функция). Иначе, TypeError;

  3. Этот метод может быть вызван без параметров. Иначе, TypeError.

  4. То же самое про quack: есть атрибут, он является методом и может быть вызван без параметров.

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

Таким образом при передаче объекта в функцию важен не конкретный тип объекта, а набор действий, который над этим объектом можно произвести. Такую типизацию называют утиной в честь известного “утиного теста”:

Если это выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, и есть утка.

На примере с функцией duck это выражение перекладывается приблизительно так: если x имеет метод swim, имеет метод quack, и оба они могут быть вызваны без аргументов, то это, вероятно, объект допустимого для функции duck типа.

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

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

Расширенный синтаксис сигнатуры функции#

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

Например, если мы предполагаем, что функция f всегда должна вызываться с целочисленным аргументом x, то можно объявить её так.

def f(x: int):
  return x + 1

Warning

Такое объявление функции не значит, что её можно вызвать только с целочисленным аргументом: на этапе исполнения программы аннотация типов не используется интерпретатором python для проверки типов аргументов на соответствие типам параметров при вызове функции.

Функция f в примере выше очевидно вернет целочисленное значение, если ей передать целое число. Это тоже можно обозначить: для этого используется комбинация символов ->.

def f(x: int) -> int:
  return x + 1

Warning

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

Пример в ячейке ниже демонстрирует, что объявленную “целочисленную” функцию f можно вызвать и с действительным числом.

def f(x: int) -> int:
    return x + 1

x = 3.14
print(f(x))
4.140000000000001

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

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

Преимущества статической типизации#

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

Обнаружение ошибок#

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

#include<iostream>
#include<string>

int f(int x){
    return x + 1;
}

int main(){
	std::string s = "abc";
	std::cout << f(s);
}

Его компиляция средствами g++ приводит к ошибке компиляции со следующим сообщением.

error: cannot convert 'std::__cxx11::string {aka std::__cxx11::basic_string<char>}' to 'int' for argument '1' to 'int f(int)'
  std::cout << f(s);
                  ^
../../_images/compilation_error.gif

Эта ошибка связана с тем, что функция f объявлена с целочисленным параметром x, а вызывается она со строковым значением.

Сравним это с аналогичным кодом на python.

def f(x):
    return x + 1

s = "abc"
print(f(s))

Запуск этого кода приведет к возникновению следующей ошибки.

Traceback (most recent call last):
  File ".\static_typing.py", line 5, in <module>
    print(f(s))
  File ".\static_typing.py", line 2, in f
    return x + 1
TypeError: can only concatenate str (not "int") to str
../../_images/execution_error.gif

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

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

../../_images/mypy_no_error.gif

Однако если добавить подсказку типа для параметра x следующим образом.

def f(x: int):
    return x + 1

s = "abc"
print(f(s))
../../_images/mypy_error.gif

Тогда mypy обнаружит несоответствие типа параметра x и переменной s.

Статический анализатор кода встроен В большинство современных IDE. Анимация ниже демонстрирует, что при добавлении аннотации типа параметра x, появляется индикации ошибки в строке с вызовом функции f.

../../_images/IDE_type_checking.gif

Документирование кода#

Рассмотрим следующий заголовок функции bisect.

def bisect(f, l, r):
    ...

Что делает эта функция bisect, и как правильно её вызывать? Из названия функции можно догадаться, что, наверное, она реализует метод бисекции. Но существует минимум два метода бисекции: а) поиск корня функции б) двоичный поиск элемента в отсортированном массиве. На реализацию какого из них мы смотрим?

Для контраста рассмотрим возможную сигнатуру схожей функции на языке С++.

double bisect(std::function<double(double)> f, double l, double r)

По сигнатуре сразу можно понять, что параметр f — функция. Разумно от сюда заключить, что это реализации именно метода бисекции для поиска корня функции, а не значения в массиве. Кроме этого, из сигнатуры можно понять, что f должна принимать и возвращать double. Т.е. сигнатура функции не только подсказывает пользователю, что она делает, но и как её правильно вызывать, тем самым осуществляя документирующую функцию.

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

from collections.abc import Callable

def bisect(f: Callable[[float], float], a: float, b: float) -> float:
    ...

Автозаполнение в IDE#

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

../../_images/cpp_hint.gif

В анимации выше среда разработки с каждой набранной буквой метода push_back подсказывает все более узкий список возможных продолжений, т.к. среде разработки известно, что переменная x является контейнером std::vector, все методы которого ей (среде разработки) тоже известны.

В случае с динамической типизацией наблюдается совершенно иная картина.

../../_images/python_no_hint.gif

При вызове функции f на месте переменной x может оказаться объект любого типа. Даже если программисту известно, что это всегда будет список, у которого есть метод append, то среде разработки это отнюдь не очевидно. Это приводит к тому, что она не может подсказать возможное продолжение, т.к. список возможных продолжений близок к безграничному.

Однако если подсказать среде разработке, что x — переменная типа list, то она начнет реагировать в соответствии.

../../_images/python_hint.gif