返回

Python Literal 类型守卫:避免重复定义属性名

python

Python 中如何创建基于 Literal 的类型守卫(TypeGuard)

直接说问题吧, 想实现一个参数名校验的TypeGuard,参数名来自于第三方数据,所以要好好校验一下。

简单起见,假设我在做一个展示狗狗信息的应用,每只狗狗只有 3 个属性:

  1. name (名字)
  2. birth_date (生日)
  3. 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 类型里的值。

  1. 原理: typing.get_args可以获取泛型类型的参数。 比如 get_args(Literal["a", "b"]) 会返回 ("a", "b")
  2. 代码示例:
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)
  1. 解释: 这样写,is_dog_property_name 函数会动态地从 DogPropertyName 获取允许的值,不用手动重复了。

方案二:定义一个列表或元组

把属性名放在一个列表或者元组里。

  1. 原理: 先定义好,校验直接复用列表或者元组,确保Type 和 is check时,信息一致。
  2. 代码示例:
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
  1. 解释: 如果没办法用 get_args, 只能手动指定Literal, 但is_dog_property_name 可以复用定义列表。

方案三: 使用枚举 (Enum)

如果属性名是固定的,而且你还想有点儿面向对象的意思,用 Enum 也挺好。

  1. 原理: Enum 可以把属性名定义成枚举成员,自带唯一性和可读性。

  2. 代码示例:

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)

  1. 解释: 枚举成员的值是字符串, 同时is_dog_property_name判断值是不是一个有效的枚举key。

  2. 进阶:
    如果不仅仅要判断 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

  1. 原理: 先定义一个包含所有可能值的集合, 然后在 TypeGuard 函数里实现自定义的检查逻辑.

  2. 代码:

    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}")
    
    1. 说明 :

      • 虽然没有直接用到 Literal, 但我们通过 _dog_property_names 和自定义的检查逻辑,保证了类型安全。
      • 注意 TypeGuard 的返回值是 TypeGuard[str],而不是 TypeGuard[DogPropertyName]。因为这里我们没有真正的 DogPropertyName 类型。
    2. 进阶使用技巧
      可以搭配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,避免重复代码。根据实际情况,选择最适合的一种就好。记住,代码清晰和类型安全都很重要!