Hacker News 中文摘要

RSS订阅

Python 搞什么鬼 -- What the Fuck Python

文章摘要

Google Colab 是一个基于云端的Python编程环境,支持实时协作和代码共享。本文通过一系列令人惊讶的Python代码片段,深入探讨了Python语言中一些反直觉和鲜为人知的特性和内部机制。这些示例不仅帮助程序员更好地理解Python的工作原理,也为有经验的Python开发者提供了挑战和回忆的机会。

文章总结

标题:Google Colab

内容来源:Google Colab

Markdown 内容:

wtf.ipynb
文件 | 编辑 | 视图 | 插入 | 运行时 | 工具 | 帮助 | 设置 | 链接 | 分享 | 登录 | 命令 | 代码 | 文本 | 复制到云端硬盘 | 展开 | 项目符号列表 | 查找 | 代码 | 密钥 | 文件夹 | 下拉箭头

What the f*ck Python! 😱
通过令人惊讶的代码片段探索和理解 Python。

翻译: 中文 | 越南语 | 添加翻译
其他模式: 交互式

Python 作为一种设计精美的高级解释型编程语言,为程序员提供了许多便利的特性。但有时,某些 Python 代码片段的结果可能并不直观。

这是一个有趣的项目,试图解释一些反直觉的代码片段和 Python 中鲜为人知的特性背后的原理。

虽然下面的一些例子可能并不完全是“WTF”,但它们会揭示一些你可能不知道的 Python 有趣之处。我认为这是学习编程语言内部机制的好方法,我相信你也会觉得它很有趣!

如果你是一位有经验的 Python 程序员,可以将其视为一个挑战,尝试在第一次尝试时就能正确理解大多数例子。你可能已经遇到过其中的一些例子,我可能会唤起你一些美好的回忆!😅

PS:如果你是一位回头客,可以在这里了解新的修改(标记有星号的例子是在最新主要修订中添加的)。

那么,我们开始吧...

示例结构

所有示例的结构如下:

▶ 一些花哨的标题
```python

设置代码

为魔法做准备...

```

输出(Python 版本):
```python

触发语句 一些意外的输出 ```

(可选): 一行描述意外输出的内容。

💡 解释:
简要解释发生了什么以及为什么会发生。

```python

设置代码

更多示例以进一步澄清(如有必要)

```

输出(Python 版本):
```python

触发 # 一些揭示魔法的示例

一些合理的输出

```

注意: 所有示例均在 Python 3.5.2 交互式解释器上测试,除非在输出前明确指定,否则它们应适用于所有 Python 版本。

使用方法

我认为充分利用这些示例的好方法是按顺序阅读它们,并对每个示例执行以下操作:

  1. 仔细阅读设置示例的初始代码。如果你是一位有经验的 Python 程序员,大多数时候你都能成功预测接下来会发生什么。
  2. 阅读输出片段,并检查输出是否与你的预期一致。
  3. 确保你知道输出为何如此的确切原因。
  4. 如果答案是否定的(这完全没问题),深呼吸,阅读解释(如果仍然不理解,请大声喊出来!并在这里创建一个问题)。
  5. 如果答案是肯定的,轻轻拍拍自己的背,然后可以跳到下一个示例。

托管笔记本说明

这只是通过 Jupyter 笔记本浏览 wtfpython 的实验性尝试。由于以下原因,某些示例是只读的:

  • 它们需要 Python 版本不受托管运行时支持。
  • 或者它们无法在笔记本环境中重现。

预期的输出已经存在于代码单元格后面的折叠单元格中。Google Colab 提供了 Python 2(2.7)和 Python 3(3.6,默认)运行时。你可以在这两者之间切换以运行特定于 Python 2 的示例。对于特定于其他次要版本的示例,你可以简单地参考折叠的输出(目前无法在托管笔记本中控制次要版本)。你可以使用以下命令检查活动版本:

```python

import sys sys.version

打印出 Python 版本

```

尽管如此,大多数示例都能按预期工作。如果你遇到任何问题,请随时参考 wtfpython 的原始内容并在仓库中创建问题。玩得开心!

下拉箭头

▶ 字符串有时会很棘手

  1. python a = "some_string" id(a)

显示隐藏输出
python id("some" + "_" + "string") # 注意两者的 id 相同

显示隐藏输出

  1. python a = "wtf" b = "wtf" a is b

显示隐藏输出
python a = "wtf!" b = "wtf!" a is b

显示隐藏输出

  1. python a, b = "wtf!", "wtf!" a is b # 除 3.7.x 外的所有版本

