Wpisz adnotacje dla * args i ** kwargs

158

Wypróbowuję adnotacje typu Pythona z abstrakcyjnymi klasami bazowymi, aby napisać niektóre interfejsy. Czy istnieje sposób na dodanie adnotacji do możliwych typów *argsi **kwargs?

Na przykład, jak można by wyrazić, że sensowne argumenty funkcji to jeden intlub dwa ints? type(args)daje Tuplewięc przypuszczam, że dodam adnotację jako Union[Tuple[int, int], Tuple[int]], ale to nie działa.

from typing import Union, Tuple

def foo(*args: Union[Tuple[int, int], Tuple[int]]):
    try:
        i, j = args
        return i + j
    except ValueError:
        assert len(args) == 1
        i = args[0]
        return i

# ok
print(foo((1,)))
print(foo((1, 2)))
# mypy does not like this
print(foo(1))
print(foo(1, 2))

Komunikaty o błędach od mypy:

t.py: note: In function "foo":
t.py:6: error: Unsupported operand types for + ("tuple" and "Union[Tuple[int, int], Tuple[int]]")
t.py: note: At top level:
t.py:12: error: Argument 1 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"
t.py:14: error: Argument 1 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"
t.py:15: error: Argument 1 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"
t.py:15: error: Argument 2 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"

To ma sens, że mypy nie lubi tego dla wywołania funkcji, ponieważ oczekuje, że tuplew samym wywołaniu będzie występować a . Dodatek po rozpakowaniu daje też błąd pisarski którego nie rozumiem.

W jaki sposób można opisać rozsądne typy dla *argsi **kwargs?

Prakseolityczny
źródło

Odpowiedzi:

167

W przypadku zmiennych pozycyjnych arguments ( *args) i zmiennych słów kluczowych arguments ( **kw) wystarczy określić oczekiwaną wartość tylko dla jednego takiego argumentu.

Z dowolnych list argumentów i wartości domyślne argumentów odcinku od typu Hints PEP:

Arbitralne listy argumentów mogą być również opatrzone adnotacjami, tak aby definicja:

def foo(*args: str, **kwds: int): ...

jest akceptowalny i oznacza to, że np. wszystkie poniższe reprezentują wywołania funkcji z prawidłowymi typami argumentów:

foo('a', 'b', 'c')
foo(x=1, y=2)
foo('', z=0)

Więc chciałbyś określić swoją metodę w następujący sposób:

def foo(*args: int):

Jeśli jednak twoja funkcja może akceptować tylko jedną lub dwie wartości całkowite, nie powinieneś jej *argsw ogóle używać, użyj jednego jawnego argumentu pozycyjnego i drugiego argumentu słowa kluczowego:

def foo(first: int, second: Optional[int] = None):

Teraz twoja funkcja jest w rzeczywistości ograniczona do jednego lub dwóch argumentów i oba muszą być liczbami całkowitymi, jeśli zostały określone. *args zawsze oznacza 0 lub więcej i nie można go ograniczyć wskazówkami typu do bardziej szczegółowego zakresu.

Martijn Pieters
źródło
1
Ciekawe, po co dodawać Optional? Czy coś się zmieniło w Pythonie, czy też zmieniłeś zdanie? Czy nadal nie jest to bezwzględnie konieczne ze względu na Nonedomyślne ustawienie?
Praxeolitic
10
@Praxeolitic tak, w praktyce automatyczna, domniemana Optionaladnotacja, gdy używasz Nonejako wartości domyślnej, utrudniła niektóre przypadki użycia i jest teraz usuwana z PEP.
Martijn Pieters
5
Oto link omawiający to dla zainteresowanych. Z pewnością brzmi to tak, jakby Optionalw przyszłości wymagane było wyraźne określenie.
Rick wspiera Monikę
W rzeczywistości nie jest to obsługiwane w przypadku Callable: github.com/python/mypy/issues/5876
Shital Shah
1
@ShitalShah: nie o to tak naprawdę chodzi w tym problemie. Callablenie obsługuje żadnych wzmianek o podpowiedziach typu *argslub **kwargs kropce . Ta konkretna kwestia dotyczy zaznaczania wywołań, które akceptują określone argumenty oraz dowolną liczbę innych , a więc używaj *args: Any, **kwargs: Anybardzo specyficznej wskazówki dotyczącej typu dla dwóch typów catch-all. W przypadkach, w których ustawisz *argsi / lub **kwargscoś bardziej szczegółowego, możesz użyć pliku Protocol.
Martijn Pieters
26

