พิมพ์คำอธิบายประกอบสำหรับ * args และ ** kwargs


158

ฉันกำลังลองใช้คำอธิบายประกอบชนิดของ Python พร้อมคลาสเบสที่เป็นนามธรรมเพื่อเขียนอินเตอร์เฟส มีวิธีการอธิบายประเภทที่เป็นไปได้*argsและ**kwargs?

ตัวอย่างเช่นเราจะแสดงให้เห็นว่าข้อโต้แย้งที่สมเหตุสมผลกับฟังก์ชั่นนั้นเป็นได้intหรือintไม่? type(args)ให้Tupleเดาว่าฉันจะอธิบายชนิดเป็นUnion[Tuple[int, int], Tuple[int]]แต่ไม่ได้ผล

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

ข้อความแสดงข้อผิดพลาดจาก 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]]"

มันสมเหตุสมผลแล้วที่ mypy ไม่ชอบสิ่งนี้สำหรับการเรียกฟังก์ชั่นเพราะมันคาดว่าจะมีtupleการเรียกในตัวมันเอง การเพิ่มหลังจากแกะออกมาแล้วยังทำให้เกิดข้อผิดพลาดในการพิมพ์ที่ฉันไม่เข้าใจ

หนึ่งคนอธิบายชนิดที่เหมาะสมสำหรับ*argsและ**kwargsอย่างไร

คำตอบ:


167

สำหรับตัวแปรตำแหน่งอาร์กิวเมนต์ ( *args) และอาร์กิวเมนต์คำหลักตัวแปร ( **kw) คุณจะต้องระบุค่าที่คาดไว้สำหรับอาร์กิวเมนต์หนึ่งรายการเท่านั้น

จากรายการอาร์กิวเมนต์โดยพลการและค่าอาร์กิวเมนต์เริ่มต้นในส่วนของประเภทคำแนะนำ PEP:

รายการอาร์กิวเมนต์ตามอำเภอใจสามารถพิมพ์คำอธิบายประกอบได้ดังนั้นคำจำกัดความ:

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

เป็นที่ยอมรับและก็หมายความว่าเช่นทั้งหมดต่อไปนี้เป็นตัวแทนการเรียกใช้ฟังก์ชันที่มีประเภทของอาร์กิวเมนต์ที่ถูกต้อง:

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

ดังนั้นคุณต้องการระบุวิธีการดังนี้:

def foo(*args: int):

อย่างไรก็ตามหากฟังก์ชั่นของคุณสามารถยอมรับได้เพียงหนึ่งหรือสองค่าจำนวนเต็มคุณไม่ควรใช้*argsเลยใช้อาร์กิวเมนต์ตำแหน่งที่ชัดเจนหนึ่งข้อและอาร์กิวเมนต์คำหลักที่สอง:

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

ตอนนี้ฟังก์ชั่นของคุณถูก จำกัด ไว้ที่หนึ่งหรือสองอาร์กิวเมนต์และทั้งสองต้องเป็นจำนวนเต็มถ้าระบุ หมายถึง 0 *args เสมอหรือมากกว่าและไม่สามารถถูก จำกัด โดยคำแนะนำประเภทเป็นช่วงที่เฉพาะเจาะจงมากขึ้น


1
แค่อยากรู้เพิ่มทำไมOptional? มีอะไรเปลี่ยนแปลงเกี่ยวกับ Python หรือคุณเปลี่ยนใจหรือเปล่า? มันยังไม่จำเป็นอย่างเคร่งครัดเนื่องจากNoneค่าเริ่มต้นหรือไม่
Praxeolitic

10
@Praxeolitic ใช่ในทางปฏิบัติโดยอัตโนมัติOptionalคำอธิบายประกอบโดยนัยเมื่อคุณใช้Noneเป็นค่าเริ่มต้นทำให้ usecases บางอย่างยากขึ้นและตอนนี้จะถูกลบออกจาก PEP
Martijn Pieters

5
นี่คือลิงค์ที่พูดถึงเรื่องนี้สำหรับผู้ที่สนใจ แน่นอนว่าเสียงที่ชัดเจนOptionalจะเป็นที่ต้องการในอนาคต
Rick สนับสนุนโมนิก้า

นี่ไม่รองรับ Callable: github.com/python/mypy/issues/5876
Shital Shah

