我有一个工厂函数,它接受多个
Optional
参数,并根据哪些参数是 None
和哪些不是来创建对象。我还有一个函数检查其所有参数是否都是 None
,还有一个函数检查其所有参数是否不是 None
。我用这些来做检查。这是 MWE:
from typing import Optional
class Arg1:
def __init__(self, arg1: float):
pass
class Arg1And2:
def __init__(self, arg1: float, arg2: float):
pass
class Arg2And3:
def __init__(self, arg2: float, arg3: float):
pass
def _all_none(*args) -> bool:
return all(a is None for a in args)
def _none_none(*args) -> bool:
return all(a is not None for a in args)
def dispatch_factory(
arg1: Optional[float] = None,
arg2: Optional[float] = None,
arg3: Optional[float] = None
):
if _all_none(arg2, arg3) and _none_none(arg1):
return Arg1(arg1) # <-- This throws a mypy error
elif _all_none(arg3) and _none_none(arg1, arg2):
return Arg1And2(arg1, arg2) # <-- And this
elif _all_none(arg1) and _none_none(arg2, arg3):
return Arg2And3(arg2, arg3) # <-- And this
else:
raise ValueError("Combination of arguments invalid")
当然,我们知道我的
Arg1
构造是有效的,因为我们刚刚检查了arg1
不是None
,因此必须是float
。我们对 Arg1And2
和 Arg2And3
知道相同的事情。我理解为什么 mypy 这样做 - 它实际上无法知道 arg1
不是 None
,因为 _none_none
中可能发生各种愚蠢的事情 - 但我有什么办法告诉 mypy _none_none
保证其参数的类型,而无需在 cast(float, arg1)
函数中显式添加 dispatch_factory
?
(另外,如果有人有一个令人信服的替代方法来避免这个问题或更干净,我很高兴听到它。)
验证分支中的类型称为类型防护。目前这是不可能的,但在PEP 647 -- 用户定义的类型防护中提出。
from typing import TypeGuard, Optional, TypeVar
T = TypeVar('T')
def all_none(args: list[Optional[T]]) -> TypeGuard[List[None]]:
return all(a is None for a in args)
由于代码必须显式地将每个参数传递给守卫,因此展开检查的工作量大致相同 - 这适用于当前类型检查。
def dispatch_factory(
arg1: Optional[float] = None,
arg2: Optional[float] = None,
arg3: Optional[float] = None
):
if arg1 is not None and arg2 is None and arg3 is None:
return Arg1(arg1)
elif arg1 is not None and arg2 is not None and arg3 is None:
return Arg1And2(arg1, arg2)
elif arg1 is None and arg2 is not None and arg3 is not None:
return Arg2And3(arg2, arg3)
else:
raise ValueError("Combination of arguments invalid")
通常,类型保护可以重新表述为“返回”值(如果它们属于正确的类型)或没有其他类型。这允许在 if
语句/表达式中使用防护(仅在返回任何内容时才继续)以及赋值表达式(以存储验证值)。
def _all_none(*args: Optional[T]) -> List[None]:
if all(a is None for a in args): # MyPy does not understand this to narrow `args`, so we need the next ignore
return list(args) # type: ignore
return []
def _none_none(*args: Optional[T]) -> List[T]:
if all(a is not None for a in args):
return list(args) # type: ignore
return []
def dispatch_factory(
arg1: Optional[float] = None,
arg2: Optional[float] = None,
arg3: Optional[float] = None
):
if _all_none(arg2, arg3) and (args :=_none_none(arg1)):
return Arg1(*args)
elif _all_none(arg3) and (args := _none_none(arg1, arg2)):
return Arg1And2(*args)
elif _all_none(arg1) and (args := _none_none(arg1, arg2)):
return Arg2And3(*args)
else:
raise ValueError("Combination of arguments invalid")