使用all()校验后对含可选成员的对象列表排序时Mypy报错的解决问询
我需要对包含可选成员的对象列表进行排序,规则是若列表中存在对象的可选成员为None则不执行排序,我通过all()语句实现该校验逻辑,但Mypy对此报错。
最小复现代码
from typing import Optional from pydantic import BaseModel class Test(BaseModel): member: Optional[int] tests = [ Test(member=1), Test(member=5), Test(member=2), ] if all(test.member is not None for test in tests): tests = sorted( tests, key=lambda x: x.member, reverse=False, ) else: raise ValueError print(tests)
Mypy报错信息
test.py:17: error: Argument "key" to "sorted" has incompatible type "Callable[[Test], Optional[int]]"; expected "Callable[[Test], Union[SupportsDunderLT, SupportsDunderGT]]" test.py:17: error: Incompatible return value type (got "Optional[int]", expected "Union[SupportsDunderLT, SupportsDunderGT]") Found 2 errors in 1 file (checked 1 source file)
调整lambda函数为lambda x: x.member if x.member is not None else 0后Mypy不再报错,但这个写法冗余,因为all()校验已确保所有成员非None。请问是否有更优解?Mypy为何无法识别all()的校验逻辑?
解答
为什么Mypy无法识别all()的校验逻辑?
Mypy的类型推导能力存在局限性,它无法追踪all(test.member is not None for test in tests)这个生成器表达式对列表中每个元素属性的类型窄化效果。all()仅返回布尔值,Mypy无法将该条件的类型信息关联到后续代码块中每个test.member的类型上,因此仍判定x.member为Optional[int]类型。而sorted的key函数要求返回可比较的非可选类型,最终触发类型错误。
更优解决方案
方案1:使用typing.cast强制类型转换
直接告知Mypy此时x.member的类型是int,无运行时开销,也不会引入冗余逻辑:
from typing import Optional, cast from pydantic import BaseModel class Test(BaseModel): member: Optional[int] tests = [ Test(member=1), Test(member=5), Test(member=2), ] if all(test.member is not None for test in tests): tests = sorted( tests, key=lambda x: cast(int, x.member), reverse=False, ) else: raise ValueError print(tests)
方案2:通过列表推导式做类型窄化
先过滤出member非None的元素,让Mypy识别到新列表的元素属性类型确定,再通过长度对比确保原列表无None元素:
from typing import Optional from pydantic import BaseModel class Test(BaseModel): member: Optional[int] tests = [ Test(member=1), Test(member=5), Test(member=2), ] valid_tests = [test for test in tests if test.member is not None] if len(valid_tests) != len(tests): raise ValueError tests = sorted(valid_tests, key=lambda x: x.member) print(tests)
这种方式更符合类型安全的最佳实践,Mypy可清晰推导valid_tests中每个test.member为int类型。
方案3:从根源避免可选类型(业务允许的话)
如果列表生成逻辑可控,可在创建Test实例时强制member不为None,或修改Pydantic模型定义,将member设为必填的int类型,彻底消除可选类型带来的问题:
from pydantic import BaseModel class Test(BaseModel): member: int # 改为必填的int类型 tests = [ Test(member=1), Test(member=5), Test(member=2), ] # 无需校验,直接排序 tests = sorted(tests, key=lambda x: x.member) print(tests)
内容的提问来源于stack exchange,提问作者stdklr

