python字典类型最疯狂的表达方式
一个Python字典表达式谜题
让我们探究一下下面这个晦涩的python字典表达式,以找出在python解释器的中未知的内部到底发生了什么。
# 一个python谜题:这是一个秘密 # 这个表达式计算以后会得到什么结果? >>> {True: 'yes', 1: 'no', 1.0: 'maybe'}
有时候你会碰到一个很有深度的代码示例 --- 哪怕仅仅是一行代码,但是如果你能够有足够的思考,它可以教会你很多关于编程语言的知识。这样一个代码片段,就像是一个Zen kōan
:一个在修行的过程中用来质疑和考验学生进步的问题或陈述。
译者注:Zen kōan
,大概就是修行的一种方式,详情见wikipedia
我们将在本教程中讨论的小代码片段就是这样一个例子。乍看之下,它可能看起来像一个简单的词典表达式,但是仔细考虑时,通过cpython解释器,它会带你进行一次思维拓展的训练。
我从这个短短的一行代码中得到了一个启发,而且有一次在我参加的一个Python会议上,我还把作为我演讲的内容,并以此开始演讲。这也激发了我的python邮件列表成员间进行了一些积极的交流。
所以不用多说,就是这个代码片。花点时间思考一下下面的字典表达式,以及它计算后将得到的内容:
>>> {True: 'yes', 1: 'no', 1.0: 'maybe'}
在这里,我先等会儿,大家思考一下...
5...
4...
3...
2...
1...
OK, 好了吗?
这是在cpython解释器交互界面中计算上述字典表达式时得到的结果:
>>> {True: 'yes', 1: 'no', 1.0: 'maybe'} {True: 'maybe'}
我承认,当我第一次看到这个结果时,我很惊讶。但是当你逐步研究其中发生的过程时,这一切都是有道理的。所以,让我们思考一下为什么我们得到这个 - 我想说的是出乎意料 - 的结果。
这个子字典是从哪里来的
当python处理我们的字典表达式时,它首先构造一个新的空字典对象;然后按照字典表达式给出的顺序赋键和值。
因此,当我们把它分解开的时候,我们的字典表达就相当于这个顺序的语句:
>>> xs = dict() >>> xs[True] = 'yes' >>> xs[1] = 'no' >>> xs[1.0] = 'maybe'
奇怪的是,Python认为在这个例子中使用的所有字典键是相等的:
>>> True == 1 == 1.0 True
OK,但在这里等一下。我确定你能够接受1.0 == 1,但实际情况是为什么True
也会被认为等于1呢?我第一次看到这个字典表达式真的让我难住了。
在python文档中进行一些探索之后,我发现python将bool
作为了int
类型的一个子类。这是在Python 2和Python 3的片段:
“The Boolean type is a subtype of the integer type, and Boolean values behave like the values 0 and 1, respectively, in almost all contexts, the exception being that when converted to a string, the strings ‘False’ or ‘True’ are returned, respectively.” “布尔类型是整数类型的一个子类型,在几乎所有的上下文环境中布尔值的行为类似于值0和1,例外的是当转换为字符串时,会分别将字符串”False“或”True“返回。“(原文)
是的,这意味着你可以在编程时上使用bool
值作为Python中的列表或元组的索引:
>>> ['no', 'yes'][True] 'yes'
但为了代码的可读性起见,您不应该类似这样的来使用布尔变量。(也请建议你的同事别这样做)
Anyway,让我们回过来看我们的字典表达式。
就python而言,True
,1
和1.0
都表示相同的字典键。当解释器计算字典表达式时,它会重复覆盖键True
的值。这就解释了为什么最终产生的字典只包含一个键。
在我们继续之前,让我们再回顾一下原始字典表达式:
>>> {True: 'yes', 1: 'no', 1.0: 'maybe'} {True: 'maybe'}
这里为什么最终得到的结果是以True
作为键呢?由于重复的赋值,最后不应该是把键也改为1.0
了?经过对cpython解释器源代码的一些模式研究,我知道了,当一个新的值与字典的键关联的时候,python的字典不会更新键对象本身:
>>> ys = {1.0: 'no'} >>> ys[True] = 'yes' >>> ys {1.0: 'yes'}
当然这个作为性能优化来说是有意义的 --- 如果键被认为是相同的,那么为什么要花时间更新原来的?在最开始的例子中,你也可以看到最初的True
对象一直都没有被替换。因此,字典的字符串表示仍然打印为以True
为键(而不是1或1.0)。
就目前我们所知而言,似乎看起来像是,结果中字典的值一直被覆盖,只是因为他们的键比较后相等。然而,事实上,这个结果也不单单是由__eq__
比较后相等就得出的。
等等,那哈希值呢?
python字典类型是由一个哈希表数据结构存储的。当我第一次看到这个令人惊讶的字典表达式时,我的直觉是这个结果与散列冲突有关。
哈希表中键的存储是根据每个键的哈希值的不同,包含在不同的“buckets”中。哈希值是指根据每个字典的键生成的一个固定长度的数字串,用来标识每个不同的键。(哈希函数详情)
这可以实现快速查找。在哈希表中搜索键对应的哈希数字串会快很多,而不是将完整的键对象与所有其他键进行比较,来检查互异性。
然而,通常计算哈希值的方式并不完美。并且,实际上会出现不同的两个或更多个键会生成相同的哈希值,并且它们最后会出现在相同的哈希表中。
如果两个键具有相同的哈希值,那就称为哈希冲突(hash collision),这是在哈希表插入和查找元素时需要处理的特殊情况。
基于这个结论,哈希值与我们从字典表达中得到的令人意外的结果有很大关系。所以让我们来看看键的哈希值是否也在这里起作用。
我定义了这样一个类来作为我们的测试工具:
class AlwaysEquals: def __eq__(self, other): return True def __hash__(self): return id(self)
这个类有两个特别之处。
第一,因为它的__eq__
魔术方法(译者注:双下划线开头双下划线结尾的是一些Python的“魔术”对象)总是返回true,所以这个类的所有实例和其他任何对象都会恒等:
>>> AlwaysEquals() == AlwaysEquals() True >>> AlwaysEquals() == 42 True >>> AlwaysEquals() == 'waaat?' True
第二,每个Alwaysequals
实例也将返回由内置函数id()
生成的唯一哈希值值:
>>> objects = [AlwaysEquals(), AlwaysEquals(), AlwaysEquals()] >>> [hash(obj) for obj in objects] [4574298968, 4574287912, 4574287072]
在CPython中,id()
函数返回的是一个对象在内存中的地址,并且是确定唯一的。
通过这个类,我们现在可以创建看上去与其他任何对象相同的对象,但它们都具有不同的哈希值。我们就可以通过这个来测试字典的键是否是基于它们的相等性比较结果来覆盖。
正如你所看到的,下面的一个例子中的键不会被覆盖,即使它们总是相等的:
>>> {AlwaysEquals(): 'yes', AlwaysEquals(): 'no'} { <AlwaysEquals object at 0x110a3c588>: 'yes', <AlwaysEquals object at 0x110a3cf98>: 'no' }
下面,我们可以换个思路,如果返回相同的哈希值是不是就会让键被覆盖呢?
class SameHash: def __hash__(self): return 1
这个SameHash
类的实例将相互比较一定不相等,但它们会拥有相同的哈希值1:
>>> a = SameHash() >>> b = SameHash() >>> a == b False >>> hash(a), hash(b) (1, 1)
一起来看看python的字典在我们试图使用SameHash
类的实例作为字典键时的结果:
>>> {a: 'a', b: 'b'} { <SameHash instance at 0x7f7159020cb0>: 'a', <SameHash instance at 0x7f7159020cf8>: 'b' }
如本例所示,“键被覆盖”的结果也并不是单独由哈希冲突引起的。
Umm..好吧,可以得到什么结论呢?
python字典类型是检查两个对象是否相等,并比较哈希值以确定两个密钥是否相同。让我们试着总结一下我们研究的结果:
{true:'yes',1:'no',1.0:'maybe'}
字典表达式计算结果为{true:'maybe'}
,是因为键true
,1
和1.0
都是相等的,并且它们都有相同的哈希值:
>>> True == 1 == 1.0 True >>> (hash(True), hash(1), hash(1.0)) (1, 1, 1)
也许并不那么令人惊讶,这就是我们为何得到这个结果作为字典的最终结果的原因:
>>> {True: 'yes', 1: 'no', 1.0: 'maybe'} {True: 'maybe'}
我们在这里涉及了很多方面内容,而这个特殊的python技巧起初可能有点令人难以置信 --- 所以我一开始就把它比作是Zen kōan
。
原文链接:https://dbader.org/blog/python-mystery-dict-expression
译文链接:http://vimiix.com/post/2017/12/28/python-mystery-dict-expression/

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
MySQL分表的3种方法
一,先说一下为什么要分表 当一张的数据达到几百万时,你查询一次所花的时间会变多,如果有联合查询的话,我想有可能会死在那儿了。分表的目的就在于此,减小数据库的负担,缩短查询时间。 根据个人经验,mysql执行一个sql的过程如下: 1、接收到sql; 2、把sql放到排队队列中 ; 3、执行sql; 4、返回执行结果。 在这个执行过程中最花时间在什么地方呢?第一,是排队等待的时间;第二,sql的执行时间。其实这二个是一回事,等待的同时,肯定有sql在执行。所以我们要缩短sql的执行时间。 mysql中有一种机制是表锁定和行锁定,为什么要出现这种机制,是为了保证数据的完整性。我举个例子来说吧,如果有两个sql都要修改同一张表的同一条数据,这个时候怎么办呢,是不是两个sql都可以同时修改这条数据呢? 很显然mysql对这种情况的处理是,一种是表锁定(myisam存储引擎),一个是行锁定(innodb存储引擎)。表锁定表示你们都不能对这张表进行操作,必须等我对表操作完才行。行锁定也一样,别的sql必须等我对这条数据操作完了,才能对这条数据进行操作。如果数据太多,一次执行的时间太长,等待的时间就...
- 下一篇
网易云原生架构实践之服务治理【转】
云原生(Cloud Native)的高阶实践是分布式服务化架构。一个良好的服务化架构,需要良好的服务发现、服务治理、服务编排等核心能力。本文为读者解析网易云的服务治理策略及其典型实践。 网易云微服务架构 在优化了版本控制策略,研发并集成了自动化构建和发布工具,实现“项目工程化”之后,网易云开始了分布式服务化架构的探索,希望解决支撑海量用户及产品高速迭代需求下的软件研发成本高、测试部署维护代价大、扩展性差等问题。 业务模块的独立,自然而然形成了基于 Docker 容器的微服务架构。网易云简化的微服务架构如图 1 所示,包括服务注册与发现、分布式配置管理、负载均衡、服务网关、断路器等模块。 图 1 微服务架构 一个产品通常由多个应用组成,容器只是提供一个应用服务的能力,需要把多个应用组合编排起来才能提供服务。在服务编排上,网易云选择开源的 Kubernetes 。Kubernetes 是自动化编排容器应用的开源平台,这些操作不仅包括部署、调度和节点集群间扩展,还包括服务发现和配置服务等架构支持的基础能力。Kubernetes 对应用层面的关注、对微服务、云原生的支持及其生态,正是网易云所需...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8编译安装MySQL8.0.19
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS7安装Docker,走上虚拟化容器引擎之路
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS8安装Docker,最新的服务器搭配容器使用
- Docker使用Oracle官方镜像安装(12C,18C,19C)