Комментарии 33
На текущий момент невозможно описать сигнатуру функции с переменным числом параметров определенного типа или указать именованные аргументы.
В mypy_extensions есть для этого способ: mypy.readthedocs.io/en/latest/additional_features.html#extended-callable-types
Есть такое, да. Но в стандарте вот такой абзац и я в целом согласен с ним:
Since using callbacks with keyword arguments is not perceived as a common use case, there is currently no support for specifying keyword arguments with Callable. Similarly, there is no support for specifying callback signatures with a variable number of argument of a specific type.
1) Отсутствует импорт Any;
2) В питоне, начиная с 3.7 'forward declaration' работают, если попросить.
from __future__ import annotations
from typing import Any
class LinkedList:
data: Any
next: LinkedList
Описание типов позволяет писать более понятный код и выловить ошибки при передаче неправильных параметров сразу на этапе написания кода, а не при прогоне тестов, или, что ещё хуже, на рабочем боксе.
Описание типов позволяет писать более понятный код и выловить ошибки при передаче неправильных параметров сразу на этапе написания кода, а не при прогоне тестов, или, что ещё хуже, на рабочем боксе.Предсказуемо. В JS-типизации такие же аргументы. Больше похоже на мантру, натягивание совы на глобус и высасывание преимуществ из пальца, которые говорят люди, никогда не писавшие на языках со статической типизацией ибо в них (в языках со статической типизацией) преимущества лежат совсем в другой плоскости.
Примеры (я не питонист, поэтому на JS):
1. У вас есть функция принимающая в качестве аргумента строку, вы пишете:
function foo(arg: string)
Вроде неплохо, но:2. У вас есть функция, принимающая в качестве аргумент объект Date или целое число (timestamp) или строку ('2019.01.01') какой тип выберете. Нормальную перегрузку методов ведь не завезли?
Если первый пример — самодокументируемый, то на второй все равно придется писать доку. Я к тому, что вместе с преимуществами (не очень большими), вы до кучи получаете и пачку проблем.
Теперь про плоскость в которой лежат преимущества статической типизации:
Вы пишете на Java/C++. Вам нужно написать класс A, в конструктор этого класса передается объект класса B. Вспомнив про SOLID вы решили, что хардкодить B в конструкторе — не самая хорошая идея и нужно написать интерфейс, чтобы, в случае необходимости, у вас была возможность вместо объекта класса B передать любой другой объект с таким же интерфейсом, что заставляет вас заранее думать о снижении зависимости между различными частями вашей программы, что, в свою очередь, делает архитектуру вашей программы менее дерьмовой. Упс интерфейсов тоже не завезли (в TypeScrips завезли, но что-то в реальных проектах они не часто используются, в каком-нибудь PropTypes даже близко ничего нет, зато гонору хоть отбавляй).
Ну и т.д. и т.п. в том же духе.
Во-первых, хочу напомнить, что в питоне сильная типизация. Если Вы ожидаете в методе строку, вы не можете передать туда дату и надеяться, что все будет работать
Во-вторых, никто не мешает создать абстрактный класс с пустыми реализациями методов и использовать его как интерфейс,
В-третьих, есть PEP-544, который к сожалению ещё не принят, но внутри модуля typing уже используется аналогичные конструкции.
Вы не правы) Вот этот код прекрасно работает
import datetime
from typing import Union
def foo(start_date: Union[datetime.date, str]) -> datetime.date:
if isinstance(start_date, str):
start_date = datetime.datetime.strptime(start_date, '%Y.%m.%d')
return start_date
print(foo('2018.12.12'))
print(foo(datetime.datetime.now()))
Вот это работает прекрасно
import datetime
from typing import Union
def foo(start_date: Union[datetime.date, str]) -> datetime.date:
if isinstance(start_date, str):
start_date = datetime.datetime.strptime(start_date, '%Y.%m.%d')
if isinstance(start_date, int):
raise RuntimeError('Invalid argument type')
return start_date
print(foo('2018.12.12'))
print(foo(datetime.datetime.now()))
print(foo(1))
Хотя тип int не объявлен в сигнатуре. А работает оно потому, что аннотации никак на выполнение не влияют. Вы можете в них что угодно прописать, там можно будет любой тип передать.
Я имел ввиду немного друго: если метод делает какие-то операции со строкой (конкатенация, поиск подстроки или ещё что-то) и сам не проверяет типы, то передав туда число, он может просто сломаться. В js — слабая типизация и в большинстве случаев все будет работать (уж не знаю, корректно ли с точки зрения бизнес-логики).
Например, в python:
>>> def x(arg: str):
... return "**"+arg+"**"
...
>>> x("hello")
'**hello**'
>>> x(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in x
TypeError: can only concatenate str (not "int") to str
И в js:
-> function x(arg) {
return "**"+arg+"**"
}
<- undefined
-> x("hello")
<- "**hello**"
-> x(1)
<- "**1**"
Если у вас реально возникает такая необходимость, в питоне это решается объявлением списка типов, которые может иметь аргумент.
def foo(arg: Union[date, str]):
...
Подозреваю, что в JS/TS такой возможности нет, и это печально. Но это не проблема аннотации типов, а проблема реализации в конкретном языке.
> Упс интерфейсов тоже не завезли
Интерфейсы обычно используются, чтобы внести понимание, как можно с этим объектом работать и какие методы он имеет. В питоне для этого можно использовать абстрактные классы, либо те же тайпинги
class T1:
def foo1(self):
pass
def foo2(self):
pass
class T2:
def foo2(self):
pass
def foo3(self):
pass
SomeInterface = NewType('SomeInterface', Union[T1, T2])
def main(arg: SomeInterface):
arg.foo2()
main(T1())
main(T2())
Тут мы и объявили интерфейс SomeInterface и он уже выступает в качестве типа для тайпчекинга. Опять же, если в JS/TS или что вы используете в работе, такой возможности нет, это проблема конкретной реализации. В питоне это сделано грамотно и вызывает только положительные чувства и никем не навязывается.
Теперь про плоскость в которой лежат преимущества статической типизацииВы очень лихо притянули преимущества интерфейсов к статической типизации.
А что, статически типизированные функциональные языки (не имеющие интерфейсов за отсутствием классов), бесполезны?
Питон — внезапно, язык с изначальной обязательной типизацией) Просто она динамическая, а не статическая. Ну и никто не мешает вам объявить тип с помощью аннотации. Да, оно не будет работать в рантайме, но подразумевается, что если вы используете тайпчекер и аннотации, до рантайма такие ошибки не дойдут.
Если же вам нужна именно статическая типизация, то тут просто нужно использовать другой язык, а не жаловаться, что в питон не завезли статику. Ну не будет ее там, поймите уже, это как раз одно из преимуществ языка. Он так спроектирован.
И по моему опыту, это приводит к значительному улучшению улучшению кодовой базы.
и сразу проектировать опираясь на интерфейсы.Готовы переучить всех программистов в команде, в особенности тех, кто такого никогда не делал? Уверены, что качество кода после этого вырастет? В требования к вакансии будете писать: «Умение работать с включенным --noImplicitAny». Такие штуки в теории хорошо работают, а на практике все равно большинство будут говнякать как привыкли. Ну не объясните вы сферическому фронтендеру в вакууме зачем это нужно.
Готовы переучить всех программистов в команде, в особенности тех, кто такого никогда не делал?Ну, на моих глазах люди переучивались вполне успешно, в т. ч. junior'ы. Требования к вакансиям особо не изменялись — интеграция в рабочий процесс дело неизбежное. Качество кода, поддерживаемость и расширяемость выросли очень сильно.
на практике все равно большинство будут говнякать как привыклиБыло бы желание (и отсутсвие командного контроля за качеством кода) — «говнякать» можно на любом языке.
Аннотации типов и тайпхинты как минимум помогают делать статический анализ кода, помогают делать правильный автокомплит "ну и т.д и т.п.". Не надо сравнивать тайпхинты и статически-типизированные языки. Идея в том, чтобы при минимуме оверхеда получить хоть какой-то контроль за кодом в полностью динамической среде, без потери этой самой динамичности.
Нормальную перегрузку методов ведь не завезли?
Ну, если использования Union не хватает, то можно так (если параметров больше одного и не все комбинации допустимы)…
from typing import overload
@overload
def f(arg: int) -> None:
...
@overload
def f(arg: str) -> None:
...
def f(arg) -> None:
if isinstance(arg, int):
print('int')
elif isinstance(arg, str):
print('str')
else:
raise TypeError
if __name__ == '__main__':
f(1)
f('abc')
f(1.3) # Pycharm warning here.
Тогда стоит упомянуть про functools.singledispatch
:
>>> from functools import singledispatch
>>> @singledispatch
... def fun(arg, verbose=False):
... if verbose:
... print("Let me just say,", end=" ")
... print(arg)
>>> @fun.register
... def _(arg: int, verbose=False):
... if verbose:
... print("Strength in numbers, eh?", end=" ")
... print(arg)
...
>>> @fun.register
... def _(arg: list, verbose=False):
... if verbose:
... print("Enumerate this:")
... for i, elem in enumerate(arg):
... print(i, elem)
if __name__ == '__main__':
fun(1.1)
то ни pycharm ошибки не заметит, ни сам питон при запуске не отругается. Надо добавить type hints к «основной» функции. И я не очень понимаю, поможет ли такой подход, если допустимыми сигнатурами будут:
f(int, str)
f(str, int)
В таком случае можно совместить typing.overload
и functools.singledispatch
, хотя я бы посоветовал избегать такого кода
@overload
def f(arg: int, arg2: str) -> None:
...
@overload
def f(arg: str, arg2: int) -> None:
...
@singledispatch
def f(arg, arg2) -> None:
raise TypeError
@f.register
def f1(arg: int, arg2: str) -> None:
print('int, str')
@f.register
def f2(arg: str, arg2: int) -> None:
print('str, int')
if __name__ == '__main__':
f(1, "abc")
f('abc', 1)
f(1.3, 1.2) # Pycharm warning here.
В языке с динамической типизацией есть понятие диспетчеризации, а не перегрузки методов. В питоне не завезли даже multipledispatch, хотя эта идея отлично ложится на тайпхинты, то есть с помощью тайпхинтов можно красиво сделать диспетчеризацию. К сожалению, в питоне есть только singledispatсh, который особо и негде использовать, и он в том числе не применим к методам класса. В некоторых языках, например, в Julia на диспетчеризации строится многое и этот механизм встроен в сам язык. Для питона есть несколько библиотек с реализацией multipledispatсh, самая продвинутая и архитектурно грамотная реализация вот. Туда бы ещё только добавить поддержку аннотаций в полной мере, чтобы можно было делать как-то так:
@dispatch
def func(*args, **kwargs):
"""Default function
"""
@func.dispatch
def func(foo: int, bar: str):
pass
@func.dispatch
def func(s: str, b: int, d: tuple):
pass
Введение в аннотации типов Python. Продолжение