Ketikkan anotasi untuk * args dan ** kwargs

158

Saya mencoba anotasi tipe Python dengan kelas dasar abstrak untuk menulis beberapa antarmuka. Apakah ada cara untuk menjelaskan jenis *argsdan kemungkinan **kwargs?

Sebagai contoh, bagaimana seseorang menyatakan bahwa argumen yang masuk akal untuk suatu fungsi adalah satu intatau dua int? type(args)memberi Tuplejadi dugaan saya adalah untuk membubuhi keterangan jenis Union[Tuple[int, int], Tuple[int]], tetapi ini tidak berhasil.

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

Pesan kesalahan dari 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]]"

Masuk akal bahwa mypy tidak menyukai ini untuk pemanggilan fungsi karena ia mengharapkan akan ada tupledalam pemanggilan itu sendiri. Tambahan setelah membongkar juga memberikan kesalahan pengetikan yang saya tidak mengerti.

Bagaimana cara menjelaskan jenis yang masuk akal untuk *argsdan **kwargs?

Praxeolitic
sumber

Jawaban:

167

Untuk argumen posisi variabel ( *args) dan argumen kata kunci variabel ( **kw) Anda hanya perlu menentukan nilai yang diharapkan untuk satu argumen tersebut.

Dari daftar argumen Sewenang - wenang dan bagian nilai argumen default dari Type Hints PEP:

Daftar argumen sewenang-wenang juga dapat diketikkan, sehingga definisi:

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

dapat diterima dan itu berarti, mis., semua yang berikut ini mewakili panggilan fungsi dengan tipe argumen yang valid:

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

Jadi, Anda ingin menentukan metode Anda seperti ini:

def foo(*args: int):

Namun, jika fungsi Anda hanya dapat menerima satu atau dua nilai integer, Anda tidak boleh menggunakan *argssama sekali, gunakan satu argumen posisi eksplisit dan argumen kata kunci kedua:

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

Sekarang fungsi Anda sebenarnya terbatas pada satu atau dua argumen, dan keduanya harus bilangan bulat jika ditentukan. *args selalu berarti 0 atau lebih, dan tidak dapat dibatasi oleh tip petunjuk ke rentang yang lebih spesifik.

Martijn Pieters
sumber
1
Hanya penasaran, mengapa menambahkan Optional? Apakah ada yang berubah tentang Python atau Anda berubah pikiran? Apakah masih belum sepenuhnya diperlukan karena Nonedefault?
Praxeolitic
10
@Praxeolitic ya, dalam praktiknya otomatis, tersirat Optionalpenjelasan ketika Anda menggunakan Nonesebagai nilai default membuat usecases tertentu lebih sulit dan yang sekarang sedang dihapus dari PEP.
Martijn Pieters
5
Berikut ini tautan yang membahas hal ini untuk mereka yang tertarik. Itu memang terdengar seperti eksplisit Optionalakan diperlukan di masa depan.
Rick mendukung Monica
Ini sebenarnya tidak didukung untuk Callable: github.com/python/mypy/issues/5876
Shital Shah
1
@ ShitalShah: bukan itu masalahnya. Callabletidak mendukung salah menyebutkan jenis petunjuk untuk *argsatau **kwargs berhenti penuh . Masalah spesifik tersebut adalah tentang menandai callable yang menerima argumen spesifik ditambah jumlah arbitrer lainnya , dan karenanya menggunakan *args: Any, **kwargs: Any, tip tipe yang sangat spesifik untuk kedua catch-alls. Untuk kasus di mana Anda mengatur *argsdan / atau **kwargsuntuk sesuatu yang lebih spesifik, Anda dapat menggunakan a Protocol.
Martijn Pieters
26

Cara yang tepat untuk melakukan ini adalah menggunakan @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))

Perhatikan bahwa Anda tidak menambahkan @overloadatau mengetik anotasi pada implementasi aktual, yang harus menjadi yang terakhir.

Anda membutuhkan versi yang agak baru dari keduanya typingdan mypy untuk mendapatkan dukungan untuk @overload di luar file rintisan .

Anda juga dapat menggunakan ini untuk memvariasikan hasil yang dikembalikan dengan cara yang membuat eksplisit jenis argumen yang sesuai dengan jenis kembali. misalnya:

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
sumber
2
Saya suka jawaban ini karena membahas kasus yang lebih umum. Melihat ke belakang, saya seharusnya tidak menggunakan panggilan fungsi (type1)vs (type1, type1)sebagai contoh saya. Mungkin (type1)vs (type2, type1)akan menjadi contoh yang lebih baik dan menunjukkan mengapa saya suka jawaban ini. Ini juga memungkinkan jenis pengembalian yang berbeda. Namun, dalam kasus khusus di mana Anda hanya memiliki satu tipe kembali dan Anda *argsdan *kwargssemuanya jenis yang sama, teknik dalam jawaban Martin lebih masuk akal sehingga kedua jawaban itu berguna.
Praxeolitic
4
Menggunakan di *argsmana ada jumlah maksimum argumen (2 di sini) masih salah .
Martijn Pieters
1
@ MartijnPieters Mengapa *argssalah di sini? Jika panggilan yang diharapkan adalah (type1)vs (type2, type1), maka jumlah argumen adalah variabel dan tidak ada standar yang sesuai untuk argumen tambahan. Mengapa penting bahwa ada maks?
Praxeolitic
1
*argsbenar-benar ada untuk nol atau lebih , argumen terbuka, tidak homogen, atau untuk 'melewati ini bersama' tersentuh 'alls-alls. Anda memiliki satu argumen yang diperlukan dan satu opsional. Itu sama sekali berbeda dan biasanya ditangani dengan memberikan argumen kedua nilai default sentinel untuk mendeteksi yang dihilangkan.
Martijn Pieters
3
Setelah melihat PEP, ini jelas bukan tujuan penggunaan @overload. Sementara jawaban ini menunjukkan cara yang menarik untuk secara individual menjelaskan jenis-jenisnya *args, jawaban yang lebih baik untuk pertanyaan ini adalah bahwa ini bukanlah sesuatu yang harus dilakukan sama sekali.
Praxeolitic
20

Sebagai tambahan singkat untuk jawaban sebelumnya, jika Anda mencoba menggunakan mypy pada file Python 2 dan perlu menggunakan komentar untuk menambahkan jenis, bukan anotasi, Anda perlu awalan jenis untuk argsdan kwargsdengan *dan **masing - masing:

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

Ini diperlakukan oleh mypy sebagai sama dengan di bawah ini, versi Python 3.5 dari foo:

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