Discuss / Python / def has_request_arg(fn):这个函数抛出错误的那段代码看的我好纠结啊

def has_request_arg(fn):这个函数抛出错误的那段代码看的我好纠结啊

Topic source
def has_request_arg(fn):
    sig = inspect.signature(fn)
    params = sig.parameters
    found = False
    for name, param in params.items():
        if name == 'request':
            found = True
            continue
        if found and (param.kind != inspect.Parameter.VAR_POSITIONAL and param.kind != inspect.Parameter.KEYWORD_ONLY and param.kind != inspect.Parameter.VAR_KEYWORD):
            raise ValueError('request parameter must be the last named parameter in function: %s%s' % (fn.__name__, str(sig)))
    return found

查了下python官方文档中关于inspect.Parameter的部分,里面有5种参数类型,和廖老师教程中有点不一样。 分别是POSITIONAL_ONLY、VAR_POSITIONAL、KEYWORD_ONLY、VAR_KEYWORD、POSITIONAL_OR_KEYWORD 分别对应廖老师教程中的位置参数、可变参数、命名关键字参数、关键字参数,最后一个是位置参数或命名关键字参数 这样算来其实也就只有四种参数类型,廖老师教程中的默认参数其实是从位置参数中衍生出来的,命名关键字参数也可以是默认参数

param.kind != inspect.Parameter.VAR_POSITIONAL and param.kind != inspect.Parameter.KEYWORD_ONLY and param.kind != inspect.Parameter.VAR_KEYWORD

()内的这段代码的意思应该是当参数类型既不是可变参数,又不是命名关键字参数,又不是关键字参数的时候,()内为True 也就是说只要是位置参数就判断为True,然后抛出错误,那为什么不直接写

param.kind == inspect.Parameter.POSITIONAL_ONLY

抛出的错误提示request parameter must be the last named parameter in function看的我更加纠结了 如果named parameter解释为有名字的参数,那所有参数都有名字,最后一个有名字的参数就是最后一个参数,也就是说request参数后面不能再有别的参数,只要有就报错,这样的话条件判断语句写成这样就好了

if found and param.kind

如果named parameter解释为命名关键字参数,那就是说request参数必须是最后命名关键字参数,而且必须是最后一个。如果是这样的话,找到request参数后应该先判断下参数类型,然后再决定是否设置found为True 如果找到request参数,那后面的参数只可能是关键字参数,如果下一个参数类型还是命名关键字参数,那就需要报错。这样的话条件判断语句就应该是

if found and param.kind==KEYWORD_ONLY

灰_手

