如何为同时支持同步异步接口的Python库生成typing类型注解信息
结论
你不需要手动维护上百套重复的接口定义,目前有两类可行方案可以解决你的问题,分别适配不同的需求场景。
方案一:直接继承类型签名(无需Protocol,适用绝大多数场景)
你现在的类型提示异常问题根源是SyncLib和AsyncLib重写方法时用了无类型的*args, **kwargs,实际上可以用ParamSpec保留原函数的完整签名,不需要单独写Protocol:
from typing import Callable, ParamSpec, TypeVar, Coroutine import functools # 兼容Python 3.10以下版本的话,从typing_extensions导入ParamSpec即可 P = ParamSpec("P") R = TypeVar("R") # 同步方法装饰器,包裹公共逻辑同时保留原签名 def sync_op(func: Callable[P, R]) -> Callable[P, R]: @functools.wraps(func) def wrapper(self, *args: P.args, **kwargs: P.kwargs) -> R: self.__complex_operation() return func(self, *args, **kwargs) return wrapper # 异步方法装饰器,包裹异步逻辑同时保留原签名 def async_op(func: Callable[P, R]) -> Callable[P, Coroutine[None, None, R]]: @functools.wraps(func) async def wrapper(self, *args: P.args, **kwargs: P.kwargs) -> R: await self.__complex_async_operation() return func(self, *args, **kwargs) return wrapper # 子类无需逐个重写方法,直接绑定装饰后的父类方法即可 class SyncLib(CommonFunctions): def __complex_operation(self): time.sleep(1) do_something = sync_op(CommonFunctions.do_something) do_something_else = sync_op(CommonFunctions.do_something_else) class AsyncLib(CommonFunctions): async def __complex_async_operation(self): await asyncio.sleep(1) do_something = async_op(CommonFunctions.do_something) do_something_else = async_op(CommonFunctions.do_something_else)
这个方案可以100%保留CommonFunctions里的参数、返回值、文档字符串,IDE和类型检查工具都可以正常识别。如果有上百个方法,你甚至可以用类装饰器或者__getattr__自动绑定,不需要逐个赋值。
方案二:自动生成异步Protocol(适用必须单独声明Protocol的场景)
如果你的场景必须用到Protocol做接口抽象,可以用静态代码生成工具来自动同步同步/异步Protocol:
- 只维护一份带完整类型和文档的同步Protocol
- 写一个简单的脚本遍历Protocol的所有方法,给每个方法加上
async关键字,返回值包裹为Coroutine,自动生成异步Protocol - 把这个脚本加入你的CI流程,每次修改同步Protocol后自动生成异步版本
这种方案的维护成本几乎为0,生成的代码是原生Python类型声明,所有类型检查工具都可以完美识别,不需要依赖任何第三方类型扩展特性。
补充说明
你之前尝试的装饰器方案不可行的问题,在Python 3.10引入ParamSpec之后已经解决,P.args和P.kwargs可以完整捕获任意参数列表包括位置参数、关键字参数、默认值等所有类型信息,完全可以满足你的需求。
内容的提问来源于stack exchange,提问作者PK Cakeout
相关产品推荐
相关产品推荐

