Как стать автором
Обновить

Комментарии 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

Да, совсем упустил. Если я правильно понимаю, речь о PEP-563?

Да, оно.

Спасибо, добавил в статью

Ой! Прямо как недавний хайп и натирание эбонитовой палки на статическую типизацию в JS. А зачем она нужна?

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

Описание типов позволяет писать более понятный код и выловить ошибки при передаче неправильных параметров сразу на этапе написания кода, а не при прогоне тестов, или, что ещё хуже, на рабочем боксе.
Предсказуемо. В 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()))
Как данный пример отрицает наличие в Python строгой типизации? Подозреваю, что передать число всё равно не выйдет. А передача даты соответствует описанной сигнатуре функции.
Вы что-то путаете. Строгая типизация — это когда нельзя поменять тип переменной после ее объявления, а также отсутствует неявное преобразование типов (хотя на самом деле термин еще более обширный ru.wikipedia.org/wiki/%D0%A1%D0%B8%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F_%D0%B8_%D1%81%D0%BB%D0%B0%D0%B1%D0%B0%D1%8F_%D1%82%D0%B8%D0%BF%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F). В питоне можно поменять тип переменной после ее объявления, но также отсутствует неявное приведение типов (например, не получится сложить строку и число, как это можно сделать в JS). Хотя по ссылке выше написано, что Питон имеет сильную динамическую типизацию.
Вот это работает прекрасно
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 не объявлен в сигнатуре. А работает оно потому, что аннотации никак на выполнение не влияют. Вы можете в них что угодно прописать, там можно будет любой тип передать.
А работает оно потому, что аннотации никак на выполнение не влияют. Вы можете в них что угодно прописать, там можно будет любой тип передать.
Вы правы, я слегка напутал.
Привык к TS с его фазой компиляции, который ругнётся на попытку вот так передать number, когда указан тип string | date.

Я имел ввиду немного друго: если метод делает какие-то операции со строкой (конкатенация, поиск подстроки или ещё что-то) и сам не проверяет типы, то передав туда число, он может просто сломаться. В 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**"
Теперь я вас понял) Спасибо
> 2. У вас есть функция, принимающая в качестве аргумент объект Date или целое число (timestamp) или строку ('2019.01.01') какой тип выберете.

Если у вас реально возникает такая необходимость, в питоне это решается объявлением списка типов, которые может иметь аргумент.
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 или что вы используете в работе, такой возможности нет, это проблема конкретной реализации. В питоне это сделано грамотно и вызывает только положительные чувства и никем не навязывается.
Подозреваю, что в JS/TS такой возможности нет, и это печально.
Да нет, в TS можно аналогично объявить сигнатуру функции:
function foo(arg: string | Date) { ... }
Теперь про плоскость в которой лежат преимущества статической типизации
Вы очень лихо притянули преимущества интерфейсов к статической типизации.

А что, статически типизированные функциональные языки (не имеющие интерфейсов за отсутствием классов), бесполезны?
Понятно что полезно. Я двумя руками за типизацию, но предполагаю, что в языках с изначально обязательной типизацией подходы к проектированию ПО несколько отличаются от языков где такая возможность является лишь дополнением и следовательно в JS/python etc. это обернется лишь указанием типов в переменных, аргументах функций и т.д. Это полезно, но вряд ли приведет к существенному улучшению качества написанного кода, ибо, по сути, лишь позволит узнать на этапе компиляции/трансляции о передаче в функцию аргумента с неправильным типов, но такая проблема возникает не часто, да и тесты её обычно решают.
> в языках с изначально обязательной типизацией

Питон — внезапно, язык с изначальной обязательной типизацией) Просто она динамическая, а не статическая. Ну и никто не мешает вам объявить тип с помощью аннотации. Да, оно не будет работать в рантайме, но подразумевается, что если вы используете тайпчекер и аннотации, до рантайма такие ошибки не дойдут.
Если же вам нужна именно статическая типизация, то тут просто нужно использовать другой язык, а не жаловаться, что в питон не завезли статику. Ну не будет ее там, поймите уже, это как раз одно из преимуществ языка. Он так спроектирован.
Мы от разных вещах говорим. ТЧК
Ничто не мешает рассматривать типизацию в TS не как «дополнение», а как неотъемлемую часть языка, включить --noImplicitAny и сразу проектировать опираясь на интерфейсы.
И по моему опыту, это приводит к значительному улучшению улучшению кодовой базы.
и сразу проектировать опираясь на интерфейсы.
Готовы переучить всех программистов в команде, в особенности тех, кто такого никогда не делал? Уверены, что качество кода после этого вырастет? В требования к вакансии будете писать: «Умение работать с включенным --noImplicitAny». Такие штуки в теории хорошо работают, а на практике все равно большинство будут говнякать как привыкли. Ну не объясните вы сферическому фронтендеру в вакууме зачем это нужно.
Готовы переучить всех программистов в команде, в особенности тех, кто такого никогда не делал?
Ну, на моих глазах люди переучивались вполне успешно, в т. ч. junior'ы. Требования к вакансиям особо не изменялись — интеграция в рабочий процесс дело неизбежное. Качество кода, поддерживаемость и расширяемость выросли очень сильно.

на практике все равно большинство будут говнякать как привыкли
Было бы желание (и отсутсвие командного контроля за качеством кода) — «говнякать» можно на любом языке.

Аннотации типов и тайпхинты как минимум помогают делать статический анализ кода, помогают делать правильный автокомплит "ну и т.д и т.п.". Не надо сравнивать тайпхинты и статически-типизированные языки. Идея в том, чтобы при минимуме оверхеда получить хоть какой-то контроль за кодом в полностью динамической среде, без потери этой самой динамичности.

По возможностям подозрительно напоминает JS/JAVA/etc-doc и реинкарнацию венгерской нотации :) То бишь чисто информационная вещь намертво вплетенная в код.
Прям инструкция: «Как перестать ломать мозг команде девелоперов, если программа написана на C++, а вы техподдержка, и пишете на встроенном python»
Нормальную перегрузку методов ведь не завезли?

Ну, если использования 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
Зарегистрируйтесь на Хабре, чтобы оставить комментарий