显示隐藏输出
python a = "wtf!"; b = "wtf!" a is b # 这将根据你调用的位置(Python shell / IPython / 作为脚本)打印 True 或 False

显示隐藏输出

双击(或回车)编辑

```python

这次在文件 some_file.py 中

a = "wtf!" b = "wtf!" print(a is b)

当模块被调用时打印 True!

```

显示隐藏输出

  1. 输出(< Python3.7)
    python 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'

显示隐藏输出
python 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'

显示隐藏输出

这有道理,对吧?

💡 解释:
第一个和第二个片段的行为是由于 CPython 的优化(称为字符串驻留),它试图在某些情况下使用现有的不可变对象,而不是每次都创建新对象。
在“驻留”之后,许多变量可能会引用内存中的同一个字符串对象(从而节省内存)。
在上面的片段中,字符串被隐式驻留。何时隐式驻留字符串的决定取决于实现。有一些规则可以用来猜测字符串是否会被驻留:
- 所有长度为 0 和长度为 1 的字符串都会被驻留。
- 字符串在编译时被驻留('wtf' 会被驻留,但 ''.join(['w', 't', 'f']) 不会被驻留)。
- 由非 ASCII 字母、数字或下划线组成的字符串不会被驻留。这解释了为什么 'wtf!' 由于 ! 而没有被驻留。CPython 实现此规则的代码可以在这里找到。
当 a 和 b 在同一行设置为 "wtf!" 时,Python 解释器会创建一个新对象,然后同时引用第二个变量。如果你在不同的行执行此操作,它不会“知道”已经有一个 "wtf!" 对象(因为根据上述事实,"wtf!" 没有被隐式驻留)。这是一个编译时优化。此优化不适用于 CPython 的 3.7.x 版本(请查看此问题以获取更多讨论)。
在交互式环境(如 IPython)中,编译单元由单个语句组成,而在模块中,编译单元由整个模块组成。a, b = "wtf!", "wtf!" 是单个语句,而 a = "wtf!"; b = "wtf!" 是同一行中的两个语句。这解释了为什么在 a = "wtf!"; b = "wtf!" 中标识不同,也解释了为什么在 some_file.py 中调用时它们相同。
第四个片段输出的突然变化是由于一种称为常量折叠的窥孔优化技术。这意味着表达式 'a'20 在编译时被替换为 'aaaaaaaaaaaaaaaaaaaa',以在运行时节省几个时钟周期。常量折叠仅适用于长度小于 21 的字符串。(为什么?想象一下表达式 'a'1010 生成的 .pyc 文件的大小)。这里是相同的实现源代码。
**注意:
在 Python 3.7 中,常量折叠从窥孔优化器移到了新的 AST 优化器,逻辑也有所改变,因此第四个片段在 Python 3.7 中不起作用。你可以在这里阅读有关此更改的更多信息。

下拉箭头

▶ 小心链式操作
python (False == False) in [False] # 有道理

显示隐藏输出
python False == (False in [False]) # 有道理

显示隐藏输出
python False == False in [False] # 现在呢?

显示隐藏输出
python True is False == False

显示隐藏输出
python False is False is False

显示隐藏输出
python 1 > 0 < 1

显示隐藏输出
python (1 > 0) < 1

显示隐藏输出
python 1 > (0 < 1)

显示隐藏输出

双击(或回车)编辑

下拉箭头

💡 解释:
根据 Python 文档,如果 a, b, c, ..., y, z 是表达式,op1, op2, ..., opN 是比较运算符,则 a op1 b op2 c ... y opN z 等价于 a op1 b and b op2 c and ... y opN z,除了每个表达式最多只计算一次。
虽然在上面的示例中这种行为可能看起来很愚蠢,但在像 a == b == c 和 0 <= x <= 100 这样的情况下,它非常棒。
False is False is False 等价于 (False is False) and (False is False)
True is False == False 等价于 True is False and False == False,由于语句的第一部分 (True is False) 计算结果为 False,整个表达式计算结果为 False。
1 > 0 < 1 等价于 1 > 0 and 0 < 1,计算结果为 True。
表达式 (1 > 0) < 1 等价于 True < 1 和
python int(True)

显示隐藏输出
python True + 1 # 与此示例无关,只是为了好玩

显示隐藏输出

所以,1 < 1 计算结果为 False

下拉箭头

▶ 如何不使用 is 运算符

以下是一个非常著名的例子,遍布互联网。

  1. python a = 256 b = 256 a is b