1
@ShalShahShah: นั่นไม่ใช่สิ่งที่เป็นปัญหาจริงๆ Callableไม่สนับสนุนใด ๆเอ่ยถึงคำใบ้ประเภทสำหรับ*argsหรือหยุดเต็ม**kwargs ปัญหาเฉพาะนั้นเกี่ยวกับการทำเครื่องหมาย callables ที่ยอมรับข้อโต้แย้งที่เฉพาะเจาะจงรวมถึงจำนวนของผู้อื่นโดยพลการดังนั้นควรใช้*args: Any, **kwargs: Anyคำแนะนำประเภทที่เฉพาะเจาะจงมากสำหรับ catch-alls ทั้งสอง สำหรับกรณีที่คุณตั้งค่า*argsและ / หรือบางสิ่งบางอย่างเฉพาะเจาะจงมากขึ้นคุณสามารถใช้**kwargs Protocol
Martijn Pieters

26

วิธีที่เหมาะสมในการทำเช่นนี้คือการใช้ @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))

โปรดทราบว่าคุณไม่ได้เพิ่ม@overloadหรือพิมพ์คำอธิบายประกอบในการใช้งานจริงซึ่งจะต้องมีอายุ

คุณจะต้องเป็นรุ่นที่ค่อนข้างใหม่ของทั้งสองtypingและ mypy ที่จะได้รับการสนับสนุนสำหรับ @overload ด้านนอกของไฟล์ต้นขั้ว

นอกจากนี้คุณยังสามารถใช้สิ่งนี้เพื่อปรับเปลี่ยนผลลัพธ์ที่ส่งคืนในแบบที่ทำให้ชัดเจนว่าประเภทของอาร์กิวเมนต์ที่สอดคล้องกับประเภทที่กลับมา เช่น:

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

2
ฉันชอบคำตอบนี้เพราะมันพูดถึงกรณีทั่วไปมากขึ้น มองย้อนกลับไปฉันไม่ควรใช้การเรียกฟังก์ชั่น(type1)vs (type1, type1)เป็นตัวอย่างของฉัน บางที(type1)vs อาจ(type2, type1)เป็นตัวอย่างที่ดีกว่าและแสดงว่าทำไมฉันถึงชอบคำตอบนี้ นอกจากนี้ยังอนุญาตประเภทการคืนสินค้าที่แตกต่างกัน อย่างไรก็ตามในกรณีพิเศษที่คุณมีผลตอบแทนประเภทเดียวและของคุณ*argsและ*kwargsเป็นประเภทเดียวกันทั้งหมดเทคนิคในคำตอบของ Martjin นั้นสมเหตุสมผลมากขึ้นดังนั้นคำตอบทั้งสองจึงมีประโยชน์
Praxeolitic

4
การใช้*argsที่มีจำนวนสูงสุดของการขัดแย้ง (2 นี่) คือยังคงไม่ถูกต้องอย่างไร
Martijn Pieters

1
@MartijnPieters ทำไม*argsจำเป็นต้องผิดที่นี่? หากการเรียกที่คาดหวังนั้นมีค่า(type1)vs (type2, type1)จำนวนอาร์กิวเมนต์เป็นตัวแปรและไม่มีค่าเริ่มต้นที่เหมาะสมสำหรับอาร์กิวเมนต์ที่ตามมา ทำไมจึงมีความสำคัญสูงสุด
Praxeolitic

1
*argsมีอยู่จริงสำหรับการเป็นศูนย์หรือมากกว่าไม่มีข้อโต้แย้งที่เป็นเนื้อเดียวกันหรือสำหรับ คุณมีอาร์กิวเมนต์ที่จำเป็นหนึ่งรายการและเป็นทางเลือกหนึ่งรายการ มันแตกต่างกันโดยสิ้นเชิงและโดยปกติแล้วการจัดการโดยให้อาร์กิวเมนต์ที่สองเป็นค่าเริ่มต้นของ Sentinel ในการตรวจจับที่ถูกละไว้
Martijn Pieters

3
หลังจากดู PEP แล้วสิ่งนี้ก็ไม่ได้ตั้งใจใช้ @overload ในขณะที่คำตอบนี้แสดงวิธีที่น่าสนใจในการเพิ่มความคิดเห็นแต่ละประเภท*argsแต่คำตอบที่ดียิ่งขึ้นสำหรับคำถามก็คือว่านี่ไม่ใช่สิ่งที่ควรทำ
Praxeolitic

20

นอกเหนือจากคำตอบก่อนหน้านี้หากคุณพยายามใช้ mypy ในไฟล์ Python 2 และจำเป็นต้องใช้ความคิดเห็นเพื่อเพิ่มประเภทแทนการเพิ่มความคิดเห็นคุณจะต้องใส่คำนำหน้าประเภทargsและkwargsด้วย*และ**ตามลำดับ:

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

นี่คือการปฏิบัติโดย mypy เหมือนกับด้านล่าง Python 3.5 เวอร์ชั่นfoo:

def foo(param: bool, *args: str, **kwargs: int) -> None:
    pass
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.