使用后get
从中提取信息API
的JSON
格式,我现在试图计算的平均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的方式来实现相同的结果,但是在更短的时间内.
1> abarnert..:
首先,你没有循环通过一个字典,你正在循环一个碰巧在dict里面的列表.
其次,为列表中的每个值执行某些操作本身就需要访问列表中的每个值; 没有办法绕线性成本.
因此,唯一可用的是微优化,这可能不会有太大的区别 - 如果你的代码太慢,10%的速度没有帮助,如果你的代码已经足够快,你就不需要了它 - 但有时他们是需要的.
在这种情况下,几乎所有的微优化也使你的代码更具可读性和Pythonic,所以没有充分的理由不这样做:
首先,你要访问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)
我们也可以使用mean
Python附带的函数而不是手动执行:
average = statistics.mean(float(sale["price"]) for sale in sales)
...除了你显然使用Python 2,所以你需要安装PyPI 的非官方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%.
使用列表推导而不是genexpr比这快1%.
使用map
和itemgetter
代替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%的改善好很多.