显示隐藏输出
python a = 257 b = 257 a is b

显示隐藏输出

  1. python a = [] b = [] a is b

显示隐藏输出
python a = tuple() b = tuple() a is b

显示隐藏输出

  1. 输出
    python a, b = 257, 257 a is b

显示隐藏输出

输出(Python 3.7.x 特别)
python a, b = 257, 257

显示隐藏输出

双击(或回车)编辑

下拉箭头

💡 解释:
is 和 == 的区别
is 运算符检查两个操作数是否引用同一个对象(即,它检查操作数的标识是否匹配)。
== 运算符比较两个操作数的值并检查它们是否相同。
所以 is 用于引用相等,而 == 用于值相等。一个例子可以澄清这一点,
python class A: pass A() is A() # 这是两个不同内存位置的两个空对象

显示隐藏输出

256 是一个现有对象,但 257 不是
当你启动 Python 时,数字从 -5 到 256 会被分配。这些数字被大量使用,所以准备好它们是有意义的。
引用自 Python 文档
当前实现为所有介于 -5 和 256 之间的整数保留一个整数对象数组,当你创建一个该范围内的整数时,你只是获得对现有对象的引用。所以应该可以改变 1 的值。我怀疑 Python 在这种情况下行为是未定义的。😄

python id(256)

显示隐藏输出
python a = 256 b = 256 id(a)

显示隐藏输出
python id(b)

显示隐藏输出
python id(257)

显示隐藏输出
python x = 257 y = 257 id(x)

显示隐藏输出
python id(y)

显示隐藏输出

在这里,解释器在执行 y = 257 时不够聪明,无法识别我们已经创建了一个值为 257 的整数,因此它继续在内存中创建另一个对象。
类似的优化也适用于其他不可变对象,如空元组。由于列表是可变的,所以 [] is [] 将返回 False,而 () is () 将返回 True。这解释了我们的第二个片段。让我们继续看第三个,
当 a 和 b 在同一行用相同的值初始化时,它们引用同一个对象。
输出
python a, b = 257, 257 id(a)

显示隐藏输出
python id(b)

显示隐藏输出
python a = 257 b = 257 id(a)

显示隐藏输出
python id(b)

显示隐藏输出

当 a 和 b 在同一行设置为 257 时,Python 解释器会创建一个新对象,然后同时引用第二个变量。如果你在不同的行执行此操作,它不会“知道”已经有一个 257 对象。
这是一个编译器优化,特别适用于交互式环境。当你在实时解释器中输入两行时,它们会分别编译,因此分别优化。如果你在 .py 文件中尝试此示例,你不会看到相同的行为,因为文件是一次性编译的。此优化不仅限于整数,它也适用于其他不可变数据类型,如字符串(请查看“字符串很棘手”示例)和浮点数,
python a, b = 257.0, 257.0 a is b

显示隐藏输出
为什么这在 Python 3.7 中不起作用? 抽象原因是这种编译器优化是特定于实现的(即可能随版本、操作系统等变化)。我仍在弄清楚确切的实现更改导致了问题,你可以查看此问题以获取更新。

下拉箭头

▶ 哈希布朗尼

  1. python some_dict = {} some_dict[5.5] = "JavaScript" some_dict[5.0] = "Ruby" some_dict[5] = "Python"

显示隐藏输出

输出:
python some_dict[5.5]

显示隐藏输出
python some_dict[5.0] # "Python" 摧毁了 "Ruby" 的存在?

显示隐藏输出
python some_dict[5]

显示隐藏输出
python complex_five = 5 + 0j type(complex_five)

显示隐藏输出
python some_dict[complex_five]

显示隐藏输出

所以,为什么 Python 到处都是?

下拉箭头

💡 解释
Python 字典中键的唯一性是通过等价性而不是标识来确定的。因此,即使 5、5.0 和 5 + 0j 是不同类型的不同对象,由于它们相等,它们不能同时存在于同一个字典(或集合)中。一旦你插入其中任何一个,尝试查找任何不同但等价的键都会成功,并返回原始映射值(而不是失败并抛出 KeyError):
python 5 == 5.0 == 5 + 0j

显示隐藏输出
python 5 is not 5.0 is not 5 + 0j

显示隐藏输出
python some_dict = {} some_dict[5.0] = "Ruby" 5.0 in some_dict

显示隐藏输出
python (5 in some_dict) and (5 + 0j in some_dict)

显示隐藏输出
这也适用于设置项。因此,当你执行 some_dict[5] = "Python" 时,Python 会找到具有等价键 5.0 -> "Ruby" 的现有项,就地覆盖其值,并保留原始键不变。
python some_dict

