Подсказки типа великолепны! Но не так давно я играл в адвоката дьявола: я утверждал, что на самом деле эти подсказки способны раздражать, особенно программистов из старой школы Python.
Думаю, многие отнеслись к этому скептически, а потому посмотрим на одну полностью выдуманную ситуацию. Если явно не указано иное, всё в ней вымышлено. Редактируя текст, я понял, что в попытках 4–6 ошибок даже больше, чем предполагалось, но переписывать снова не буду.
Итак, именно вы поддерживаете популярную стороннюю библиотеку slowadd
. В ней много вспомогательных функций, декораторов, классов и метаклассов, но вот главная функция:
def slow_add(a, b):
time.sleep(0.1)
return a + b
Типы
Вы всегда работали с традиционной утиной типизацией: если a
и b
не складываются, то функция выдаёт исключение. Но только что вы отказались от поддержки Python 2, так что пользователи требуют подсказок типов.
Попытка №1
Вы добавляете простые подсказки:
def slow_add(a: int, b: int) -> int:
time.sleep(0.1)
return a + b
Все тесты пройдены, кодовая база соответствует требованиям mypy, и в примечаниях к релизу вы пишете: «Добавлена поддержка подсказок типов!».
Попытка №2
Пользователи сразу заваливают GitHub Issues жалобами! MyPy не работает, потому что в slow_add
передаются числа с плавающей точкой. Так сборка нарушается, а из-за внутренних политик организаций, по которым всегда необходимо увеличивать покрытие подсказок типа, откатиться до старой версии нельзя. Выходные ваших пользователей испорчены.
Вы исследуете проблему, и оказывается, что для цепочки типов ints -> float -> complex
MyPy поддерживает совместимость утиной типизации. Круто!
Вот новый релиз:
def slow_add(a: complex, b: complex) -> complex:
time.sleep(0.1)
return a + b
Забавно, что это нотка MyPy, а не стандарт PEP…
Попытка №3
Пользователи благодарны за скорость, но через пару дней один из них спрашивает, почему Decimal
больше не поддерживается. Вы заменяете тип complex
на Decimal
— и падают другие ваши тесты MyPy.
В Python 3 появились числовые абстрактные базовые классы, поэтому идеальный вариант — просто подсказывать типы как numbers.Number
.
Но MyPy не считает числами ни целые числа, ни числа с плавающей запятой, ни десятичные дроби. Почитав о typing, вы догадываетесь: дело в Decimals
и Union
:
def slow_add(
a: Union[complex, Decimal], b: Union[complex, Decimal]
) -> Union[complex, Decimal]:
time.sleep(0.1)
return a + b
Оx, нет! Теперь MyPy жалуется, что в Decimal нельзя добавить другие типы чисел. Ну, в любом случае, вы хотели не этого… Ещё немного чтения — и вы попробуете перегрузку:
@overload
def slow_add(a: Decimal, b: Decimal) -> Decimal:
...
@overload
def slow_add(a: complex, b: complex) -> complex:
...
def slow_add(a, b):
time.sleep(0.1)
return a + b
А теперь строгий MyPy жалуется на пропущенную в slow_add
аннотацию типа. Прочитав об этой проблеме вы понимаете, что @overload
полезна только пользователям, а тело функции больше не проверяется в mypy. Хорошо, что в обсуждении проблемы нашёлся пример реализации решения проблемы:
T = TypeVar("T", Decimal, complex)
def slow_add(a: T, b: T) -> T:
time.sleep(0.1)
return a + b
Попытка №4
Вы выкатываете новый релиз. Через несколько дней начинают жаловаться всё больше пользователей. Очень увлечённый пользователь объясняет особо критичный случай применения с кортежами: slow_add((1, ), (2, ))
. А добавлять каждый тип раз за разом не хочется, должен же быть способ лучше!
Вы изучаете протоколы, переменные типа и только позиционные параметры… уф… много всего, но теперь-то должно быть идеально:
T = TypeVar("T")
class Addable(Protocol):
def __add__(self: T, other: T, /) -> T:
...
def slow_add(a: Addable, b: Addable) -> Addable:
time.sleep(0.1)
return a + b
Отвлечёмся слегка
Вы вновь выкатываете новый релиз, отмечая в Release Notes, что «теперь поддерживается любой добавляемый тип».
Пользователь кортежа снова сразу же жалуется и говорит, что подсказки не работают для кортежей длиннее, вот таких: slow_add((1, 2), (3, 4))
. Это странно, ведь вы проверили несколько длин кортежей.
После дебага пользовательской среды пробежками «туда-обратно» по GitHub вы увидели, что pyright выдаёт код выше как ошибку, а MyPy — нет, даже в строгом режиме. Предположительно MyPy ведёт себя правильно, поэтому вы продолжаете блаженствовать, не обращая внимания на фундаментальную ошибку.
Если MyPy работает неверно, то Pyright явно должен выдавать ошибку. Об этом я сообщил в оба проекта, и подробности о решении, если вам интересно, объяснил мейнтейнер. К сожалению, эти подробности не учитывались до «Попытки №7».
Попытка №5
Неделю спустя пользователь сообщает о проблеме: в последнем релизе говорится, что «теперь поддерживается любой добавляемый тип», но у них есть куча классов, реализовать которые можно только с помощью __radd__
, а новая версия выдаёт ошибки typing.
Вы пробуете несколько подходов. Лучше всего решает проблему этот:
T = TypeVar("T")
class Addable(Protocol):
def __add__(self: T, other: T, /) -> T:
...
class RAddable(Protocol):
def __radd__(self: T, other: Any, /) -> T:
...
@overload
def slow_add(a: Addable, b: Addable) -> Addable:
...
@overload
def slow_add(a: Any, b: RAddable) -> RAddable:
...
def slow_add(a: Any, b: Any) -> Any:
time.sleep(0.1)
return a + b
Досадно, что теперь у MyPy нет согласованного подхода к телу функции. А ещё не вышло полностью выразить условие, что когда b — RAddable
, a
не должно быть того же типа, потому что аннотации типов Python ещё не поддерживают исключения типов.
Попытка №6
Пару дней спустя новый пользователь жалуется, что получает ошибки подсказки типа, пытаясь возвести вывод нашей основной функции в степень: pow(slow_add(1, 1), slow_add(1, 1))
. Это не так уж плохо, вы быстро понимаете, что проблема заключается в аннотации протоколов. На самом деле аннотировать нужно не протоколы, а переменные типа:
T = TypeVar("T")
class Addable(Protocol):
def __add__(self: T, other: T, /) -> T:
...
A = TypeVar("A", bound=Addable)
class RAddable(Protocol):
def __radd__(self: T, other: Any, /) -> T:
...
R = TypeVar("R", bound=RAddable)
@overload
def slow_add(a: A, b: A) -> A:
...
@overload
def slow_add(a: Any, b: R) -> R:
...
def slow_add(a: Any, b: Any) -> Any:
time.sleep(0.1)
return a + b
Попытка №7
Пользователь кортежа снова с нами! Теперь он говорит, что MyPy в строгом режиме жалуется на выражение slow_add((1,), (2,)) == (1, 2)
:
Non-overlapping equality check (left operand type: "Tuple[int]", right operand type: "Tuple[int, int]")
Вы понимаете, что ничего не можете гарантировать в отношении типа возвращаемого из произвольного __add__
или __radd__
значения, а потому начинаете щедро разбрасываться Any
:
class Addable(Protocol):
def __add__(self: "Addable", other: Any, /) -> Any:
...
class RAddable(Protocol):
def __radd__(self: "RAddable", other: Any, /) -> Any:
...
@overload
def slow_add(a: Addable, b: Any) -> Any:
...
@overload
def slow_add(a: Any, b: RAddable) -> Any:
...
def slow_add(a: Any, b: Any) -> Any:
time.sleep(0.1)
return a + b
Попытка №8
Пользователи просто сошли с ума! Хорошие автоматические предположения о типе, которые в прошлом релизе их IDE были, теперь исчезли! Ну, вы не можете подсказывать тип всего, но могли бы включить подсказки встроенных типов и, может быть, некоторых типов стандартной библиотеки, к примеру Decimal:
Вы решаете, что можно положиться на некоторые утиные типы MyPy, но проверяете этот код:
@overload
def slow_add(a: complex, b: complex) -> complex:
...
И понимаете, что MyPy выдаёт ошибку на что-то вроде slow_add(1, 1.0).as_integer_ratio()
. Итак, в конечном счёте вы реализуете:
class Addable(Protocol):
def __add__(self: "Addable", other: Any, /) -> Any:
...
class RAddable(Protocol):
def __radd__(self: "RAddable", other: Any, /) -> Any:
...
@overload
def slow_add(a: int, b: int) -> int:
...
@overload
def slow_add(a: float, b: float) -> float:
...
@overload
def slow_add(a: complex, b: complex) -> complex:
...
@overload
def slow_add(a: str, b: str) -> str:
...
@overload
def slow_add(a: tuple[Any, ...], b: tuple[Any, ...]) -> tuple[Any, ...]:
...
@overload
def slow_add(a: list[Any], b: list[Any]) -> list[Any]:
...
@overload
def slow_add(a: Decimal, b: Decimal) -> Decimal:
...
@overload
def slow_add(a: Fraction, b: Fraction) -> Fraction:
...
@overload
def slow_add(a: Addable, b: Any) -> Any:
...
@overload
def slow_add(a: Any, b: RAddable) -> Any:
...
def slow_add(a: Any, b: Any) -> Any:
time.sleep(0.1)
return a + b
Как уже говорилось, MyPy не использует сигнатуры перегрузок и сравнивает их с телом функции, поэтому все эти подсказки типов на предмет точности вы должны проверить сами, вручную.
Попытка №9
Несколько месяцев спустя пользователь говорит, что использует встраиваемую версию Python, и в ней нет Decimal. Зачем же ваш пакет вообще его импортирует? Итак, теперь код выглядит так:
from __future__ import annotations
import time
from typing import TYPE_CHECKING, Any, Protocol, TypeVar, overload
if TYPE_CHECKING:
from decimal import Decimal
from fractions import Fraction
class Addable(Protocol):
def __add__(self: "Addable", other: Any, /) -> Any:
...
class RAddable(Protocol):
def __radd__(self: "RAddable", other: Any, /) -> Any:
...
@overload
def slow_add(a: int, b: int) -> int:
...
@overload
def slow_add(a: float, b: float) -> float:
...
@overload
def slow_add(a: complex, b: complex) -> complex:
...
@overload
def slow_add(a: str, b: str) -> str:
...
@overload
def slow_add(a: tuple[Any, ...], b: tuple[Any, ...]) -> tuple[Any, ...]:
...
@overload
def slow_add(a: list[Any], b: list[Any]) -> list[Any]:
...
@overload
def slow_add(a: Decimal, b: Decimal) -> Decimal:
...
@overload
def slow_add(a: Fraction, b: Fraction) -> Fraction:
...
@overload
def slow_add(a: Addable, b: Any) -> Any:
...
@overload
def slow_add(a: Any, b: RAddable) -> Any:
...
def slow_add(a: Any, b: Any) -> Any:
time.sleep(0.1)
return a + b
TL;DR
Превратить даже самую простую функцию, которая полагается на утиную типизацию, в полезную функцию с подсказкой типа — это может быть мучительно сложно. Пожалуйста, всегда проявляйте эмпатию, когда просите кого-то обновить код так, как, по вашему мнению, он должен работать.
Пока я писал этот пост, я многое узнал о подсказках типов. Пожалуйста, попробуйте найти крайние случаи, когда мои подсказки типов неверны или могут стать лучше. Это хорошее упражнение.
Ещё поправка: я сдался перед исправлениями поздней ночью, но умные люди заметили ошибки! У меня есть «десятая попытка» исправить их. Но pyright жалуется, ведь мои перегрузки перекрываются. Но я не думаю, что есть способ выразить желаемое в аннотациях без перекрытия.
Mypy жалуется, что иногда ранее размещённый код пользователей выдаёт ошибку comparison-overlap, что интересно. Но, похоже, отсутствие перекрытий пользовательского кода здесь может увидеть pyright.
Я опишу проблемы pyright и mypy на Github, хотя в основном они могут быть обусловлены архитектурой, то есть оказаться ограничением текущего состояния подсказок типов Python в принципе:
T = TypeVar("T")
class Addable(Protocol):
def __add__(self: "Addable", other: Any, /) -> Any:
...
class SameAddable(Protocol):
def __add__(self: T, other: T, /) -> T:
...
class RAddable(Protocol):
def __radd__(self: "RAddable", other: Any, /) -> Any:
...
class SameRAddable(Protocol):
def __radd__(self: T, other: Any, /) -> T:
...
SA = TypeVar("SA", bound=SameAddable)
RA = TypeVar("RA", bound=SameRAddable)
@overload
def slow_add(a: SA, b: SA) -> SA:
...
@overload
def slow_add(a: Addable, b: Any) -> Any:
...
@overload
def slow_add(a: Any, b: RA) -> RA:
...
@overload
def slow_add(a: Any, b: RAddable) -> Any:
...
def slow_add(a: Any, b: Any) -> Any:
time.sleep(0.1)
return a + b
Вот как непросто может быть в IT. Но мы поможем освоить нужную теорию, приобрести полезный опыт и, если трудности вас не пугают, устроиться на работу в сфере информационных технологий:
Data Science и Machine Learning
- Профессия Data Scientist
- Профессия Data Analyst
- Курс «Математика для Data Science»
- Курс «Математика и Machine Learning для Data Science»
- Курс по Data Engineering
- Курс «Machine Learning и Deep Learning»
- Курс по Machine Learning
Python, веб-разработка
- Профессия Fullstack-разработчик на Python
- Курс «Python для веб-разработки»
- Профессия Frontend-разработчик
- Профессия Веб-разработчик
Мобильная разработка
Java и C#
- Профессия Java-разработчик
- Профессия QA-инженер на JAVA
- Профессия C#-разработчик
- Профессия Разработчик игр на Unity
От основ — в глубину
А также
Комментарии (9)
lea
00.00.0000 00:00+1Python придерживается философии «должен существовать один — и, желательно, только один — очевидный способ сделать это».
Но что-то пошло не так...
outlingo
00.00.0000 00:00+14А всё потому, что кое-кому захотелось реализовать всё в одной функции.
Не принимая в расчет того, что сложение целых чисел, вещественных, комплексных и вообще не чисел это не "операция сложения" а "разные операции сложения".
Чему, кстати, учат на курсе алгебры в ВУЗах, когда поясняют что сложение не привычное всем арифметическое сложение, а просто некоторая абстрактная и в общем случе не обязательно даже коммутативная операция (это к слову о ненужности профильного образования)
funca
00.00.0000 00:00+2Пример некоммутативного сложения в python: "a" + "b" == "ab", "b" + "a" == "ba"
artemev
00.00.0000 00:00+3Это не сложение, а конкатенация строк
funca
00.00.0000 00:00+2Вообще вы правы - в математике сложение коммутативно по определению. Но напрограммировать плюс можно как угодно. У @Stefanio есть целая статья про это https://habr.com/ru/post/655059/.
0xd34df00d
00.00.0000 00:00+2А все потому, что тяжело прикручивать типизацию пост-фактум.
В том же хаскеле спокойно можно написать аналогичную функцию, работающую и с целыми, и с вещественными, и с комплексными, и с заранее неизвестными пользовательскими типами.
funca
00.00.0000 00:00Так когда в хаскеле функция не проходит проверку типов, то разработчики идут искать ошибку в своем коде. А когда в питоне происходит то же самое, то они строчат ишью разработчикам тайпчекеров (и кстати нередко в этом действительно есть смысл).
fishHook
00.00.0000 00:00Подсказки типов могут раздражать, сейчас я это докажу
и... не доказал. Автору нужна была была функция которая применяет + к двум любым аргументам, а если операция не допустима, то вываливается исключение. Собственно, как в старые добрые времена. Ну вот вам такая функция
def any_add(a: Any, b: Any) -> Any:
return a + b
А почему автора это не раздражало пятнадцать лет назад, а когда добавили аннотации вдруг зараждражало? Вы меня извините, но такое чувство, что вы статью написали исключительн ради самолюбования - вот какие я хитрые штуки узнал позавчера.
sci_nov
Я бы на complex и остановился. Decimal им не работает... ишь! А дальше так это вообще гОниво какое-то...