《纯Python。 专业人士编程的精妙之处»

图片 嗨,habrozhiteli! 学习Python的所有可能性是一项艰巨的任务,而通过这本书,您可以专注于真正重要的实践技能。 在Python标准库中挖出隐藏的金子,然后立即开始编写干净的代码。

如果您有使用旧版Python的经验,则可以使用Python 3中引入的现代模板和函数来加快工作速度。

如果您使用过其他编程语言,并且想切换到Python,那么您会发现成为有效的pythonist所需的实用技巧。
如果您想学习如何编写简洁的代码,将在这里找到最有趣的示例和鲜为人知的技巧。

摘录“西方最疯狂的字典表达方式”


有时,您会遇到一个很小的代码示例,其深度确实出乎意料之外—如果仔细考虑一下,一行代码可以教给您很多知识。 这样的一段代码就像禅宗佛教中的一句古训:禅宗实践中用来引发疑问和考验学生成绩的问题或陈述。

我们在本节中讨论的一小段代码就是这样的示例。 乍一看,它看起来像是一个简单的词汇表达,但是仔细检查后,它就会带您使用Python解释器进行令人难以置信的迷幻之旅。

从这个单线开始,我得到的嗡嗡声使我什至在将它打印在Python会议参与者徽章上作为交谈的机会。 这导致与我的Python电子邮件列表的成员进行了一些建设性的对话。
因此,事不宜迟,这是这段代码。 休息一下,思考下面的词汇表达及其计算结果:

>>> {True: '', 1: '', 1.0: ''} 

我在这里等...

好,准备好了吗?

以下是在Python解释器会话中评估上述字典表达式时得到的结果:

 >>> {True: '', 1: '', 1.0: ''} {True: ''} 

我承认,当我第一次看到这个结果时,我非常傻眼。 但是,当您悠闲地逐步研究这里发生的事情时,所有事情都将落到位。 我必须说,让我们考虑一下为什么得到这个结果不是很直观的结果。

当Python处理我们的字典表达式时,它首先构建一个新的空字典对象,然后按照将键和值传递给字典表达式的顺序为其分配键和值。

然后,当我们将其分解为几部分时,我们的字典表达式将等效于按顺序执行的以下指令序列:

 >>> xs = dict() >>> xs[True] = '' >>> xs[1] = '' >>> xs[1.0] = '' 

奇怪的是,Python认为此字典示例中使用的所有键都是等效的:

 >>> True == 1 == 1.0 True 

好的,请稍等。 我相信您可以直观地接受1.0 == 1,但是为什么True被认为也等于1? 我第一次看到这个字典表达时,确实感到困惑。

在Python文档中进行了一些反复的研究,我发现Python将bool类型视为int类型的子类。 在Python 2和Python 3中就是这种情况:

布尔类型是整数类型的子类型,并且布尔值在几乎所有上下文中分别表现为值0和1,不同之处在于,当转换为字符串类型时,分别返回字符串值False或True。 '。

当然,这意味着在Python中,布尔值在技术上可以用作列表或元组索引:

 >>> ['', ''][True] '' 

但是您可能不应该以清晰(和同事的心理健康)为名使用这种逻辑变量。

一种或另一种方式,回到我们的字典表达式。

至于Python语言,所有这些值(True,1和1.0)代表相同的字典键。 解释器评估字典表达式时,会反复覆盖True键的值。 这解释了为什么最终的字典只包含一个键。

在继续之前,请再看一下原始的字典表达式:

 >>> {True: '', 1: '', 1.0: ''} {True: ''} 

为什么在这里我们仍将True作为关键? 由于最后反复分配,键是否也不应更改为1.0?

在对Python解释器的源代码进行了一些研究之后,我发现当一个新值与一个键对象相关联时,Python词典本身不会更新该键对象:

 >>> ys = {1.0: ''} >>> ys[True] = '' >>> ys {1.0: ''} 

当然,这对于性能优化是有意义的:如果密钥被认为相同,那么为什么要浪费时间更新原始密钥呢?
在上一个示例中,您看到从未替换原始True对象作为键。 因此,字典的字符串表示形式仍将键显示为True(而不是1或1.0)。

