图文无关, 单纯是我拍的照片
前情:
我有一段大一时候写的代码, 用于自动完成学校的经典阅读试卷, 最初版本参照 @左易方 学长的博客, 并摒弃了他字符串拼接式的提交让代码更清晰.
之后我做了更多, 我使用了 Flask 完成了一个原网页的代理, 以此公开我所修改一些内容, 大概流程就是对于每一个请求, 如果是不需要变动的, 那么使用 requests 原样转发, 如果是则按需变动.
在最终提交部分, 有一个这样的 post (的确是post, 而且是用 JavaScript 字符拼接得到的)
1 | 26866=94166&26866=94167&26866=94168&26866=94169&27352=95873&27352=95874&27352=95875&27352=95876 |
大致意思是
1 | 26866 对应了答案 94166 |
对于 Flask 得到的请求, 我采用如下方式转发
1 | t = sess.post(url, request.form) // sess 是第一次访问页面时分配的 |
到四月份一直相安无事. 直到今天坏了, 上面那一段提交后实际上得到的是
1 | 26866=94166&27352=95873 |
第一个重复项之后的都丢失了, 我第一反应就是处理的时候重复项因为未知原因被丢掉了, 大概看了看文档就知道如何做了
1 | t = sess.post(url, request.form.lists()) // sess 是第一次访问页面时分配的 |
问题到此解决, 但是——
但是我之前一直都好好的啊, 接下来是探秘.
requests 中请求的构造在models.py
的prepare_body
里, 构造非 stream 非 file body时, 使用的是self._encode_params(data)
, 核心代码如下
1 | result = [] |
result
在这里还是个list
, 不用担心重复问题, 那么问题就一定是在for k, vs in to_key_val_list(data)
了, to_key_val_list
是utils.py
里的一个函数, 简单粗暴
1 | def to_key_val_list(value): |
只关注最后几行, Mapping
在collections
里定义, isinstance(value, Mapping)
在这里是真的, 那么问题就在value.items()
或者list(value)
中了.
此处我以前是直接传入的 Flask 中 request.form
, 其类型是 werkzeug.datastructures.ImmutableMultiDict
, 翻一翻代码可以发现, 它的items()
方法继承自MultiDict
, 代码如下
1 | def items(self, multi=False): |
iteritems
方法来自_compat.py
1 | iteritems = lambda d, *args, **kwargs: iter(d.items(*args, **kwargs)) |
iteritems(dict, self)
即dict.items(self)
(莫名像 Linux 的系统调用宏)
又dict.items
要求其参数具有len
和iter
两个方法来生成新的 Dictionary view objects, (这是 Python 的鸭子类型的表现). 且MultiDict
的len
和iter
实际上是继承自dict
, 因此, len(MultiDict)
时实际上是把MultiDict
当成dict
给出的值, 那么此处实际上是先把对象转成了dict
, 然后才取其items
的.
也就是说, 在一层层的转换过程中, 某一层把[('26866', '94166'), ('26866', '94167')]
变成了{'26866': '94166'}
, 然后又取items
变成了[('26866', '94166')]
, 因此导致了重复键的其他值丢失.
另一方面, 这样一层层下来, 做了不少无用功, 而这些无用功很大程度上不是写代码的人造成的, 而是语言造成的, 也无怪乎 Python 的性能差了. 但是写起来舒服哇.
下周二就要考试了, 还花了半晚上 de 这个 bug, 啊, 滚去复习