Geben Sie Anmerkungen für * args und ** kwargs ein

157

Ich probiere Pythons Typanmerkungen mit abstrakten Basisklassen aus, um einige Schnittstellen zu schreiben. Gibt es eine Möglichkeit, die möglichen Arten von *argsund mit Anmerkungen zu versehen **kwargs?

Wie würde man zum Beispiel ausdrücken, dass die sinnvollen Argumente für eine Funktion entweder ein intoder zwei ints sind? type(args)gibt Tupleso meine Vermutung , die Art , wie mit Anmerkungen versehen war Union[Tuple[int, int], Tuple[int]]nicht, aber das funktioniert nicht.

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))

Fehlermeldungen von 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]]"

Es ist sinnvoll, dass mypy dies für den Funktionsaufruf nicht mag, da erwartet wird, dass tupleim Aufruf selbst ein a vorhanden ist. Das Hinzufügen nach dem Auspacken führt auch zu einem Tippfehler, den ich nicht verstehe.

Wie kommentiert man die sinnvollen Typen für *argsund **kwargs?

Praxeolitisch
quelle

Antworten:

166

Für variable Positionsargumente ( *args) und variable Schlüsselwortargumente ( **kw) müssen Sie nur den erwarteten Wert für ein solches Argument angeben .

Aus dem Abschnitt Beliebige Argumentlisten und Standardargumentwerte des Typhinweises PEP:

Beliebige Argumentlisten können ebenfalls mit Typanmerkungen versehen werden, so dass die Definition:

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

ist akzeptabel und bedeutet, dass z. B. alle folgenden Funktionen Funktionsaufrufe mit gültigen Argumenttypen darstellen:

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

Sie möchten Ihre Methode also folgendermaßen angeben:

def foo(*args: int):

Wenn Ihre Funktion jedoch nur einen oder zwei ganzzahlige Werte akzeptieren kann, sollten Sie keinesfalls *argsein explizites Positionsargument und ein zweites Schlüsselwortargument verwenden:

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

Jetzt ist Ihre Funktion tatsächlich auf ein oder zwei Argumente beschränkt, und beide müssen Ganzzahlen sein, falls angegeben. bedeutet *args immer 0 oder mehr und kann nicht durch Typhinweise auf einen spezifischeren Bereich beschränkt werden.

Martijn Pieters
quelle
1
Nur neugierig, warum das hinzufügen Optional? Hat sich etwas an Python geändert oder haben Sie Ihre Meinung geändert? Ist es aufgrund der NoneStandardeinstellung immer noch nicht unbedingt erforderlich ?
Praxeolitic
10
@Praxeolitic Ja, in der Praxis hat die automatische, implizite OptionalAnnotation, wenn Sie sie Noneals Standardwert verwenden, bestimmte Anwendungsfälle erschwert, und diese werden jetzt aus dem PEP entfernt.
Martijn Pieters
5
Hier ist ein Link, der dies für Interessierte diskutiert . Es hört sich sicherlich so an, als ob Optionalin Zukunft explizite Anforderungen gestellt werden.
Rick unterstützt Monica
Dies wird für Callable nicht unterstützt: github.com/python/mypy/issues/5876
Shital Shah
1
@ShitalShah: Darum geht es in diesem Thema nicht wirklich. Callableunterstützt keine Erwähnung eines Typhinweises für *argsoder **kwargs Punkt . Bei diesem speziellen Problem geht es darum, Callables zu markieren, die bestimmte Argumente und eine beliebige Anzahl anderer akzeptieren , und daher *args: Any, **kwargs: Anyeinen sehr spezifischen Typhinweis für die beiden Catch-Alls zu verwenden. Für Fälle, in denen Sie *argsund / oder **kwargsauf etwas Spezifischeres setzen, können Sie a verwenden Protocol.
Martijn Pieters
25

Der richtige Weg, dies zu tun, ist die Verwendung von @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))

Beachten Sie, dass Sie @overloadder tatsächlichen Implementierung keine Anmerkungen hinzufügen oder eingeben, die zuletzt erfolgen müssen.

Sie benötigen eine neue Version von beiden typingund mypy, um Unterstützung für @overload außerhalb von Stub-Dateien zu erhalten .

Sie können dies auch verwenden, um das zurückgegebene Ergebnis so zu variieren, dass explizit angegeben wird, welche Argumenttypen welchem ​​Rückgabetyp entsprechen. z.B:

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
quelle
2
Ich mag diese Antwort, weil sie den allgemeineren Fall anspricht. Rückblickend hätte ich nicht (type1)vs (type1, type1)Funktionsaufrufe als mein Beispiel verwenden sollen. Vielleicht wäre (type1)vs (type2, type1)ein besseres Beispiel gewesen und zeigt, warum mir diese Antwort gefällt. Dies ermöglicht auch unterschiedliche Rückgabetypen. In dem speziellen Fall, in dem Sie nur einen Rückgabetyp haben *argsund *kwargsalle gleich sind, ist die Technik in Martjins Antwort sinnvoller, sodass beide Antworten nützlich sind.
Praxeolitic
4
Die Verwendung *argseiner maximalen Anzahl von Argumenten (hier 2) ist jedoch immer noch falsch .
Martijn Pieters
1
@MartijnPieters Warum ist hier *argsunbedingt falsch? Wenn die erwarteten Aufrufe (type1)vs waren (type2, type1), ist die Anzahl der Argumente variabel und es gibt keinen geeigneten Standard für das nachfolgende Argument. Warum ist es wichtig, dass es ein Maximum gibt?
Praxeolitic
1
*argsist wirklich da für null oder mehr , unbegrenzte, homogene Argumente oder dafür, diese unberührt weiterzugeben. Sie haben ein erforderliches und ein optionales Argument. Das ist völlig anders und wird normalerweise dadurch behandelt, dass dem zweiten Argument ein Sentinel-Standardwert zugewiesen wird, um festzustellen, dass dieser weggelassen wurde.
Martijn Pieters
3
Nach dem Betrachten des PEP ist dies eindeutig nicht die beabsichtigte Verwendung von @overload. Während diese Antwort eine interessante Möglichkeit darstellt, die Arten von individuell zu kommentieren *args, ist eine noch bessere Antwort auf die Frage, dass dies überhaupt nicht getan werden sollte.
Praxeolitic
20

Als kurz neben der vorherigen Antwort, wenn Sie versuchen , mypy auf Python 2 - Dateien und verwenden müssen , um Kommentare zu verwenden Typen anstelle von Anmerkungen hinzufügen möchten , müssen Sie die Typen für Präfix argsund kwargsmit *und **jeweils:

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

Dies wird von mypy als dasselbe behandelt wie die folgende Python 3.5-Version von foo:

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