专栏首页云爬虫技术研究笔记由一个简单的Python合并字典问题引发的思考,如何优化我们的代码?

由一个简单的Python合并字典问题引发的思考,如何优化我们的代码?

作者: Lateautumn4lin 来源:云爬虫技术研究笔记

AKA 逆向小学生

今天我们的题目是由一个简单的Python合并字典问题引发的思考,如何优化我们的代码?,为什么会有这个话题呢?起因是今天和一位刚刚面试完Python开发岗位的朋友交流,这个问题也是他在面试中遇到的问题:

怎么用一个简单的表达式合并Python中的两个Dict

我相信很多人会质疑这个问题很需要解答吗?好吧,But!这个问题虽然是一道很简单的问题,并且解题的思路也有很多种。不过问题虽小,但是我们也借此分析一下更深层次的东西,关于代码如何优化优化思路等等。

首先我们简单的思考一下,Python中合并两个Dict有哪些方法?我们分别举Python3Python2的例子。

01

我们举个小案例~~

假设我们现在有DictXX和DictYY,我们想要合并它们得到新的DictZZ,我们会这么做:

  • 在Python 3.5或更高版本中:
z = {**x, **y}
  • 在Python 2(或3.4或更低版本)中,编写一个函数:
def merge_two_dicts(x, y):
    z = x.copy()   # start with x's keys and values
    z.update(y)    # modifies z with y's keys and values & returns None
    return z
z = merge_two_dicts(x, y)

02

Python3.5版本以上方法分析

假设我们有两个字典,并且想要将它们合并为新字典而不更改原始字典:

x = {'a': 1, 'b': 2}
y = {'b': 3, 'c': 4}

理想的结果是获得一个z是合并后的新字典,第二个Dict的值覆盖第一个字典Dict的值。

>>> z
{'a': 1, 'b': 3, 'c': 4}

在PEP 448中提出并从Python 3.5开始可用的新语法是:

z = {**x, **y}

它只需要一个非常简洁的表达式就可以完成,另外,我们也可以使用解包来进行操作:

z = {**x, 'foo': 1, 'bar': 2, **y}

结果如下:

>>> z
{'a': 1, 'b': 3, 'foo': 1, 'bar': 2, 'c': 4}

它现在正式的在3.5的发布时间表中实现,PEP 478,并且已进入Python 3.5的新功能文档。

我们大致看一下这个新功能的使用方式

这个功能允许我们在同一个表达式中使用多个解包表达式,能够很方便的合并迭代器和普通的列表,而不需要将迭代器先转化成列表再进行合并。

但是,由于许多组织仍在使用Python 2,因此我们可能希望以向后兼容的方式进行操作。在Python 2Python 3.0-3.4中可用的经典Pythonic方法是分两个步骤完成的:

z = x.copy()
z.update(y) # which returns None since it mutates z

这种方法中,我们拷贝x生成新的对象z,再使用dictupdate的方法合并两个dict

03

Python3.5版本以下方法分析

如果我们尚未使用Python 3.5,或者需要编写向后兼容的代码,并且希望在单个表达式中使用它,则最有效的方法是将其放入函数中:

def merge_two_dicts(x, y):
    """Given two dicts, merge them into a new dict as a shallow copy."""
    z = x.copy()
    z.update(y)
    return z

然后我们需要这样使用函数:

z = merge_two_dicts(x, y)

您还可以创建一个合并多个dict的函数,并且我们可以指定任意数量的dict

def merge_dicts(*dict_args):
    """
    Given any number of dicts, shallow copy and merge into a new dict,
    precedence goes to key value pairs in latter dicts.
    """
    result = {}
    for dictionary in dict_args:
        result.update(dictionary)
    return result

此函数将在Python 23中适用于所有字典。我们可以这样使用:

z = merge_dicts(a, b, c, d, e, f, g)

不过注意的是:越往后的dict的键值优先度越高,会覆盖前面的键值。

04

发散脑洞,我们想想有没有其他回答

Python 2中,我们还可以这么操作:

z = dict(x.items() + y.items())