显示隐藏输出
python some_dict[5] = "Python" some_dict

显示隐藏输出

那么我们如何将键更新为 5(而不是 5.0)? 我们实际上无法就地更新,但我们可以先删除键(del somedict[5.0]),然后设置它(somedict[5])以将整数 5 作为键而不是浮点数 5.0,尽管这在极少数情况下才需要。

Python 是如何在包含 5.0 的字典中找到 5 的? Python 通过使用哈希函数在恒定时间内完成此操作,而无需扫描每个项。当 Python 在字典中查找键 foo 时,它首先计算 hash(foo)(在恒定时间内运行)。由于在 Python 中要求比较相等的对象也具有相同的哈希值(文档在这里),5、5.0 和 5 + 0j 具有相同的哈希值。
python 5 == 5.0 == 5 + 0j

显示隐藏输出
python hash(5) == hash(5.0) == hash(5 + 0j)

显示隐藏输出

注意: 反之则不一定成立:具有相同哈希值的对象本身可能不相等。(这会导致所谓的哈希冲突,并降低哈希通常提供的恒定时间性能。)

下拉箭头

▶ 深入来看,我们都一样。
python class WTF: pass

显示隐藏输出

输出:
python WTF() == WTF() # 两个不同的实例不能相等

显示隐藏输出
python WTF() is WTF() # 标识也不同

显示隐藏输出
python hash(WTF()) == hash(WTF()) # 哈希值_应该_也不同

显示隐藏输出
python id(WTF()) == id(WTF())

显示隐藏输出

双击(或回车)编辑

下拉箭头

💡 解释:
当调用 id 时,Python 创建了一个 WTF 类对象并将其传递给 id 函数。id 函数获取其 id(其内存位置),然后丢弃该对象。该对象被销毁。
当我们连续执行两次时,Python 也会为第二个对象分配相同的内存位置。由于(在 CPython 中)id 使用内存位置作为对象 id,因此两个对象的 id 相同。
因此,对象的 id 仅在对象的生命周期内是唯一的。在对象被销毁后,或在对象创建之前,其他东西可以具有相同的 id。
但为什么 is 运算符计算结果为 False?让我们通过这个

评论总结

评论主要围绕Python语言的设计和使用展开,观点分为支持和批评两派。

支持Python的观点: 1. Python设计合理,行为可预测:多位评论者认为Python的设计避免了意外行为,与JavaScript相比更为稳定。例如,ltbarcly3指出:“Python几乎总是按预期行事,很少遇到奇怪的意外行为。” (Python almost always behaves as expected. If you think there's an equivalence here you are just a JS fanboy.) 2. 批评者误解了Python的规范:一些评论者认为,所谓的“WTF”现象源于对Python规范的不理解或误用。LPisGood表示:“这些现象似乎是因为使用Python超出了规范,并对实现细节感到惊讶。” (Some of these just seem to be using Python out of spec and being surprised that implementation details exist.)

批评Python的观点: 1. Python存在一些设计上的奇怪之处:部分评论者认为Python的某些设计选择(如is操作符)容易引发混淆。hansvm提到:“is和相关的操作符有专门的简短语法,这有点奇怪。” (It's a little weird that is and friends have dedicated, short, nice syntax.) 2. Python的某些行为可能令人困惑:一些评论者指出,Python的某些行为(如整数对象的复用)可能会让初学者感到困惑。g42gregory表示:“对于足够小的整数,解释器可能会选择复用对象,但对于较大的整数,则会创建不同的对象。” (Interpreter may choose to recycle objects or it may not. In case of integers, low enough number in different parts of the program, will be represented by the same object.)

其他观点: 1. 评论者对“WTF”现象的态度:部分评论者认为这些“WTF”现象只是对Python语言特性的误解,而非真正的设计缺陷。happytoexplain总结道:“许多评论归结为‘如果你理解语言,这些就不是WTF。’” (These aren't WTFs if you understand the language.) 2. 对Python学习资源的讨论:一些评论者提到Python的学习资源(如GitHub上的“WTF Python”项目)可以帮助理解语言的底层行为。harrisi认为:“理解代码的概念与实际执行之间的差异是有趣且重要的。” (Understanding the difference between the conceptual idea of each line of code and what actually happens is fun, and, in some cases, important.)

总体而言,评论者对Python的设计和使用有不同的看法,但多数认为其行为可预测,且所谓的“WTF”现象多源于对语言规范的不理解。