Python:What the f×ck Python(下)

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

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

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

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

上一篇 Python:What the f*ck Python(上)

原本每个的标题都是原版中的英文,有些取名比较奇怪,不直观,我换成了可以描述主题的中文形式,有些是自己想的,不足之处请指正。另外一些 Python 中的彩蛋被我去掉了。

26. 非英文字符

什么鬼?

将代码复制到 pycharm 里看一下就明白了。

有些一些非西方字符虽然看起来和英语字母相同,但会被解释器识别为不同的字母。我们基本不会用到。

27. 空间移动

Output:

说明:

energy_send 函数中创建的 numpy 数组并没有返回,因此内存空间被释放并可以被重新分配。

numpy.empty() 直接返回下一段空闲内存,而不重新初始化。而这个内存点恰好就是刚刚释放的那个(通常情况下,并不绝对)。

28. 不要混用制表符(tab)和空格(space)

tab 是8个空格,而用空格表示则一个缩进是4个空格,混用就会出错。python3 里直接不允许这种行为了,会报错:

TabError: inconsistent use of tabs and spaces in indentation

很多编辑器,例如 pycharm,可以直接设置 tab 表示 4 个空格。

29. 迭代字典时的修改

Output(Python 2.7- Python 3.5):

说明:

Python不支持对字典进行迭代的同时修改它,它之所以运行 8 次,是因为字典会自动扩容以容纳更多键值(译: 应该是因为字典的初始最小值是8, 扩容会导致散列表地址发生变化而中断循环)。

在不同的Python实现中删除键的处理方式以及调整大小的时间可能会有所不同,python3.6开始,到5就会扩容。

而在 list 中,这种情况是允许的,list 和 dict 的实现方式是不一样的,list 虽然也有扩容,但 list 的扩容是整体搬迁,并且顺序不变。

这个代码可以一直运行下去直到 int 越界。但一般不建议在迭代的同时修改 list。

30. __del__

Output:

说明:

del x 并不会立刻调用,每当遇到,Python 会将 x 的引用数减 1,当 x 的引用数减到 0 时就会调用。

我们再加一点变化:

之所以未被调用,是因为前一条语句(>>> y)对同一对象创建了另一个引用,从而防止在执行后对象的引用数变为 0。(这其实是 Python 交互解释器的特性,它会自动让保存上一个表达式输出的值。)

调用导致引用被销毁,因此我们可以看到 "Deleted!" 终于被输出了。

31. 迭代列表时删除元素

在 29 中,我附加了一个迭代列表时添加元素的例子,现在来看看迭代列表时删除元素。

Output:

说明:

在迭代时修改对象是一个很愚蠢的主意,正确的做法是迭代对象的副本,就是这么做的。

del、remove、pop 的不同:

del var_name 只是从本地或全局命名空间中删除了 var_name(这就是为什么 list_1 没有受到影响)。

remove 会删除第一个匹配到的指定值,而不是特定的索引,如果找不到值则抛出 ValueError 异常。

pop 则会删除指定索引处的元素并返回它,如果指定了无效的索引则抛出 IndexError 异常。

为什么输出是 [2, 4]?

列表迭代是按索引进行的,所以当我们从或中删除 1 时,列表的内容就变成了。剩余元素会依次位移,也就是说,的索引会变为 0,会变为 1。由于下一次迭代将获取索引为 1 的元素(即), 因此将被彻底的跳过。类似的情况会交替发生在列表中的每个元素上。

32. 循环变量泄漏!

Output:

Output:

Output:

Output:

说明:

在 Python 中,for 循环使用所在作用域并在结束后保留定义的循环变量。如果我们曾在全局命名空间中定义过循环变量,它会重新绑定现有变量。

Python 2.x 和 Python 3.x 解释器在列表推导式示例中的输出差异,在文档 What’s New In Python 3.0 中可以找到相关的解释:

"列表推导不再支持句法形式。使用代替。另外注意,列表推导具有不同的语义:它们更接近于构造函数中生成器表达式的语法糖,特别是循环控制变量不再泄漏到周围的作用域中。"

简单来说,就是 python2 中,列表推导式依然存在循环控制变量泄露,而 python3 中不存在。

33. 当心默认的可变参数!

Output:

说明:

Python 中函数的默认可变参数并不是每次调用该函数时都会被初始化。相反,它们会使用最近分配的值作为默认值。当我们明确的将作为参数传递给的时候,就不会使用的默认值, 所以函数会返回我们所期望的结果。

避免可变参数导致的错误的常见做法是将指定为参数的默认值,然后检查是否有值传给对应的参数。例:

34. 捕获异常

这里将的是 python2

Output:

说明:

如果你想要同时捕获多个不同类型的异常时,你需要将它们用括号包成一个元组作为第一个参数传递。第二个参数是可选名称,如果你提供,它将与被捕获的异常实例绑定。

也就是说,代码原意是捕获两种异常,但在 python2 中,必须写成,示例中的写法解析器会将理解成绑定的异常实例名。

在 python3 中,不会有这种误解,因为必须使用关键字。

35. +=就地修改

Output:

Output:

说明:

并不总是与 表现相同。

表达式 会生成一个新列表,并让 引用这个新列表,同时保持 不变。

表达式 实际上是使用的是 函数,就地修改列表,所以 和 仍然指向已被修改的同一列表。

36. 外部作用域变量

Output:

说明:

当在函数中引用外部作用域的变量时,如果不对这个变量进行修改,则可以直接引用,如果要对其进行修改,则必须使用 关键字,否则解析器将认为这个变量是局部变量,而做修改之前并没有定义它,所以会报错。

Output:

37. 小心链式操作

根据https://docs.python.org/2/reference/expressions.html#not-in

形式上,如果 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,除了每个表达式最多被评估一次。

就相当于

虽然上面的例子似乎很愚蠢, 但是像 或 就很棒了。

38. 忽略类作用域的名称解析

① 生成器表达式

Output:

② 列表推导式

Output(Python 2.x):

Output(Python 3.x):

说明:

类定义中嵌套的作用域会忽略类内的名称绑定。

生成器表达式有它自己的作用域。

从 Python 3 开始,列表推导式也有自己的作用域。

39. 元组

Output:

Output:

说明:

对于 1,正确的语句是 。

对于 2,正确的语句是 或者 , (缺少逗号) 否则解释器会认为 t 是一个字符串,并逐个字符对其进行迭代。

是一个特殊的标记,表示空元组。

40. else

① 循环末尾的 else

Output:

② try 末尾的 else

Output:

说明:

循环后的 子句只会在循环执行完成(没有触发 break、return 语句)的情况下才会执行。

之后的 子句也被称为 "完成子句",因为在 语句中到达 子句意味着 块实际上已成功完成。

41. 名称改写

Output:

说明:

python 中不能像 Java 那样使用 private 修饰符创建私有属性。但是,解释器会通过给类中以 __(双下划线)开头且结尾最多只有一个下划线的类成员名称加上 _类名来修饰。这能避免子类意外覆盖父类的“私有”属性。

举个例子:有人编写了一个名为 的类,这个类的内部用到了 实例属性,但是没有将其开放。现在,你创建了 类的子类 ,如果你在毫不知情的情况下又创建了一个 实例属性,那么在继承的方法中就会把 类的 属性覆盖掉。

为了避免这种情况,python 会将 变成 ,而对于 Beagle 类来说,会变成 。这个语言特性就叫名称改写(name mangling)。

42. +=更快

说明:

连接两个以上的字符串时 += 比 + 更快,因为在计算过程中第一个字符串(例如, s1 += s2 + s3 中的 s1)不会被销毁。(就是 += 执行的是追加操作,少了一个销毁新建的动作。)

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

扫码关注云+社区

领取腾讯云代金券