Python 2中,我们使用.items()会得到list,也就是我们将会在内存中创建两个列表,然后在内存中创建第三个列表,其长度等于前两个字典的长度,最后丢弃所有三个列表以创建字典,就是我们需要的Dict。 但是注意,我们决不能在Python 3中这么使用,在Python 3中,这会失败失败是因为我们是将两个dict_items对象而不是两个列表加在一起。

>>> c = dict(a.items() + b.items())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'dict_items' and 'dict_items'

当然,我们真的想要实现的话,我们也可以强制转换,将它们明确创建为列表,例如z = dict(list(x.items()) + list(y.items())),但是这反而浪费了资源和计算能力。

类似地,当值是不可散列的对象(例如列表)时,items()在Python 3(viewitems()在Python 2.7中)进行联合也将失败。即使您的值是可哈希的,由于集合在语义上是无序的,因此关于优先级的行为是不确定的。所以不要这样做:

>>> c = dict(a.items() | b.items())

我们演示一下值不可散列时会发生的情况:

>>> x = {'a': []}
>>> y = {'b': []}
>>> dict(x.items() | y.items())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

这是一个示例,其中y应该优先,但是由于集合的任意顺序,保留了x的值:

>>> x = {'a': 2}
>>> y = {'a': 1}
>>> x.items() | y.items()
{('a', 1), ('a', 2)}
>>> dict(x.items() | y.items())
{'a': 2}

另外一种我们不应该使用的另一种技巧:

z = dict(x, **y)

这使用了dict构造函数,并且非常快速且具有内存效率(甚至比我们的两步过程略高),但是除非我们确切地知道里面正在发生什么(也就是说,第二个dict作为关键字参数传递给dict,构造函数)我们才能使用,要不然这个表达式很难阅读,有时我们并不能很快的理解这算什么用法,因此不算Pythonic

由于这种情况的存在,我们看看在django中修复的用法示例。

字典旨在获取可散列的键(例如,frozensettuple),但是当键不是字符串时,此方法在Python 3中失败。

