一个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. GSL介绍【转】

    GSL(GNU Scientific Library)是一个C写成的用于科学计算的库,下面是一些相关的包 http://www.thebigdata.cn/JiShuBoKe/5612.html

  2. Codeforces 498B Name That Tune

    不想说啥了…这是我被卡常数卡得最惨的一次… 首先根据期望的线性性,我们考虑每首歌能够被认出来的概率,也就是每首歌对答案贡献的期望.那么定义F[i]为第i首歌被认出来的概率是做不了的,自然想到F[i][ ...

  3. BZOJ5343 & 洛谷4602 & LOJ2555:[CTSC2018]混合果汁——题解

    https://www.luogu.org/problemnew/show/P4602 https://loj.ac/problem/2555 https://www.lydsy.com/JudgeO ...

  4. BZOJ3675 [Apio2014]序列分割 【斜率优化dp】

    3675: [Apio2014]序列分割 Time Limit: 40 Sec  Memory Limit: 128 MB Submit: 3366  Solved: 1355 [Submit][St ...

  5. Java第一次实验报告——Java开发环境的熟悉

    北京电子科技学院(BESTI) 实    验    报    告 课程名称:java程序设计实验      班级:1352         姓名:洪韶武      学号:20135219 成绩:   ...

  6. 剑桥offer(21~30)

    21.题目描述 输入一个字符串,按字典序打印出该字符串中字符的所有排列.例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba.   还不 ...

  7. Javascript中的date对象和getTime()方法

    有些时候我们需要计算两个日期间的天数,或者小时数等等.下面用JavaScript实现这个需求,然后学习一下需要用到的一些JavaScript函数. JavaScript程序如下: 1 <scri ...

  8. angularJS 条件查询 品优购条件查询品牌(条件查询和列表展示公用方法解决思路 及 post请求混合参数提交方式)

    Brand.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> &l ...

  9. sudoers文件配置

    http://note.drx.tw/2008/01/linuxsudo.html foobar ALL=(ALL) ALL 現在讓我們來看一下那三個 ALL 到底是什麼意思.第一個 ALL 是指網路 ...

  10. 洛谷 P1044 栈

    题目背景 栈是计算机中经典的数据结构,简单的说,栈就是限制在一端进行插入删除操作的线性表. 栈有两种最重要的操作,即pop(从栈顶弹出一个元素)和push(将一个元素进栈). 栈的重要性不言自明,任何 ...