Python Literal 类型守卫:避免重复定义属性名
2025-03-02 05:23:29
Python 中如何创建基于 Literal 的类型守卫(TypeGuard)
直接说问题吧, 想实现一个参数名校验的TypeGuard
,参数名来自于第三方数据,所以要好好校验一下。
简单起见,假设我在做一个展示狗狗信息的应用,每只狗狗只有 3 个属性:
name
(名字)birth_date
(生日)favorite_food
(最喜欢的食物)
目前,我的TypeGuard
是这样写的:
from typing import Literal, TypeGuard
type DogPropertyName = Literal["name", "birth_date", "favorite_food"]
def is_dog_property_name(value: str) -> TypeGuard[DogPropertyName]:
return value in ["name", "birth_date", "favorite_food"]
问题在于,我把这些属性名写了两遍。 我试过解包元组,但不行:
dog_property_names = ("name", "birth_date", "favorite_food")
type DogPropertyName = Literal[*dog_property_names]
# Error: Unpacked arguments cannot be used in this context
def is_dog_property_name(value: str) -> TypeGuard[DogPropertyName]:
return value in dog_property_names
我能不能用 Literal TypeGuard
实现这种需求?
问题分析
核心问题是重复定义了 DogPropertyName
。我们想避免重复,最好能从一个“源头”生成TypeGuard
和 Literal 类型。直接用Literal[*tuple]
是不行的,因为Literal
不支持解包。
解决方案
有好几种办法可以解决这个问题,下面分别介绍。
方案一:使用 typing.get_args
(Python 3.8+)
如果用 Python 3.8 或更高版本,typing.get_args
可以帮我们拿到 Literal
类型里的值。
- 原理:
typing.get_args
可以获取泛型类型的参数。 比如get_args(Literal["a", "b"])
会返回("a", "b")
。 - 代码示例:
from typing import Literal, TypeGuard, get_args
type DogPropertyName = Literal["name", "birth_date", "favorite_food"]
def is_dog_property_name(value: str) -> TypeGuard[DogPropertyName]:
return value in get_args(DogPropertyName)
- 解释: 这样写,
is_dog_property_name
函数会动态地从DogPropertyName
获取允许的值,不用手动重复了。
方案二:定义一个列表或元组
把属性名放在一个列表或者元组里。
- 原理: 先定义好,校验直接复用列表或者元组,确保Type 和 is check时,信息一致。
- 代码示例:
from typing import Literal, TypeGuard, Union
dog_property_names = ("name", "birth_date", "favorite_food") #也可以用列表
DogPropertyName = Union[Literal["name"], Literal["birth_date"], Literal["favorite_food"]]
def is_dog_property_name(value: str) -> TypeGuard[DogPropertyName]:
return value in dog_property_names
- 解释: 如果没办法用
get_args
, 只能手动指定Literal
, 但is_dog_property_name
可以复用定义列表。
方案三: 使用枚举 (Enum)
如果属性名是固定的,而且你还想有点儿面向对象的意思,用 Enum
也挺好。
-
原理:
Enum
可以把属性名定义成枚举成员,自带唯一性和可读性。 -
代码示例:
from enum import Enum
from typing import TypeGuard
class DogPropertyName(str, Enum):
NAME = "name"
BIRTH_DATE = "birth_date"
FAVORITE_FOOD = "favorite_food"
def is_dog_property_name(value: str) -> TypeGuard[DogPropertyName]:
return value in DogPropertyName.__members__
# 或者 return any(value == item.value for item in DogPropertyName)
-
解释: 枚举成员的值是字符串, 同时
is_dog_property_name
判断值是不是一个有效的枚举key。 -
进阶:
如果不仅仅要判断 key, 还要保证传入DogPropertyName
的值时,也可以添加如下判断。from enum import Enum from typing import TypeGuard class DogPropertyName(str, Enum): NAME = "name" BIRTH_DATE = "birth_date" FAVORITE_FOOD = "favorite_food" def is_dog_property_name(value: str) -> TypeGuard[DogPropertyName]: return value in DogPropertyName.__members__ or any(value == item.value for item in DogPropertyName)
方案四: 反向定义 + 自定义 TypeGuard
(适用于更复杂场景)
有时候,可能类型检查逻辑比较复杂,不能简单地用 in
来判断。我们可以自定义更强大的 TypeGuard
。
-
原理: 先定义一个包含所有可能值的集合, 然后在
TypeGuard
函数里实现自定义的检查逻辑. -
代码:
from typing import TypeGuard # 所有可能的属性名 _dog_property_names = {"name", "birth_date", "favorite_food"} # 自定义 TypeGuard def is_dog_property_name(prop: str) -> TypeGuard[str]: # 注意这里返回 TypeGuard[str] # 更复杂的校验逻辑... if not isinstance(prop, str): return False return prop in _dog_property_names # 举个例子:假设我们要用这个 TypeGuard def process_dog_data(data: dict, prop_name: str): if is_dog_property_name(prop_name): # 现在 prop_name 在这里被认为是有效的 value = data.get(prop_name) #可以安全地访问 print(f"Processing {prop_name}: {value}") else: print(f"Invalid property name: {prop_name}")
-
说明 :
- 虽然没有直接用到
Literal
, 但我们通过_dog_property_names
和自定义的检查逻辑,保证了类型安全。 - 注意
TypeGuard
的返回值是TypeGuard[str]
,而不是TypeGuard[DogPropertyName]
。因为这里我们没有真正的DogPropertyName
类型。
- 虽然没有直接用到
-
进阶使用技巧
可以搭配typing.cast
做更精准的控制。
from typing import TypeGuard, cast, Literal # 所有可能的属性名 _dog_property_names = {"name", "birth_date", "favorite_food"} DogPropertyName = Literal["name", "birth_date", "favorite_food"] # 自定义 TypeGuard def is_dog_property_name(prop: str) -> TypeGuard[str]: # 注意这里返回 TypeGuard[str] # 更复杂的校验逻辑... if not isinstance(prop, str): return False return prop in _dog_property_names # 举个例子:假设我们要用这个 TypeGuard def process_dog_data(data: dict, prop_name: str): if is_dog_property_name(prop_name): # 现在 prop_name 在这里被认为是有效的 value = data.get(cast(DogPropertyName,prop_name)) #可以安全地访问 print(f"Processing {prop_name}: {value}") else: print(f"Invalid property name: {prop_name}")
-
安全建议
- 来源验证 : 既然数据来自第三方,除了校验属性名,属性值的类型和内容也要好好查一下 (比如日期格式,字符串长度等等)。
- 异常处理 : 遇到非法属性名或值时,要妥善处理异常,可以记录日志或者返回错误信息, 不要让程序崩溃.
总结
以上几种方法都能帮你创建 Literal TypeGuard,避免重复代码。根据实际情况,选择最适合的一种就好。记住,代码清晰和类型安全都很重要!