>>> c = dict(a, **b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: keyword arguments must be strings

在邮件列表中,大佬Guido van Rossum写道:

我宣布dict({},** {1:3})是非法的使用方式,因为这是对**机制的滥用。
显然dict(x,** y)和直接调用x.update(y)并返回x这种“酷”的操作很类似。
但是我个人觉得它比“酷”的操作更低俗。

不过根据我的理解(以及对大佬的话的理解),dict(**y)命令的预期用途是为了创建可读性强的字典,例如:

dict(a=1, b=10, c=11)

用来代替

{'a': 1, 'b': 10, 'c': 11}

在这个地方使用**运算符也不会滥用该机制,我们使用**正是为了将dict作为关键字传递而设计的。

05

最后看看那些性能较差的实现方案

这些方法的性能较差,但是它们将提供正确的行为。它们的性能将不及copyupdate新的解包方式,因为它们在更高的抽象级别上遍历每个键值对,但它们确实遵循优先级的顺序(后者决定了优先级)

  • 我们可以在使用生成式来做:
{k: v for d in dicts for k, v in d.items()} # iteritems in Python 2.7

或在python 2.6中(也许在引入生成器表达式时早在2.4中):

dict((k, v) for d in dicts for k, v in d.items())

itertools.chain 迭代器的骚操作

import itertools
z = dict(itertools.chain(x.iteritems(), y.iteritems()))

ChainMap骚操作

>>> from collections import ChainMap
>>> x = {'a':1, 'b': 2}
>>> y = {'b':10, 'c': 11}
>>> z = ChainMap({}, y, x)
>>> for k, v in z.items():
        print(k, '-->', v)

a --> 1
b --> 10
c --> 11

06

我们做做时间分析

我将仅对已知行为正确的用法进行性能分析。

import timeit

Ubuntu 18上完成以下操作 在Python 2.7(系统Python)中:

>>> min(timeit.repeat(lambda: merge_two_dicts(x, y)))
0.5726828575134277
>>> min(timeit.repeat(lambda: {k: v for d in (x, y) for k, v in d.items()} ))
1.163769006729126
>>> min(timeit.repeat(lambda: dict(itertools.chain(x.iteritems(), y.iteritems()))))
1.1614501476287842
>>> min(timeit.repeat(lambda: dict((k, v) for d in (x, y) for k, v in d.items())))
2.2345519065856934

Python 3.5中:

>>> min(timeit.repeat(lambda: {**x, **y}))
0.4094954460160807
>>> min(timeit.repeat(lambda: merge_two_dicts(x, y)))
0.7881555100320838
>>> min(timeit.repeat(lambda: {k: v for d in (x, y) for k, v in d.items()} ))
1.4525277839857154
>>> min(timeit.repeat(lambda: dict(itertools.chain(x.items(), y.items()))))
2.3143140770262107
>>> min(timeit.repeat(lambda: dict((k, v) for d in (x, y) for k, v in d.items())))
3.2069112799945287

07

我们的结论

经过我们之前的一系列分析和实验,我们可以得到这个问题的结论

  • Python 2中我们就采用copy加上update的方案
  • Python 3中我们就采用多重解包的方案

不过对比以上两种,显然多重解包更快而且更简洁,针对大家不熟悉Python 3可以参考我之前的一篇文章Python2寿命只剩一个月啦!还不快赶紧学起Python3酷炫到爆的新特性!,可以帮助大家快速的切换成Python 3开发,不过注意的是Python 3高版本和Python 2.7差别也是比较大,因此大家要是涉及线上业务的切换,请注意

最后我们来谈谈优化代码的问题,从这个问题入手,我们可以总结出优化代码的思路:

我们分析出有哪些解决方案?

哪些解决方案是有效的?

这些有效的方案怎么做对比?

最佳的方案需要我们做出哪些牺牲?

本文分享自微信公众号 - 云爬虫技术研究笔记(cloudcrawler),作者:Lateautumn4lin

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-11-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 轻JS逆向分析“攒经验”项目之某交易所Sign加密参数逆向分析

    这篇文章是公众号《云爬虫技术研究笔记》的《JS逆向分析“攒经验”项目》的第一篇:《某交易所Sign加密参数逆向分析》

    云爬虫技术研究笔记
  • 听说这个爬虫面试题很难?看完你就知道怎么做了

    最近(2019年6月)有一个爬虫面试题(http://shaoq.com:7777/exam)在圈内看起来挺火的,经常在各个爬虫群里看到它被提到,而几乎所有提到...

    云爬虫技术研究笔记
  • 下一代容器架构已出,Docker何去何处?看看这里的6问6答!!

    我猜很多人一看这个标题已经感觉很懵逼了,什么?下一代容器都出来了,我还没学Docker呢!!!

    云爬虫技术研究笔记
  • Winamp退出历史舞台--- AOL-Nullsoft联姻失败

    --------------------------------------------------------------------------------...

    数据和云01
  • 解决stackoverflow打开慢不能注册登录

    问题原因:并不是stackoverflow被墙,而是因为stackoverflow用了google的api,而Google在天朝是用不了的,所以才导致像stac...

    bear_fish
  • 微信为什么没参加AR红包大战?原因竟然是……

    互联网行业2016年不缺黑天鹅事件,但同样也不缺乏黑马产品。作为年度最热领域之一的AR(增强现实),就连续出现黑马产品。支付宝在12月21日推出“AR实景红包”...

    罗超频道
  • C#中的泛型(类型参数的约束)

    RemoveElement方法用于删除数组中指定位置的元素,PrintArrayInfo方法用于输出数组。

    卡尔曼和玻尔兹曼谁曼
  • 从架构到应用,全面解析混合云的优势

    云计算在2016年有了极大的增长。一方面,AWS、阿里云等大型公有云厂商的云计算收入呈爆发式增长且绝对值数据可观;另一方面,通过持续市场培育,云计算的价值逐步被...

    BestSDK
  • C语言中调用系统命令(system popen...)

    相关函数 fork,execve,waitpid,popen 表头文件 #include<stdlib.h> 定义函数 int system(const cha...

    用户5807183
  • 使用Springboot版mybatis逆向生成工具

    mybatis-generatorConfig地址链接:https://pan.baidu.com/s/1TE5ugwmo4UMchOz7VWOAfQ 密码:e...

    听城

扫码关注云+社区

领取腾讯云代金券

玩转腾讯云 有奖征文活动