Python:What the f×ck Python(上)

GitHub 上有一个名为《What the f*ck Python!》的项目,这个有趣的项目意在收集 Python 中那些难以理解和反人类直觉的例子以及鲜为人知的功能特性,并尝试讨论这些现象背后真正的原理!

原版地址:https://github.com/satwikkansal/wtfpython

最近,一位名为“暮晨”的贡献者将其翻译成了中文。

中文版地址:https://github.com/leisurelicht/wtfpython-cn

我就做回搬运工,将其搬运到公众号上,当中我也加了一些自己的理解。

1. Strings can be tricky sometimes

说明:

这些行为是由于 Cpython 在编译优化时,某些情况下会尝试使用已经存在的不可变对象而不是每次都创建一个新对象。(这种行为被称作字符串的驻留[string interning])。发生驻留之后, 许多变量可能指向内存中的相同字符串对象从而节省内存。

有一些方法可以用来猜测字符串是否会被驻留:

所有长度为 0 和长度为 1 的字符串都被驻留(①中字符串被驻留)

字符串在编译时被实现('wtf' 将被驻留,但是 ''.join(['w', 't', 'f'] 将不会被驻留)

字符串中只包含字母、数字或下划线时将会驻留,所以 'wtf!' 由于包含 '!' 而未被驻留

当在同一行将 a 和 b 的值设置为 "wtf!" 的时候,Python 解释器会创建一个新对象,然后两个变量同时指向这个对象。如果你在不同的行上进行赋值操作,它就不会“知道”已经有一个 wtf! 对象(因为 "wtf!" 不是按照上面提到的方式被隐式驻留的)。

常量折叠(constant folding)是 Python 中的一种窥孔优化(peephole optimization)技术。这意味着在编译时表达式 'a' * 20 会被替换为 'aaaaaaaaaaaaaaaaaaaa' 以减少运行时的时钟周期。只有长度小于 20 的字符串才会发生常量折叠。(为啥?想象一下由于表达式 'a' * 10 ** 10 而生成的 .pyc 文件的大小)。

如果你在 .py 文件中尝试这个例子,则不会看到相同的行为,因为文件是一次性编译的。

2. Time for some hash brownies!

说明:

Python 字典检查键值是否相等是通过比较哈希值是否相等来确定的。如果两个对象在比较的时候是相等的,那它们的散列值必须相等,否则散列表就不能正常运行了。例如,如果 1 == 1.0 为真,那么 hash(1) == hash(1.0) 必须也为真,但其实两个数字(整数和浮点数)的内部结构是完全不一样的。

3. Return return everywhere!

Output:

说明:

函数的返回值由最后执行的 return 语句决定。由于 finally 子句一定会执行,所以 finally 子句中的 return 将始终是最后执行的语句。

4. Deep down, we're all the same.

Output:

说明:

当调用 id 函数时,Python 创建了一个 WTF 类的对象并传给 id 函数,然后 id 函数获取其 id 值(也就是内存地址),然后丢弃该对象,该对象就被销毁了。

当我们连续两次进行这个操作时,Python会将相同的内存地址分配给第二个对象,因为在 CPython 中 id 函数使用对象的内存地址作为对象的id值,所以两个对象的id值是相同的。

综上,对象的 id 值仅仅在对象的生命周期内唯一,在对象被销毁之后或被创建之前,其他对象可以具有相同的id值。

Output:

正如你所看到的,对象销毁的顺序是造成所有不同之处的原因。

5. For what?

说明:

这一条仔细看一下很好理解,for 循环每次迭代都会给分配目标赋值,some_dict[i] = value 就相当于给字典添加键值对了。

有趣的是下面这个例子,你可曾觉得这个循环只会运行一次?

6. Evaluation time discrepancy

说明:

在生成器表达式中 in 子句在声明时执行,而条件子句则是在运行时执行。

①中,在运行前 array 已经被重新赋值为 [2, 8, 22],因此对于之前的 1, 8, 15,只有 count(8) 的结果是大于 0 ,所以生成器只会生成 8。

②中,g1 和 g2 的输出差异则是由于变量 array_1 和 array_2 被重新赋值的方式导致的。

在第一种情况下,array_1 被绑定到新对象 [1, 2, 3, 4, 5],因为 in 子句是在声明时被执行的,所以它仍然引用旧对象 [1, 2, 3, 4](并没有被销毁)。

在第二种情况下,对 array_2 的切片赋值将相同的旧对象 [1, 2, 3, 4] 原地更新为 [1, 2, 3, 4, 5]。因此 g2 和 array_2 仍然引用同一个对象[1, 2, 3, 4, 5]。

7. is is not what it is!

说明:

is 和 == 的区别

is 运算符检查两个运算对象是否引用自同一对象

== 运算符比较两个运算对象的值是否相等

因此 is 代表引用相同,== 代表值相等。下面的例子可以很好的说明这点:

256 是一个已经存在的对象,而 257 不是

当启动 Python 的时候,-5 到 256 的数值就已经被分配好了。这些数字因为经常使用所以适合被提前准备好。

当前的实现为 -5 到 256 之间的所有整数保留一个整数对象数组,当你创建了一个该范围内的整数时,你只需要返回现有对象的引用。所以改变 1 的值是有可能的。

但是,当 a 和 b 在同一行中使用相同的值初始化时,会指向同一个对象。

这是一种特别为交互式环境做的编译器优化,当你在实时解释器中输入两行的时候,他们会单独编译,因此也会单独进行优化, 如果你在 .py 文件中尝试这个例子,则不会看到相同的行为,因为文件是一次性编译的。

8. A tic-tac-toe where X wins in the first attempt!

说明:

我们来输出 id 看下:

row 是一个 list,其中三个元素都指向地址 5143216,当对 board[0][0] 进行赋值以后,row 的第一个元素指向 7536232。而 board 中的三个元素都指向 row,row 的地址并没有改变。

我们可以通过不使用变量 row 生成 board 来避免这种情况。

这里用了推导式,每次迭代都会生成一个新的 _ ,所以 board 中三个元素指向的是不同的变量。

9. The sticky output function

Output:

说明:

当在循环内部定义一个函数时,如果该函数在其主体中使用了循环变量,则闭包函数将与循环变量绑定,而不是它的值。因此,所有的函数都是使用最后分配给变量的值来进行计算的。

可以通过将循环变量作为命名变量传递给函数来获得预期的结果。为什么这样可行?因为这会在函数内再次定义一个局部变量。

Output:

10. is not … is not is (not …)/is not … 不是 is (not …)

说明:

is not 是个单独的二元运算符,与分别使用 is 和 not 不同。

11. The surprising comma

略过,我想没人会在函数的最后一个参数后面再加一个逗号吧!

况且,尾随逗号的问题已经在 Python 3.6 中被修复了。

12. Backslashes at the end of string

说明:

在以 r 开头的原始字符串中,反斜杠并没有特殊含义。解释器所做的只是简单的改变了反斜杠的行为,因此会直接传递反斜杠及后一个的字符。这就是反斜杠在原始字符串末尾不起作用的原因。

13. not knot!

说明:

一句话,== 运算符的优先级要高于 not 运算符。

14. Half triple-quoted strings

说明:

''' 和 """ 在 Python中也是字符串定界符,Python 解释器在先遇到三个引号的的时候会尝试再寻找三个终止引号作为定界符,如果不存在则会导致 SyntaxError 异常。

而 Python 提供隐式的字符串链接:

15. Midnight time doesn't exist?

Output:

midnight_time 并没有被输出。

说明:

在Python 3.5之前,如果 datetime.time 对象存储的UTC的午夜0点, 那么它的布尔值会被认为是 False。

这个我特意下了个 python 3.4 验证了下,真是这样。

16. What's wrong with booleans

Output:

说明:

布尔值是 int 的子类

在引入实际 bool 类型之前,0 和 1 是真值的官方表示。为了向下兼容,新的 bool 类型需要像 0 和 1 一样工作。

17. Class attributes and instance attributes

Output:

Output:

说明:

类变量和实例变量在内部是通过类对象的字典来处理(__dict__ 属性),如果在当前类的字典中找不到的话就去它的父类中寻找。

+= 运算符会在原地修改可变对象,而不是创建新对象。因此,修改一个实例的属性会影响其他实例和类属性。

18. yielding None

Output:

说明:

这是CPython在理解和生成器表达式中处理yield的一个错误,在Python 3.8中修复,在Python 3.7中有弃用警告。 请参阅Python错误报告和Python 3.7和Python 3.8的新增条目。

来源和解释可以在这里找到: https://stackoverflow.com/questions/32139885/yield-in-list-comprehensions-and-generator-expressions

相关错误报告: http://bugs.python.org/issue10544

19. Mutating the immutable!

说明:

元组中不可变的元素的标识(即元素的地址),如果元素是引用类型,元组的值会随着引用的可变对象的变化而变化。所以 another_tuple[2].append(1000) 是可以的。

+= 操作符在原地修改了列表。元素赋值操作并不工作,但是当异常抛出时,元素已经在原地被修改了。+= 并不是原子操作,而是 extend 和 = 两个动作,这里 = 操作虽然会抛出异常,但 extend 操作已经修改成功了。

20. The disappearing variable from outer scope

Output: python2

Output: python3

说明:

当使用 as 为目标分配异常的时候,将在 except 子句的末尾清除该异常。

这就好像:

会被翻译成:

这意味着必须将异常分配给其他名称才能在 except 子句之后引用它。而异常之所以会被清除,是因为附加了回溯信息(trackback),它们与栈帧(stack frame)形成一个引用循环,使得该栈帧中的所有本地变量在下一次垃圾回收发生之前都处于活动状态(不会被回收)。

子句在 Python 中并没有独立的作用域。示例中的所有内容都处于同一作用域内,所以变量 e 会由于执行了 except 子句而被删除。而对于有独立的内部作用域的函数来说情况就不一样了。下面的例子说明了这一点:

Output:

21. When True is actually False

Output:

说明:

最初,Python 并没有 bool 型(人们用 0 表示假值, 用非零值比如 1 作为真值)。后来他们添加了 True, False, 和 bool 型,但是,为了向后兼容,他们没法把 True 和 False 设置为常量,只是设置成了内置变量。

Python 3 由于不再需要向后兼容,终于可以修复这个问题了,所以这个例子无法在 Python 3.x 中执行。

22. From filled to None in one instruction…

Output:

说明:

大多数修改序列/映射对象的方法,比如 list.append,dict.update,list.sort 等等,都是原地修改对象并返回 None,这样可以避免创建对象的副本来提高性能。

23. Subclass relationships

子类关系应该是可传递的,对吧?即,如果 A 是 B 的子类,B 是 C 的子类,那么 A 应该 是 C 的子类。

说明:

Python 中的子类关系并不必须是传递的,任何人都可以在元类中随意定义__subclasscheck__。

当 issubclass(cls, Hashable) 被调用时,它只是在 cls 中寻找 "__hash__" 方法或继承自 "__hash__" 的方法。

由于 object 是可散列的(hashable),而 list 是不可散列的,所以它打破了这种传递关系。

24. The mysterious key type conversion

Output:

说明:

由于 SomeClass 会从 str 自动继承 __hash__ 方法,所以 s 对象和 "s" 字符串的哈希值是相同的。

而 SomeClass("s") == "s" 为 True 是因为 SomeClass 也继承了 str 类 __eq__ 方法。

由于两者的哈希值相同且相等,所以它们在字典中表示相同的键。

如果想要实现期望的功能, 我们可以重定义 SomeClass 的 __eq__ 方法.

Output:

25. Let's see if you can guess this?

说明:

根据 Python 语言参考,赋值语句的形式如下:

赋值语句计算表达式列表(expression list)(请记住,这可以是单个表达式或以逗号分隔的列表, 后者返回元组)并将单个结果对象从左到右分配给目标列表中的每一项。

(target_list "=")+ 中的 + 意味着可以有一个或多个目标列表。在这个例子中,目标列表是 a, b 和 a[b]。表达式列表只能有一个,是 {}, 5。

这话看着非常的晦涩,我们来看一个简单的例子:

Output:

在这个简单的例子中,目标列表是 a, b 和 b, c,表达式是 1, 2。将表达式从左到右赋给目标列表,上述例子就可以拆分成:

所以结果就是 1 1 2。

那么,原例子就不难理解了,拆解开来就是:

这里不能写作 a[b] = {}, 5,因为这样第一句中的 {} 和第二句中的 {} 其实就是不同的对象了,而实际他们是同一个对象。这就形成了循环引用,输出中的 {…} 指与 a 引用了相同的对象。

我们来验证一下:

可见确实是同一个对象。

以下是一个简单的循环引用的例子:

  • 发表于:
  • 原文链接:https://kuaibao.qq.com/s/20181208G02I2Z00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券