前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[Python]实现函数的输入输出参数的强类型检验

[Python]实现函数的输入输出参数的强类型检验

作者头像
明月AI
发布2023-12-15 15:10:35
1660
发布2023-12-15 15:10:35
举报
文章被收录于专栏:野生AI架构师野生AI架构师

Python是一门弱类型的解释型语言,弱类型有其优势,非常适用于算法开发以及一些短平快的项目,但也有其劣势,当代码越来越多的时候,自由的代价就会呈现出来,维护的代价也会越来越大。特别是,如果我们开发接口给别人使用的时候,如果没有强类型的校验,别人就不能清晰的知道输入输出的数据结构是什么,报错的时候也比较难定位问题,因此在有些场景下,需要对函数输入输出进行强类型约束。

使用包装器尽量减少代码的侵入式

比较笨的实现方式是在每个需要进行参数校验的地方,手动加入类似如下代码:

代码语言:javascript
复制
assert type(data) == list, "data参数必须是list类型"

这确实可以实现,不失为凑代码行数的好方法,但是这很丑陋,后期也很难维护。

使用FastAPI的体验都比较清楚,在FastAPI中,接口的输入输出参数是可以定义成强类型的,这也是自己最初看到FastAPI就觉得这就是Python当前最好的框架之一。总结一下,我们的实现方式应该做到如下两个要求:

  1. 非侵入式的,尽量避免对业务代码的更改;
  2. 实现输入输出参数的强类型校验。

参考FastAPI的实现,我们的实现应该也是采用包装器的形式来实现。本来想直接FastAPI的源码里找到对应的代码,复制出来使用的,但是找到了一段时间,也没有定位到代码对应的地方,就自己直接实现了,其实并不复杂。

在包装器中实现对目标函数的输入输出校验,下面是一个示例的业务代码:

代码语言:javascript
复制
class ClassTool:
    def run(self, input_text: str = '', text_len: int = 100) -> str:
        assert input_text, f"input_text参数不能为空"
        input_class = '正常'
        if len(input_text) > text_len:
            input_class = '文本过长'
        return input_class

原来的实现就是使用“assert”来对输入参数进行校验,但是对于复杂类型校验就比较麻烦了,例如类型List[str], List[Dict[str, int]]等,硬是要使用“assert”也是可以的,只是代码很罗嗦,很多重复代码。

校验包装器实现

使用包装器实现输入输出参数的校验,具体代码如下:

代码语言:javascript
复制
from functools import wraps
from inspect import get_annotations
from pydantic import BaseModel

def ToolParamsCheck(fun):
    """工具参数校验
    注意:当接口有未知参数的时候,不能使用该参数检查
    """
    @wraps(fun)
    def wrap_fun(cls, **kwargs):
        tool_key = cls.__class__.__name__
        # 处理输入参数
        params = get_annotations(fun)
        keys: list = list(params.keys())
        if 'return' in keys:    # 去掉返回值
            keys.remove('return')
        if len(keys) != 1:
            raise Exception(f"工具{tool_key}的输入参数数量不为1: {len(keys)}")
        InputParams = params[keys[0]]   # 输入参数类
        support_params = get_annotations(InputParams)
        for key in kwargs.keys():
            if key not in support_params:
                raise Exception(f"工具{tool_key}不支持参数: {key}")

        # 执行
        try:
            input_params = InputParams(**kwargs)
        except Exception as e:
            raise Exception(f"工具{tool_key}的输入参数异常: {e}    支持的参数列表: {support_params}")

        # 执行工具
        res = fun(cls, input_params)

        # 处理输出参数
        if 'return' in params:
            class ReturnParam(BaseModel):
                param: params['return']
            try:
                ReturnParam(param=res)
            except Exception as e:
                raise Exception(f"工具{tool_key}的输出参数异常: {e}  期望的输出类型: {params['return']}, 实际的输出类型: {type(res)}")

        return res

    return wrap_fun

对应的业务代码也需要做一些简单的修改,如下:

代码语言:javascript
复制
from pydantic import BaseModel, Field
from .utils import ToolParamsCheck

class InputParams(BaseModel):
    """定义输入参数"""
    input_text: str = Field(..., title='输入文本')
    text_len: int = Field(100, title='超过该长度,则返回"文本过长",否则返回"正常"')

class ClassTool:
    @ToolParamsCheck
    def _run(self, params: InputParams) -> str:
        input_class = '正常'
        if len(params.input_text) > params.text_len:
            input_class = '文本过长'
        return input_class

这对代码有一点侵入性,是的,但是这正是我所期待的,相比原来的方式输入,个人更喜欢将参数定义成这样,类似FastAPI,后面可以作为对象使用,避免低级错误,例如写错变量名等。

从实现上,要点如下:

  1. 输入参数:使用参数类(如上面的InputParams)将输入的“**kwargs”参数在包装器中进行转换,如果数据中有类型不匹配,则会抛出异常。注意如果多传了参数,这是不会报错的,需要在包装器中使用代码进行判断;
  2. 使用“get_annotations”获取目标函数的输入输出参数的类型信息;
  3. 输出参数:这个的校验比较特别,试了好几种方法,最后觉得这样式最好的,在需要返回值校验的时候,定义了一个动态的类“ReturnParam”(见上面的代码)。

输出参数校验的时候,没有参考FastAPI使用一个“response_model”之类的包装器参数,而是使用更加直接的方式。

说明:因为我们的场景下,输入输出都需要是普通的数据,并没有将输入输出转成强类型数据,外部在调用时(通过HTTP接口)还是普通的输入输出。

使用限制

原业务函数中如果包含了类似*args/**kwargs这类的可变参数,则上面的包装器还是完善,例如对于*args参数,可以类似输出参数的方式进行处理。

不过对于这两类的参数,这个“get_annotations”方法获取不到对应的信息,需要找其他的方式。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-12-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 野生AI架构师 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档