一个Python字典表达式谜题

让我们探究一下下面这个晦涩的python字典表达式,以找出在python解释器的中未知的内部到底发生了什么。

# 一个python谜题:这是一个秘密
# 这个表达式计算以后会得到什么结果? >>> {True: 'yes', 1: 'no', 1.0: 'maybe'}
  • 1
  • 2
  • 3
  • 4

有时候你会碰到一个很有深度的代码示例 — 哪怕仅仅是一行代码,但是如果你能够有足够的思考,它可以教会你很多关于编程语言的知识。这样一个代码片段,就像是一个Zen kōan:一个在修行的过程中用来质疑和考验学生进步的问题或陈述。

译者注:Zen kōan,大概就是修行的一种方式,详情见wikipedia

我们将在本教程中讨论的小代码片段就是这样一个例子。乍看之下,它可能看起来像一个简单的词典表达式,但是仔细考虑时,通过cpython解释器,它会带你进行一次思维拓展的训练。

我从这个短短的一行代码中得到了一个启发,而且有一次在我参加的一个Python会议上,我还把作为我演讲的内容,并以此开始演讲。这也激发了我的python邮件列表成员间进行了一些积极的交流。

所以不用多说,就是这个代码片。花点时间思考一下下面的字典表达式,以及它计算后将得到的内容:

>>> {True: 'yes', 1: 'no', 1.0: 'maybe'}
  • 1

在这里,我先等会儿,大家思考一下…

  • 5…
  • 4…
  • 3…
  • 2…
  • 1…

OK, 好了吗?

这是在cpython解释器交互界面中计算上述字典表达式时得到的结果:

>>> {True: 'yes', 1: 'no', 1.0: 'maybe'}
{True: 'maybe'}
  • 1
  • 2

我承认,当我第一次看到这个结果时,我很惊讶。但是当你逐步研究其中发生的过程时,这一切都是有道理的。所以,让我们思考一下为什么我们得到这个 - 我想说的是出乎意料 - 的结果。

这个子字典是从哪里来的

当python处理我们的字典表达式时,它首先构造一个新的空字典对象;然后按照字典表达式给出的顺序赋键和值。

因此,当我们把它分解开的时候,我们的字典表达就相当于这个顺序的语句:

>>> xs = dict()
>>> xs[True] = 'yes'
>>> xs[1] = 'no'
>>> xs[1.0] = 'maybe'
  • 1
  • 2
  • 3
  • 4

奇怪的是,Python认为在这个例子中使用的所有字典键是相等的:

>>> True == 1 == 1.0
True
  • 1
  • 2

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'
  • 1
  • 2

但为了代码的可读性起见,您不应该类似这样的来使用布尔变量。(也请建议你的同事别这样做)

Anyway,让我们回过来看我们的字典表达式。

就python而言,True11.0都表示相同的字典键。当解释器计算字典表达式时,它会重复覆盖键True的值。这就解释了为什么最终产生的字典只包含一个键。

在我们继续之前,让我们再回顾一下原始字典表达式:

>>> {True: 'yes', 1: 'no', 1.0: 'maybe'}
{True: 'maybe'}
  • 1
  • 2

这里为什么最终得到的结果是以True作为键呢?由于重复的赋值,最后不应该是把键也改为1.0了?经过对cpython解释器源代码的一些模式研究,我知道了,当一个新的值与字典的键关联的时候,python的字典不会更新键对象本身:

>>> ys = {1.0: 'no'}
>>> ys[True] = 'yes'
>>> ys
{1.0: 'yes'}
  • 1
  • 2
  • 3
  • 4

当然这个作为性能优化来说是有意义的 — 如果键被认为是相同的,那么为什么要花时间更新原来的?在最开始的例子中,你也可以看到最初的True对象一直都没有被替换。因此,字典的字符串表示仍然打印为以True为键(而不是1或1.0)。

就目前我们所知而言,似乎看起来像是,结果中字典的值一直被覆盖,只是因为他们的键比较后相等。然而,事实上,这个结果也不单单是由__eq__比较后相等就得出的。

等等,那哈希值呢?

python字典类型是由一个哈希表数据结构存储的。当我第一次看到这个令人惊讶的字典表达式时,我的直觉是这个结果与散列冲突有关。

