图文无关, 单纯是我拍的照片

前情:

我有一段大一时候写的代码, 用于自动完成学校的经典阅读试卷, 最初版本参照 @左易方 学长的博客, 并摒弃了他字符串拼接式的提交让代码更清晰.

之后我做了更多, 我使用了 Flask 完成了一个原网页的代理, 以此公开我所修改一些内容, 大概流程就是对于每一个请求, 如果是不需要变动的, 那么使用 requests 原样转发, 如果是则按需变动.

在最终提交部分, 有一个这样的 post (的确是post, 而且是用 JavaScript 字符拼接得到的)

26866=94166&26866=94167&26866=94168&26866=94169&27352=95873&27352=95874&27352=95875&27352=95876

大致意思是

26866 对应了答案 94166
26866 对应了答案 94167
26866 对应了答案 94168
26866 对应了答案 94169
27352 对应了答案 95873
27352 对应了答案 95874
27352 对应了答案 95875
27352 对应了答案 95876 

对于 Flask 得到的请求, 我采用如下方式转发

t = sess.post(url, request.form) // sess 是第一次访问页面时分配的
return t.content

到四月份一直相安无事. 直到今天坏了, 上面那一段提交后实际上得到的是

26866=94166&27352=95873

第一个重复项之后的都丢失了, 我第一反应就是处理的时候重复项因为未知原因被丢掉了, 大概看了看文档就知道如何做了

t = sess.post(url, request.form.lists()) // sess 是第一次访问页面时分配的
return t.content

问题到此解决, 但是——


但是我之前一直都好好的啊, 接下来是探秘.

requests 中请求的构造在models.pyprepare_body里, 构造非 stream 非 file body时, 使用的是self._encode_params(data), 核心代码如下

result = []
for k, vs in to_key_val_list(data):
    if isinstance(vs, basestring) or not hasattr(vs, '__iter__'):
        vs = [vs]
    for v in vs:
        if v is not None:
            result.append(
                (k.encode('utf-8') if isinstance(k, str) else k,
                 v.encode('utf-8') if isinstance(v, str) else v))
return urlencode(result, doseq=True)

result 在这里还是个list, 不用担心重复问题, 那么问题就一定是在for k, vs in to_key_val_list(data)了, to_key_val_listutils.py里的一个函数, 简单粗暴

def to_key_val_list(value):
    """Take an object and test to see if it can be represented as a
    dictionary. If it can be, return a list of tuples, e.g.,

    ::

        >>> to_key_val_list([('key', 'val')])
        [('key', 'val')]
        >>> to_key_val_list({'key': 'val'})
        [('key', 'val')]
        >>> to_key_val_list('string')
        ValueError: cannot encode objects that are not 2-tuples.

    :rtype: list
    """
    if value is None:
        return None

    if isinstance(value, (str, bytes, bool, int)):
        raise ValueError('cannot encode objects that are not 2-tuples')

    if isinstance(value, Mapping):
        value = value.items()

    return list(value)

只关注最后几行, Mappingcollections里定义, isinstance(value, Mapping)在这里是真的, 那么问题就在value.items()或者list(value)中了.

此处我以前是直接传入的 Flask 中 request.form, 其类型是 werkzeug.datastructures.ImmutableMultiDict, 翻一翻代码可以发现, 它的items()方法继承自MultiDict, 代码如下

def items(self, multi=False):
    """Return an iterator of ``(key, value)`` pairs.
    :param multi: If set to `True` the iterator returned will have a pair
                  for each value of each key.  Otherwise it will only
                  contain pairs for the first value of each key.
    """

    for key, values in iteritems(dict, self):
        if multi:
            for value in values:
                yield key, value
        else:
            yield key, values[0] 

iteritems方法来自_compat.py

iteritems = lambda d, *args, **kwargs: iter(d.items(*args, **kwargs)) 

iteritems(dict, self)dict.items(self)(莫名像 Linux 的系统调用宏)

dict.items要求其参数具有leniter两个方法来生成新的 Dictionary view objects, (这是 Python 的鸭子类型的表现). 且MultiDictleniter实际上是继承自dict, 因此, len(MultiDict)时实际上是把MultiDict当成dict给出的值, 那么此处实际上是先把对象转成了dict, 然后才取其items的.

也就是说, 在一层层的转换过程中, 某一层把[('26866', '94166'), ('26866', '94167')]变成了{'26866': '94166'}, 然后又取items变成了[('26866', '94166')], 因此导致了重复键的其他值丢失.

另一方面, 这样一层层下来, 做了不少无用功, 而这些无用功很大程度上不是写代码的人造成的, 而是语言造成的, 也无怪乎 Python 的性能差了. 但是写起来舒服哇.


下周二就要考试了, 还花了半晚上 de 这个 bug, 啊, 滚去复习