Właściwym sposobem na to jest użycie @overload

from typing import overload

@overload
def foo(arg1: int, arg2: int) -> int:
    ...

@overload
def foo(arg: int) -> int:
    ...

def foo(*args):
    try:
        i, j = args
        return i + j
    except ValueError:
        assert len(args) == 1
        i = args[0]
        return i

print(foo(1))
print(foo(1, 2))

Zauważ, że nie dodajesz @overloadani nie wpisujesz adnotacji do rzeczywistej implementacji, która musi być ostatnia.

Będziesz potrzebować nowej wersji obu typingi mypy, aby uzyskać obsługę @overload poza plikami pośredniczącymi .

Możesz również użyć tego do zróżnicowania zwracanego wyniku w sposób, który wyraźnie określa, które typy argumentów odpowiadają typowi zwracanemu. na przykład:

from typing import Tuple, overload

@overload
def foo(arg1: int, arg2: int) -> Tuple[int, int]:
    ...

@overload
def foo(arg: int) -> int:
    ...

def foo(*args):
    try:
        i, j = args
        return j, i
    except ValueError:
        assert len(args) == 1
        i = args[0]
        return i

print(foo(1))
print(foo(1, 2))
chadrik
źródło
2
Podoba mi się ta odpowiedź, ponieważ dotyczy bardziej ogólnego przypadku. Patrząc wstecz, nie powinienem był używać wywołań funkcji (type1)vs (type1, type1)jako mojego przykładu. Może (type1)vs (type2, type1)byłby lepszym przykładem i pokazuje, dlaczego podoba mi się ta odpowiedź. Pozwala to również na różne typy zwrotów. Jednak w szczególnym przypadku, gdy masz tylko jeden typ zwracania i wszystkie twoje *argsi *kwargssą tego samego typu, technika w odpowiedzi Martjina ma więcej sensu, więc obie odpowiedzi są przydatne.
Praxeolitic
4
Jednak użycie *argsmaksymalnej liczby argumentów (tutaj 2) jest nadal błędne .
Martijn Pieters
1
@MartijnPieters Dlaczego tutaj *argskoniecznie jest źle? Jeśli oczekiwane wywołania były (type1)vs (type2, type1), liczba argumentów jest zmienna i nie ma odpowiedniej wartości domyślnej dla argumentu końcowego. Dlaczego ważne jest, aby było maksimum?
Praxeolitic
1
*argsnaprawdę istnieje dla zera lub więcej , nieograniczonych, jednorodnych argumentów lub dla „przekazania ich nietkniętych” argumentów typu catch-all. Masz jeden wymagany argument i jeden opcjonalny. Jest to zupełnie inne i zwykle jest obsługiwane przez nadanie drugiemu argumentowi domyślnej wartości wartownika, aby wykryć, że została pominięta.
Martijn Pieters
3
Po przyjrzeniu się PEP wyraźnie nie jest to zamierzone użycie @overload. Chociaż ta odpowiedź pokazuje interesujący sposób indywidualnego opisywania typów *args, jeszcze lepszą odpowiedzią na pytanie jest to, że nie należy tego w ogóle robić.
Praxeolitic
20

Jako krótki dodatek do poprzedniej odpowiedzi, jeśli starasz się używać mypy na Pythonie 2 pliki i trzeba używać komentarzy, aby dodać typy zamiast adnotacji, trzeba poprzedzić typy do argsi kwargsz *i **odpowiednio:

def foo(param, *args, **kwargs):
    # type: (bool, *str, **int) -> None
    pass

Jest to traktowane przez mypy jako to samo, co poniżej, wersja Pythona 3.5 foo:

def foo(param: bool, *args: str, **kwargs: int) -> None:
    pass
Michael0x2a
źródło