#2 Created at ... [Delete] [Delete and Lock User]
def`foo(a, *b, c, **d):
    pass        

a == POSITIONAL_OR_KEYWORD # a是用位置或参数名都可赋值的
b == VAR_POSITIONAL        # b是可变长列表  
c == KEYWORD_ONLY          # c只能通过参数名的方式赋值
d == VAR_KEYWORD           # d是可变长字典

 POSITIONAL_ONLY 这类型在官方说明是不会出现在普通函数的,一般是内置函数什么的才会有,可能是self,cls或者是更底层的东西

灰_手

#3 Created at ... [Delete] [Delete and Lock User]

你可以看看我自己重构的版本,应该会容易理解吧,我也想不清楚在老师的版本中为什么request一定是要在最后,不都是用字典传值吗,位置无关紧要的吧

Rand01ph

#4 Created at ... [Delete] [Delete and Lock User]

廖老师意思应该是: request应该作为POSITIONAL_OR_KEYWORD类型的参数,就是最普通的Python函数的参数类型。

如果request作为VAR_POSITIONAL类型,那么赋值给他request对象是会报错的。

如果request作为KEYWORD_ONLY类型,那么赋值给request是可以,不会报错,但是这个参数赋值没有什么意义,因为会被request对象覆盖,如果http请求里面不予赋值也会报错。

如果request作为VAR_KEYWORD类型,那么传过去的参数request就在参数字典中,需要再次解析,也没有意义。

为什么request要作为最后一个参数,我的理解是业务约定,不是代码层面的问题,因为最后调用的函数参数肯定是从request对象中取出的。 如果调用的函数需要传入request,那么它可以完全不需要传入其他参数。 之所以传入其他参数一个作用是通过url装饰器更加直观的对应,另外一个就是KEYWORD_ONLY参数作为必填参数,这都是业务上的约定了。

灰_手

#5 Created at ... [Delete] [Delete and Lock User]

遇到逻辑泥团必须考虑重构!先让我们理一下一个路由函数的所有参数都是怎么来的,只有清晰理解这一点才有可能聊得下去,先来看这样一个路由函数:

@get('/api/{table}')
async def api_model(table, page=1, request):
    pass

先不必管参数是属于什么类型的,先来聊聊这些参数是可能是从哪里来的。读过源码的就会知道路由函数的参数主要有三个来源。但我们还是从第一步说起吧,首先先用inspect.signature(self._func).parameters的函数获取路由api_model的参数表,不难看出是有[table, page, request]三个参数,其中page是有默认值的。接下来要获取参数了,有三个来源:

  1. 网页中的GETPOST方法(获取/?page=10还有jsonform的数据。)
  2. request.match_info(获取@get('/api/{table}')装饰器里面的参数)
  3. def __call__(self, request)(获取request参数)

顺序能变吗?不能!你若读过我的代码,就会发现我重构之后的代码和廖老师在获取参数的顺序都是一致的,只不过我简化了逻辑泥团,将事先检验改成事后检验而已。

最容易改变,或者说最容易被用户操纵的就是GET方法的参数,万一你已经获取了tablerequest的参数,用户只要网址后面加入/?table=users&request=XXX就可以轻易替换你原本的参数,所以,网页的参数必须最先加入参数字典的,在这个部分必须小心处理,只有在路由函数所要求的参数才能加到参数字典(比如api_model的,只有[table, page, request]这三个参数才接受,其他的都会被忽略的)。

request.match_info的参数能不能改变,我也不知道,把table换成page的参数直接就服务器错误,找不到此网页

但是最重要的是request是最后一个传入的!如果此前GETPOST传入了request参数,那最后一定是被覆盖成正确的request参数!所以request绝对没有任何可能是VAR类型的。如果你不小心在路由函数把request写成*request或者**request是一定会报错的,request可以是KEYWORD_ONLY也可以是POSITIONAL_OR_KEYWORD类型的,这两种类型都完全可以接受的。因为最后都是用await self._func(**kw)来调用,**kw是解压字典的意思,也就说是所有参数都是用KEYWORD来传参的,KEYWORD_ONLYPOSITIONAL_OR_KEYWORD都无所谓,唯一考量只是接口规范性,什么参数类型实质上并不是特别重要。

灰_手

#6 Created at ... [Delete] [Delete and Lock User]

所以我才说,RequestHandler用了四五段代码来检验参数的类型实在是太夸张了,真正重要的是从三个数据源获取参数的顺序!这个远比检验参数类型重要得多,廖老师这段代码确实挺让人迷惑的。

Rand01ph

#7 Created at ... [Delete] [Delete and Lock User]

我上面的回复表达的应该很清楚,因为廖老师是在aiohttp上层封装的一个框架,参数是KEYWORD_ONLY和POSITIONAL_OR_KEYWORD都无所谓。 这个没错,通过dict[key]传参是可以避免参数错误的问题。 代码里的逻辑这个就是廖老师框架这层的“约定”了,这个“约定”的好处是所有定义的handle都很清晰,参数位置是确定的,这个不是代码的对错,而是良好的设计。

灰_手

#8 Created at ... [Delete] [Delete and Lock User]

你真的确定request一定得是在最后程序才不会报错吗? 我想你是被我的言论误导了,只有我说是“最后”,代码只是在说“尽可能往后”。空口无凭,我们来看代码。

for name, param in params.items():
    if name == 'request':
        found = True
        continue        
    if found and (param.kind != inspect.Parameter.VAR_POSITIONAL and param.kind != inspect.Parameter.KEYWORD_ONLY and param.kind != inspect.Parameter.VAR_KEYWORD):
        raise ValueError('request parameter must be the last named parameter in function: %s%s' % (fn.__name__, str(sig)))   

if found and (param.kind != VAR_POSITIONAL and param.kind != KEYWORD_ONLY and param.kind != VAR_KEYWORD) 重点是这句,当找到'request'参数后,如果后面的参数不是VAR_POSITIONAL,也不是KEYWORD_ONLY,还不是VAR_KEYWORD的时候就报错,绕不绕?我看到都纠结死了。换一种写法

if found and (param.kind not in (VAR_POSITIONAL, KEYWORD_ONLY, VAR_KEYWORD))

再换一种写法:

if found and (param.kind in (POSITIONAL_ONLY, POSITIONAL_OR_KEYWORD))

翻译成中文的意思就是:如果找到request参数的话,后面还有POSITIONAL_ONLY, POSITIONAL_OR_KEYWORD)这两类参数就报错,否则是不会报错的!

def foo(request, a)  # 这种是会报错
def foo(a, request, *b)  # 这种不报错
def foo(a, *b, request)  # 这种也不报错
def foo(a, *b, request, c)  # 这种还是不报错
def foo(a, *b, c, request, **d)  # 这种还是不报错
def foo(a, *b, c, **d,  request)  # 这种系统会报错,VAR_KEYWORD只能是最后一个参数    

以上的例子你可以随便拿去测试,看看我是否所言非虚。

这些例子足以说明request并非像你想象的那么固定,而是可变的,接下来看看再好的替代方案,如果想要request位置确定,很简单,如果有request,让它永远处在第一位好了!代码也可以非常简洁:

def has_request_arg(fn):
    sig = inspect.signature(fn)
    params = sig.parameters
    if 'request' in params:
        if 'request' != list(params.keys())[0]:
            raise ValueError('request must be the first parameter in function: %s%s' % (fn.__name__, str(sig)))
        return True
    return False

但我还是觉得没有太大的必要指明request的位置,在这项目中,RequestHandler的代码虽然是存在很多问题的,但瑕不掩瑜,我还是在这里学会搭建我的博客的,欢迎来踩,交流讨论什么都行,有很多东西都是不辩不明的。

灰_手

#9 Created at ... [Delete] [Delete and Lock User]
def foo(a, *request):pass
def foo(a, **request):pass

于是我丧心病狂再测试两个应该报错的例子,然而还是没有报错... 参考可以,切莫盲从,知其然,更要知所以然。

Rand01ph

#10 Created at ... [Delete] [Delete and Lock User]

这个是否报错是要配合url参数的,如果你要说url里面不传参数,但是function里面传request,或者*request,那就当我前面的都没说吧。。。。

我说这个是框架的设计和约定,并不是代码非要这么写。


  • 1
  • 2

Reply