有没有比使用 for 循环更 pythonic / 更高效的方法来循环遍历包含列表的字典呢?

4

在使用getJSON格式的API中提取信息后,我现在正尝试以一种高效的方式计算price的平均值。

data(来自API调用的示例响应):

...
{u'status': u'success', u'data': {u'context_id': u'2', u'app_id': u'123', u'sales': [{u'sold_at': 133, u'price': u'1.8500', u'hash_name': u'Xuan881', u'value': u'-1.00000'}, {u'sold_at': 139, u'price': u'2.6100', u'hash_name': u'Xuan881', u'value': u'-1.00000'},
... etc.

我已经用以下代码成功实现了:

我已经使用以下代码:

len_sales = len(data["data"]["sales"])
total_p = 0 
for i in range(0,len_sales):
    total_p += float(data["data"]["sales"][i]["price"])
average = total_p/len_sales
print average

然而,由于检索到的data字典很大,因此在输出显示之前需要等待相当长的时间。

因此,我想知道是否有更高效和/或更pythonic的方法来以更短的时间实现相同的结果。


你能发布有效的数据吗? - Sushant
2个回答

7
首先,你不是在循环一个字典,而是在循环一个恰好在字典中的列表。
其次,对于列表中的每个值都需要执行某些操作,这本质上需要访问列表中的每个值;没有办法避免线性成本。
因此,唯一可用的是微小的优化,这可能不会有太大的区别——如果您的代码太慢,那么快10%也无济于事;如果您的代码已经足够快,那么您就不需要它,但偶尔也是需要的。
在这种情况下,几乎所有的微小优化也会使您的代码更易读且更符合Python风格,所以没有理由不去做:
首先,你两次访问了`data["data"]["sales"]`。这样做的性能成本可能可以忽略不计,但它也会使您的代码难以阅读,因此让我们来修复一下:
sales = data["data"]["sales"]

接下来,不要再循环 for i in range(0, len_sales): 来使用 sales[i],更快,并且更易读的方式是直接循环 sales

for sale in sales:
    total_p += float(sale["price"])

现在我们可以将这个循环转换为一个推导式,稍微更加高效(尽管部分效率会因为添加生成器而抵消—你可能需要测试一下):

prices = (float(sale["price"]) for sale in sales)

直接将该值传递给 sum

total_p = sum(float(sale["price"]) for sale in sales)

我们也可以使用Python自带的mean函数来代替手动计算:
average = statistics.mean(float(sale["price"]) for sale in sales)

除此之外,您显然在使用Python 2,因此需要安装PyPI上的非官方后移版本(官方的stats后移版本仅适用于3.1及以上版本; 2.x版本已经被放弃),所以让我们跳过这部分。

将所有内容组合在一起:

sales = data["data"]["sales"]
total = sum(float(sale["price"]) for sale in sales)
average = total / len(sales)

有几件事情可能会有所帮助——如果有关系的话,您肯定需要使用timeit进行测试:

您可以使用operator.itemgetter获取price项。这意味着您的表达式现在只是链接两个函数调用,这意味着您可以链接两个map调用:

total = sum(map(float, map(operator.itemgetter("price"), sales)))

对于不是来自Lisp背景的人来说,这可能比推导式难以阅读,但它肯定不是太糟糕了,而且可能会更快。


另外,对于中等大小的输入,构建一个临时列表有时是值得的。当然,你浪费时间分配内存和复制数据,但迭代列表比迭代生成器快,所以唯一确定的方法是进行测试。


还有一件事可能会有所不同,那就是将整个内容移动到一个函数中。顶层代码没有局部变量,只有全局变量,查找速度较慢。

如果你真的需要挤出最后几个百分点,有时甚至值得将全局和内置函数如float复制到本地。当然,这对于map没有帮助(因为我们只访问它们一次),但对于推导式可能有帮助,因此我将展示如何执行:

def total_price(sales):
    _float = float
    pricegetter = operator.itemgetter("price")
    return sum(map(_float, map(pricegetter, sales)))

衡量代码性能的最好方法是使用 timeit 模块,或者如果您使用的是IPython,则可以使用 %timeit 魔法指令。它的使用方式如下:

In [3]: %%timeit
... total_p = 0 
... for i in range(0,len_sales):
...     total_p += float(data["data"]["sales"][i]["price"])
10000 loops, best of 3: 28.4 µs per loop
In [4]: %timeit sum(float(sale["price"]) for sale in sales)
10000 loops, best of 3: 18.4 µs per loop
In [5]: %timeit sum(map(float, map(operator.itemgetter("price"), sales)))
100000 loops, best of 3: 16.9 µs per loop
In [6]: %timeit sum([float(sale["price"]) for sale in sales])
100000 loops, best of 3: 18.2 µs per loop
In [7]: %timeit total_price(sales)
100000 loops, best of 3: 17.2 µs per loop

在我的笔记本上,使用您提供的示例数据:
  • 直接循环遍历sales并使用生成器表达式而非语句,速度大约快35%。
  • 使用列表推导式而非生成器表达式,速度比前者快1%。
  • 使用mapitemgetter而非生成器表达式,速度快约10%。
  • 将其包装在函数中并缓存局部变量会使事情稍微变慢。(不足为奇——如上所述,由于map,我们每个名称仅进行一次查找,因此我们只是为可能没有任何好处的微小开销添加了一个微小开销。)

总的来说,在我的笔记本上,对于这个特定的输入,sum(map(…map(…)))是最快的。

但是当差异小到10%都很重要时,您肯定会想在您的实际环境和实际输入上重复此测试。不能仅仅假设细节会转移。


还有一件事:如果您真的需要加速,通常最简单的方法就是使用PyPy解释器而不是通常的CPython解释器运行完全相同的代码。重复一些以上测试:

In [4]: %timeit sum(float(sale["price"]) for sale in sales)
680 ns ± 19.8 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [5]: %timeit sum(map(float, map(operator.itemgetter("price"), sales)))
800 ns ± 24.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [6]: %timeit sum([float(sale["price"]) for sale in sales])
694 ns ± 24.4 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

现在生成器表达式版本是最快的,但更重要的是,这三个版本的速度大约比CPython快20倍。 2000%的改进比35%的改进要好得多。


非常感谢您的帮助!我真的很感激您在回答这个问题时所花费的细节与时间 :) 祝您有愉快的一天。 - Enigmatic
关于您最新的编辑,有没有可能编辑帖子以显示“最快”的解决方案?谢谢 :) - Enigmatic
完美!谢谢。 - Enigmatic
1
这太棒了! - Sushant

1

您可以使用一个名为statistics的库,找到销售清单的平均值。要获取销售清单,您可以使用列表推导式 -

prices = [float(v) for k, v in i.iteritems() for i in data["data"]["sales"] if k == "price"]

这会给你一个价格列表。现在,你需要做的就是使用上述库。
mean(prices)

或者,你可以做像这样的事情 -
mean_price = sum(prices) / len(prices)

你将拥有价格的平均值。使用列表推导式,你已经优化了你的代码。查看this并阅读答案的最后一段。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接