现在我们已经知道,显然,重写字典中的值只是因为比较将始终显示它们彼此相等。 但是,事实证明,这种效果也不是通过__eq__方法进行的等效性测试的结果。

Python字典依赖于哈希表数据结构。 当我第一次看到这个惊人的字典表达时,我首先想到的是这种行为某种程度上与哈希冲突有关。

事实是,内部表示中的哈希表根据每个键的哈希值将可用的键存储在各种“篮子”中。 哈希值从密钥派生为固定长度的数字值,用于唯一标识密钥。

这一事实使您可以执行快速搜索操作。 在查找表中查找键哈希值比将完整键对象与所有其他键进行比较并进行等效检查要快得多。

但是,用于计算哈希值的方法通常并不理想。 最终,实际上不同的两个或多个键将具有相同的派生哈希值,并且最终将出现在相同的搜索表篮子中。
当两个键具有相同的哈希值时,这种情况称为哈希冲突,这是一种特殊情况,应处理在哈希表中插入和查找元素的算法。

根据此评估,哈希很可能与我们从字典表达式中获得的意外结果相关。 因此,让我们在这里查找键哈希值是否也起一定作用。
我将下面的类定义为一个小的侦探工具:

 class AlwaysEquals: def __eq__(self, other): return True def __hash__(self): return id(self) 

该类的特点有两个方面。

首先,由于__eq__ Dunder方法始终返回True,因此该类的所有实例都假装等效于任何对象:

 >>> AlwaysEquals() == AlwaysEquals() True >>> AlwaysEquals() == 42 True >>> AlwaysEquals() == '?' True 

其次,AlwaysEquals的每个实例还将返回由内置函数id()生成的唯一哈希值:

 >>> objects = [AlwaysEquals(), AlwaysEquals(), AlwaysEquals()] >>> [hash(obj) for obj in objects] [4574298968, 4574287912, 4574287072] 

在Python中,id()函数返回RAM中对象的地址,该地址被保证是唯一的。

使用此类,您现在可以创建假装与任何其他对象均等的对象,但同时具有与之关联的唯一哈希值。 这将允许您仅根据它们对等的比较结果来检查字典的键是否被重写。

而且,如您所见,尽管比较将始终将它们显示为彼此等效,但以下示例中的键并不对应:

 >>> {AlwaysEquals(): '', AlwaysEquals(): ''} { <AlwaysEquals object at 0x110a3c588>: '', <AlwaysEquals object at 0x110a3cf98>: '' } 

我们还可以从另一侧看一下这个想法,并检查是否返回相同的哈希值是否足以迫使键被重写:

 class SameHash: def __hash__(self): return 1 

比较SameHash类的实例将显示它们彼此不相等,但是它们的哈希值均为1:

 >>> a = SameHash() >>> b = SameHash() >>> a == b False >>> hash(a), hash(b) (1, 1) 

让我们看看当我们尝试将SameHash类实例用作字典键时Python字典如何响应:

 >>> {a: 'a', b: 'b'} { <SameHash instance at 0x7f7159020cb0>: 'a', <SameHash instance at 0x7f7159020cf8>: 'b' } 

如该示例所示,“键被覆盖”的影响不仅是由于哈希值冲突引起的。

字典执行等效检查并比较哈希值以确定两个键是否相同。 让我们尝试总结一下我们的研究结果。

字典表达式{True:'yes',1:'no',1.0:'maybe'}的计算公式为{True:'possible'},因为比较此示例的所有键True,1和1.0将显示它们彼此等效,并且它们都具有相同的哈希值:

 >>> True == 1 == 1.0 True >>> (hash(True), hash(1), hash(1.0)) (1, 1, 1) 

也许现在得到字典最终状态这样的结果并不奇怪:

 >>> {True: '', 1: '', 1.0: ''} {True: ''} 

在这里,我们涵盖了很多主题,而这个特殊的Python技巧一开始可能并不适合您-这就是为什么在本节开始时我将它与Zen koan进行比较的原因。

如果您很难理解本节的内容,请尝试依次在Python解释器会话中尝试所有代码示例。 您将通过扩展对Python语言内部机制的了解而获得回报。

»这本书的更多信息可以在出版商的网站上找到
» 目录
» 摘录

小贩优惠券20%优惠-Python

Source: https://habr.com/ru/post/zh-CN418761/


All Articles