哈希表中键的存储是根据每个键的哈希值的不同,包含在不同的“buckets”中。哈希值是指根据每个字典的键生成的一个固定长度的数字串,用来标识每个不同的键。(哈希函数详情

这可以实现快速查找。在哈希表中搜索键对应的哈希数字串会快很多,而不是将完整的键对象与所有其他键进行比较,来检查互异性。

然而,通常计算哈希值的方式并不完美。并且,实际上会出现不同的两个或更多个键会生成相同的哈希值,并且它们最后会出现在相同的哈希表中。

如果两个键具有相同的哈希值,那就称为哈希冲突(hash collision),这是在哈希表插入和查找元素时需要处理的特殊情况。

基于这个结论,哈希值与我们从字典表达中得到的令人意外的结果有很大关系。所以让我们来看看键的哈希值是否也在这里起作用。

我定义了这样一个类来作为我们的测试工具:

class AlwaysEquals:
def __eq__(self, other):
return True def __hash__(self):
return id(self)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这个类有两个特别之处。

第一,因为它的__eq__魔术方法(译者注:双下划线开头双下划线结尾的是一些Python的“魔术”对象)总是返回true,所以这个类的所有实例和其他任何对象都会恒等:

>>> AlwaysEquals() == AlwaysEquals()
True
>>> AlwaysEquals() == 42
True
>>> AlwaysEquals() == 'waaat?'
True
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

第二,每个Alwaysequals实例也将返回由内置函数id()生成的唯一哈希值值:

>>> objects = [AlwaysEquals(),
AlwaysEquals(),
AlwaysEquals()]
>>> [hash(obj) for obj in objects]
[4574298968, 4574287912, 4574287072]
  • 1
  • 2
  • 3
  • 4
  • 5

在CPython中,id()函数返回的是一个对象在内存中的地址,并且是确定唯一的。

通过这个类,我们现在可以创建看上去与其他任何对象相同的对象,但它们都具有不同的哈希值。我们就可以通过这个来测试字典的键是否是基于它们的相等性比较结果来覆盖。

正如你所看到的,下面的一个例子中的键不会被覆盖,即使它们总是相等的:

>>> {AlwaysEquals(): 'yes', AlwaysEquals(): 'no'}
{ <AlwaysEquals object at 0x110a3c588>: 'yes',
<AlwaysEquals object at 0x110a3cf98>: 'no' }
  • 1
  • 2
  • 3

下面,我们可以换个思路,如果返回相同的哈希值是不是就会让键被覆盖呢?

class SameHash:
def __hash__(self):
return 1
  • 1
  • 2
  • 3

这个SameHash类的实例将相互比较一定不相等,但它们会拥有相同的哈希值1:

>>> a = SameHash()
>>> b = SameHash()
>>> a == b
False
>>> hash(a), hash(b)
(1, 1)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

一起来看看python的字典在我们试图使用SameHash类的实例作为字典键时的结果:

>>> {a: 'a', b: 'b'}
{ <SameHash instance at 0x7f7159020cb0>: 'a',
<SameHash instance at 0x7f7159020cf8>: 'b' }
  • 1
  • 2
  • 3

如本例所示,“键被覆盖”的结果也并不是单独由哈希冲突引起的。

Umm..好吧,可以得到什么结论呢?

python字典类型是检查两个对象是否相等,并比较哈希值以确定两个密钥是否相同。让我们试着总结一下我们研究的结果:

{true:'yes',1:'no',1.0:'maybe'}字典表达式计算结果为{true:'maybe'},是因为键true11.0都是相等的,并且它们都有相同的哈希值:

>>> True == 1 == 1.0
True
>>> (hash(True), hash(1), hash(1.0))
(1, 1, 1)
  • 1
  • 2
  • 3
  • 4

也许并不那么令人惊讶,这就是我们为何得到这个结果作为字典的最终结果的原因:

>>> {True: 'yes', 1: 'no', 1.0: 'maybe'}
{True: 'maybe'}
  • 1
  • 2

我们在这里涉及了很多方面内容,而这个特殊的python技巧起初可能有点令人难以置信 — 所以我一开始就把它比作是Zen kōan

如果很难理解本文中的内容,请尝试在Python交互环境中逐个去检验一下代码示例。你会收获一些关于python深处知识。

注:转载请保留下面的内容

原文链接:https://dbader.org/blog/python-mystery-dict-expression

译文链接:http://vimiix.com/post/2017/12/28/python-mystery-dict-expression/

[转载]关于python字典类型最疯狂的表达方式的更多相关文章

  1. python字典类型

    字典类型简介 字典(dict)是存储key/value数据的容器,也就是所谓的map.hash.关联数组.无论是什么称呼,都是键值对存储的方式. 在python中,dict类型使用大括号包围: D = ...

  2. Python字典类型、

    字典类型: # msg_dic = {#     'apple': 10,#     'tesla': 100000,#     'mac': 3000,#     'lenovo': 30000,# ...

  3. python中字符串的几种表达方式(用什么方式表示字符串)

    说明: 今天在学习python的基础的内容,学习在python中如何操作字符串,在此记录下. 主要是python中字符串的几种表达,表示方式. python的几种表达方式 1 使用单引号扩起来字符串 ...

  4. python中字符串的四种表达方式

    今天在学习python的基础的内容,学习在python中如何操作字符串,在此记录下. 主要是python中字符串的几种表达,表示方式. python的几种表达方式 1 使用单引号扩起来字符串 > ...

  5. python中字典类型的使用

    Python字典类型 字典是一种键值对的集合,键值对之间无序 字典类型的定义 采用{}或者dict()来创建字典对象,键值对之间使用:进行分隔. {<键1>:<值1>, < ...

  6. Python字典及相关操作(内含例题)

    Python字典类型 今天将会介绍一种在python中十分常见的组合数据类型——字典 通过一些实例来理解字典中的常规操作 什么是字典类型? 列表中查找是通过整数的索引(元素在列表中的序号)来实现查找功 ...

  7. python字典中的元素类型

    python字典默认的是string item={"browser " : 'webdriver.irefox()', 'url' : 'http://xxx.com'} 如果这样 ...

  8. Python变量类型(l整型,长整形,浮点型,复数,列表,元组,字典)学习

    #coding=utf-8 __author__ = 'Administrator' #Python变量类型 #Python数字,python支持四种不同的数据类型 int整型 long长整型 flo ...

  9. python数据类型——字典类型

    字典(dictionary) python中唯一的映射类型,采用键值对(key-value)的形式储存数据,python对key进行哈希函数运算,所以key值必须是可哈希的,可哈希表示key必须是不可 ...

随机推荐

  1. Dwango Programming Contest V 翻车记

    A:签到. #include<iostream> #include<cstdio> #include<cmath> #include<cstdlib> ...

  2. SOA,ESB,WebService的关系

    1. 什么是SOA SOA(Service-Oriented Architecture)既服务导向架构,是指为了解决在inernet环境下业务集成的需要,通过连接能完成特定任务的独立功能实现的一种软件 ...

  3. Oracle 以某字段分组,以某字段排序,取前几条

    select * from (select row_number() over(partition by 以此字段为分组  order by 以此字段排序 desc rn from dual) whe ...

  4. BZOJ1058:[ZJOI2007]报表统计——题解

    https://www.lydsy.com/JudgeOnline/problem.php?id=1058 https://www.luogu.org/problemnew/show/P1110#su ...

  5. 洛谷 P3119 [USACO15JAN]草鉴定Grass Cownoisseur 解题报告

    P3119 [USACO15JAN]草鉴定Grass Cownoisseur 题目描述 约翰有\(n\)块草场,编号1到\(n\),这些草场由若干条单行道相连.奶牛贝西是美味牧草的鉴赏家,她想到达尽可 ...

  6. bzoj4822: [Cqoi2017]老C的任务(扫描线+BIT/线段树)

    裸题... 依旧是写了BIT和线段树两种(才不是写完线段树后才想起来可以写BIT呢 怎么卡常数都挺大...QAQ ccz和yy的写法好快哇%%% BIT: #include<iostream&g ...

  7. [学习笔记]分治FFT

    一般的分治FFT是指: https://www.luogu.org/problemnew/show/P4721 考虑后面的f和前面的f有关系,但是贡献可以分着计算,逐一累计上去. 考虑cdq分治.算出 ...

  8. Nginx反向代理两个tomcat服务器

    第一步,在Linux上安装两个tomcat,修改好端口号后,启动起来. 第二步,配置本地的DNS解析,即修改host文件: 第三步,配置Nginx配置文件 反向代理的配置虚拟主机配置差不多也要配置虚拟 ...

  9. STL使用总结

    转载于http://blog.csdn.net/daisy_chenting/article/details/6898184 1.    概述 泛型编程思想最早缘于A.Stepanov提出的部分算法可 ...

  10. DOM案例五星评分控件

    模仿网页上评分的五个五星. <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> < ...