【问题标题】:Is there a more pythonic/more efficient way to loop through dictionary containing lists rather than using for loops?是否有一种更 Pythonic/更有效的方式来遍历包含列表的字典而不是使用 for 循环?
【发布时间】:2018-12-29 18:29:19
【问题描述】:

使用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 字典很大,因此在显示输出之前似乎有相当长的等待时间。

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

【问题讨论】:

  • 你能发布有效数据吗?

标签: python python-2.7 list loops for-loop


【解决方案1】:

首先,您不是在遍历字典,而是在遍历恰好位于字典内的列表。

其次,为列表中的每个值做某事本质上需要访问列表中的每个值;没有办法绕过线性成本。

所以,唯一可用的就是微优化,这可能不会产生太大影响——如果你的代码太慢,快 10% 也无济于事,如果你的代码已经足够快,你就不用不需要它——但偶尔需要它们。

在这种情况下,几乎所有的微优化也会使您的代码更具可读性和 Python 风格,因此没有充分的理由这样做:


首先,您访问了两次data["data"]["sales"]。这样做的性能成本可能可以忽略不计,但它也会降低代码的可读性,所以让我们来解决这个问题:

sales = data["data"]["sales"]

接下来,不是为了使用sales[i]而循环for i in range(0, len_sales):,而是循环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 上安装 unofficial backport(官方的 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%。
  • 使用列表推导而不是geneexpr 比这快大约1%。
  • 使用mapitemgetter 代替genexpr 大约快10%。
  • 将其包装在一个函数中并缓存本地变量会使事情变得稍微慢一些。 (不足为奇 - 如上所述,由于map,我们对每个名称都只进行了一次查找,因此我们只是增加了一点开销,而收益可能为 0。)

总的来说,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% 的改进要好得多。

【讨论】:

  • 非常感谢您的帮助!我非常感谢您为这个答案付出的细节和时间:)祝您有美好的一天。
  • 关于您的最新编辑,您是否有机会编辑帖子以显示“最快”的解决方案?谢谢:)
  • 完美!谢谢。
  • 这太棒了!
【解决方案2】:

您可以使用一个名为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 并阅读答案的最后一段

【讨论】:

    猜你喜欢
    • 2013-10-31
    • 2018-04-23
    • 1970-01-01
    • 2019-09-30
    • 2012-02-15
    • 2020-09-25
    • 1970-01-01
    • 2020-03-25
    • 1970-01-01
    相关资源
    最近更新 更多