如何将一个字典转换为 TypedDict 的好方法?

11
注意:由于这个答案一直被点赞,虽然仍然有使用TypedDict的情况,但我认为今天应该使用dataclass代替。
我希望有一种良好的(`mypy --strict`和符合Python风格)方式,将一个未类型化的`dict`(来自`json.loads()`)转换为`TypedDict`。我的目前方法如下:
class BackupData(TypedDict, total=False):
    archive_name: str
    archive_size: int
    transfer_size: int
    transfer_time: float
    error: str


def to_backup_data(data: Mapping[str, Any]) -> BackupData:
    result = BackupData()
    if 'archive_name' in data:
        result['archive_name'] = str(data['archive_name'])
    if 'archive_size' in data:
        result['archive_size'] = int(data['archive_size'])
    if 'transfer_size' in data:
        result['transfer_size'] = int(data['transfer_size'])
    if 'transfer_time' in data:
        result['transfer_time'] = int(data['transfer_time'])
    if 'error' in data:
        result['error'] = str(data['error'])
    return result

我有一个带有可选键的TypedDict,并且想要一个TypedDict实例。
上面的代码是多余和非功能性的(从函数式编程的角度来看),因为我必须四次写入名称,两次写入类型,并且result必须是可变的。 遗憾的是,TypedDict不能有方法,否则我可以写出类似的东西。
backup_data = BackupData.from(json.loads({...}))

关于,我是否有什么遗漏的地方?这个能不能以一种简洁、非冗余的方式写出来?
1个回答

6
当您使用TypedDict时,所有信息都存储在__annotations__字段中。
以您的示例为例:
BackupData.__annotations__

返回值:

{'archive_name': <class 'str'>, 'archive_size': <class 'int'>, 'transfer_size': <class 'int'>, 'transfer_time': <class 'float'>, 'error': <class 'str'>}

现在,我们可以使用该字典来迭代数据并使用值进行类型转换:
def to_backup_data(data: Mapping[str, Any]) -> BackupData:
    result = BackupData()
    for key, key_type in BackupData.__annotations__.items():
        if key not in data:
            raise ValueError(f"Key: {key} is not available in data.")
        result[key] = key_type(data[key])
    return result

请注意,当数据不可用时,我会抛出一个错误,这可以根据您的意愿进行更改。

使用以下测试代码:

data = dict(
        archive_name="my archive",
        archive_size="50",
        transfer_size="100",
        transfer_time="2.3",
        error=None,
)

for key, value in result.items():
    print(f"Key: {key.ljust(15)}, type: {str(type(value)).ljust(15)}, value: {value!r}")

结果将是:
Key: archive_name   , type: <class 'str'>  , value: 'my archive'
Key: archive_size   , type: <class 'int'>  , value: 50
Key: transfer_size  , type: <class 'int'>  , value: 100
Key: transfer_time  , type: <class 'float'>, value: 2.3
Key: error          , type: <class 'str'>  , value: 'None'

虽然我喜欢这种方法,但它可能会导致一些意外行为。例如,在源字典中,如果archive_nameNone,那么生成的数据将显示为"None",因为str(None)->"None" - Joe Sadoski
1
嗨,乔,那是真的,这就是为什么我提供了一个带有error = None的示例,它确实会产生字符串“'None'”。不幸的是,在输入答案时,我没有给予它更多的关注。如果您想接受None作为可能的答案,则输入应为Optional[str],对于该解决方案将无法使用。 - Thymen
我认为你的回答是一个很好的起点,对于任何应用程序,可能需要添加更多的行为。我只是想警告未来的谷歌搜索者! - Joe Sadoski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接