GitHub 上有一个名为《What the f*ck Python!》的项目, 这个有趣的项目意在收集 Python 中那些难以理解和反人类直觉的例子以及鲜为人知的功能特性, 并尝试讨论这些现象背后真正的原理! 原版地址: https://github.com/satwikkansal/wtfpython . 最近, 一位名为 "暮晨" 的贡献者将其翻译成了中文. 中文版地址: https://github.com/leisurelicht/wtfpython-cn
上一篇 Python: 鲜为人知的功能特性(上)
原本每个的标题都是原版中的英文, 有些取名比较奇怪, 不直观, 我换成了可以描述主题的中文形式, 有些是自己想的, 不足之处请指正. 另外一些 Python 中的彩蛋被我去掉了.
我将所有代码都亲自试过了, 加入了一些自己的理解和例子, 所以会和原文稍有不同.
21. 子类关系
- >>> from collections import Hashable
- >>> issubclass(list, object)
- True
- >>> issubclass(object, Hashable)
- True
- >>> issubclass(list, Hashable)
- False
子类关系应该是可传递的, 对吧? 即, 如果 A 是 B 的子类, B 是 C 的子类, 那么 A 应该 是 C 的子类. 说明:
Python 中的子类关系并不必须是传递的, 任何人都可以在元类中随意定义 __subclasscheck__.
当
issubclass(cls, Hashable)
被调用时, 它只是在 cls 中寻找 __hash__() 方法或继承自 __hash__() 的方法.
由于 object 是可散列的(hashable), 而 list 是不可散列的, 所以它打破了这种传递关系.
22. 神秘的键型转换
- class SomeClass(str):
- pass
- some_dict = {'s': 42}
- Output:
- >>> type(list(some_dict.keys())[0])
- <class 'str'>
- >>> s = SomeClass('s')
- >>> some_dict[s] = 40
- >>> some_dict # 预期: 两个不同的键值对
- {
- 's': 40
- }
- >>> type(list(some_dict.keys())[0])
- <class 'str'>
说明:
由于 SomeClass 会从 str 自动继承 __hash__() 方法, 所以 s 对象和's' 字符串的哈希值是相同的.
而
SomeClass('s') == 's'
为 True 是因为 SomeClass 也继承了 str 类 __eq__() 方法.
由于两者的哈希值相同且相等, 所以它们在字典中表示相同的键.
如果想要实现期望的功能, 我们可以重定义 SomeClass 的 __eq__() 方法.
- class SomeClass(str):
- def __eq__(self, other):
- return (
- type(self) is SomeClass
- and type(other) is SomeClass
- and super().__eq__(other)
- )
- # 当我们自定义 __eq__() 方法时, Python 不会再自动继承 __hash__() 方法
- # 所以我们也需要定义它
- __hash__ = str.__hash__
- some_dict = {'s':42}
- Output:
- >>> s = SomeClass('s')
- >>> some_dict[s] = 40
- >>> some_dict
- {
- 's': 40, 's': 42
- }
- >>> keys = list(some_dict.keys())
- >>> type(keys[0]), type(keys[1])
- <class 'str'> <class '__main__.SomeClass'>
23. 链式赋值表达式
- >>> a, b = a[b] = {
- }, 5
- >>> a
- {
- 5: ({
- ...
- }, 5)
- }
说明: 根据 Python 语言参考, 赋值语句的形式如下:
(target_list "=")+ (expression_list | yield_expression)
赋值语句计算表达式列表 (expression list)(请记住, 这可以是单个表达式或以逗号分隔的列表, 后者返回元组) 并将单个结果对象从左到右分配给目标列表中的每一项.
(target_list "=")+ 中的 + 意味着可以有一个或多个目标列表. 在这个例子中, 目标列表是 a, b 和 a[b]. 表达式列表只能有一个, 是 {}, 5.
这话看着非常的晦涩, 我们来看一个简单的例子:
- a, b = b, c = 1, 2
- print(a, b, c)
Output:
1 1 2
在这个简单的例子中, 目标列表是 a, b 和 b, c, 表达式是 1, 2. 将表达式从左到右赋给目标列表, 上述例子就可以拆分成:
- a, b = 1, 2
- b, c = 1, 2
所以结果就是 1 1 2.
那么, 原例子就不难理解了, 拆解开来就是:
- a, b = {
- }, 5
- a[b] = a, b
这里不能写作 a[b] = {}, 5, 因为这样第一句中的 {} 和第二句中的 {} 其实就是不同的对象了, 而实际他们是同一个对象. 这就形成了循环引用, 输出中的 {...} 指与 a 引用了相同的对象.
我们来验证一下:
- >>> a[b][0] is a
- True
可见确实是同一个对象.
以下是一个简单的循环引用的例子:
- >>> some_list = some_list[0] = [0]
- >>> some_list
- [[...]]
- >>> some_list[0]
- [[...]]
- >>> some_list is some_list[0]
- True
- >>> some_list[0][0][0][0][0][0] == some_list
- True
24. 空间移动
- import numpy as np
- def energy_send(x):
- # 初始化一个 numpy 数组
- np.array([float(x)])
- def energy_receive():
- # 返回一个空的 numpy 数组
- return np.empty((), dtype=np.float).tolist()
- Output:
- >>> energy_send(123.456)
- >>> energy_receive()
- 123.456
说明: energy_send() 函数中创建的 numpy 数组并没有返回, 因此内存空间被释放并可以被重新分配. numpy.empty() 直接返回下一段空闲内存, 而不重新初始化. 而这个内存点恰好就是刚刚释放的那个(通常情况下, 并不绝对).
25. 不要混用制表符 (tab) 和空格(space)
tab 是 8 个空格, 而用空格表示则一个缩进是 4 个空格, 混用就会出错. python3 里直接不允许这种行为了, 会报错:
TabError: inconsistent use of tabs and spaces in indentation
很多编辑器, 例如 pycharm, 可以直接设置 tab 表示 4 个空格.
26. 迭代字典时的修改
- x = {0: None}
- for i in x:
- del x[i]
- x[i+1] = None
- print(i)
- Output(Python 2.7- Python 3.5):
- 0
- 1
- 2
- 3
- 4
- 5
- 6
- 7
说明: Python 不支持 对字典进行迭代的同时修改它, 它之所以运行 8 次, 是因为字典会自动扩容以容纳更多键值(译: 应该是因为字典的初始最小值是 8, 扩容会导致散列表地址发生变化而中断循环). 在不同的 Python 实现中删除键的处理方式以及调整大小的时间可能会有所不同, python3.6 开始, 到 5 就会扩容.
而在 list 中, 这种情况是允许的, list 和 dict 的实现方式是不一样的, list 虽然也有扩容, 但 list 的扩容是整体搬迁, 并且顺序不变.
- list = [1]
- j = 0
- for i in list:
- print(i)
- list.append(i + 1)
这个代码可以一直运行下去直到 int 越界. 但一般不建议在迭代的同时修改 list.
- 27. _del_
- class SomeClass:
- def __del__(self):
- print("Deleted!")
- Output:
- >>> x = SomeClass()
- >>> y = x
- >>> del x # 这里应该会输出 "Deleted!"
- >>> del y
- Deleted!
说明: del x 并不会立刻调用 x.__del__(), 每当遇到 del x,Python 会将 x 的引用数减 1, 当 x 的引用数减到 0 时就会调用 x.__del__().
我们再加一点变化:
- >>> x = SomeClass()
- >>> y = x
- >>> del x
- >>> y # 检查一下 y 是否存在
- <__main__.SomeClass instance at 0x7f98a1a67fc8>
- >>> del y # 像之前一样, 这里应该会输出 "Deleted!"
- >>> globals() # 好吧, 并没有. 让我们看一下所有的全局变量
- Deleted!
- {
- '__builtins__': <module '__builtin__' (built-in)>, 'SomeClass': <class __main__.SomeClass at 0x7f98a1a5f668>, '__package__': None, '__name__': '__main__', '__doc__': None
- }
y.__del__()之所以未被调用, 是因为前一条语句 (>>> y) 对同一对象创建了另一个引用, 从而防止在执行 del y 后对象的引用数变为 0.(这其实是 Python 交互解释器的特性, 它会自动让 _ 保存上一个表达式输出的值.) 调用 globals()导致引用被销毁, 因此我们可以看到 Deleted! 终于被输出了.
28. 迭代列表时删除元素
在前面我附加了一个迭代列表时添加元素的例子, 现在来看看迭代列表时删除元素.
- list_1 = [1, 2, 3, 4]
- list_2 = [1, 2, 3, 4]
- list_3 = [1, 2, 3, 4]
- list_4 = [1, 2, 3, 4]
- for idx, item in enumerate(list_1):
- del item
- for idx, item in enumerate(list_2):
- list_2.remove(item)
- for idx, item in enumerate(list_3[:]):
- list_3.remove(item)
- for idx, item in enumerate(list_4):
- list_4.pop(idx)
- Output:
- >>> list_1
- [1, 2, 3, 4]
- >>> list_2
- [2, 4]
- >>> list_3
- []
- >>> list_4
- [2, 4]
说明: 在迭代时修改对象是一个很愚蠢的主意, 正确的做法是迭代对象的副本, list_3[:]就是这么做的.
del,remove,pop 的不同:
del var_name 只是从本地或全局命名空间中删除了 var_name(这就是为什么 list_1 没有受到影响).
remove 会删除第一个匹配到的指定值, 而不是特定的索引, 如果找不到值则抛出 ValueError 异常.
pop 则会删除指定索引处的元素并返回它, 如果指定了无效的索引则抛出 IndexError 异常.
为什么输出是 [2, 4]? 列表迭代是按索引进行的, 所以当我们从 list_2 或 list_4 中删除 1 时, 列表的内容就变成了[2, 3, 4]. 剩余元素会依次位移, 也就是说, 2 的索引会变为 0,3 会变为 1. 由于下一次迭代将获取索引为 1 的元素(即 3), 因此 2 将被彻底的跳过. 类似的情况会交替发生在列表中的每个元素上.
29. 循环变量泄漏!
- 1
- for x in range(7):
- if x == 6:
- print(x, ': for x inside loop')
- print(x, ': x in global')
- Output:
- 6 : for x inside loop
- 6 : x in global
- 2
- # 这次我们先初始化 x
- x = -1
- for x in range(7):
- if x == 6:
- print(x, ': for x inside loop')
- print(x, ': x in global')
- Output:
- 6 : for x inside loop
- 6 : x in global
- 3
- x = 1
- print([x for x in range(5)])
- print(x, ': x in global')
- Output(Python 2):
- [0, 1, 2, 3, 4]
- (4, ': x in global')
- Output(Python 3):
- [0, 1, 2, 3, 4]
- 1 : x in global
说明: 在 Python 中, for 循环使用所在作用域并在结束后保留定义的循环变量. 如果我们曾在全局命名空间中定义过循环变量, 它会重新绑定现有变量. Python 2.x 和 Python 3.x 解释器在列表推导式示例中的输出差异, 在文档 What's New In Python 3.0 https://docs.python.org/3/whatsnew/3.0.html 中可以找到相关的解释:" 列表推导不再支持句法形式 [... for var in item1, item2, ...]. 使用[... for var in (item1, item2, ...)] 代替. 另外注意, 列表推导具有不同的语义: 它们更接近于 list()构造函数中生成器表达式的语法糖, 特别是循环控制变量不再泄漏到周围的作用域中."
简单来说, 就是 python2 中, 列表推导式依然存在循环控制变量泄露, 而 python3 中不存在.
30. 当心默认的可变参数!
- def some_func(default_arg=[]):
- default_arg.append("some_string")
- return default_arg
- Output:
- >>> some_func()
- ['some_string']
- >>> some_func()
- ['some_string', 'some_string']
- >>> some_func([])
- ['some_string']
- >>> some_func()
- ['some_string', 'some_string', 'some_string']
说明: Python 中函数的默认可变参数并不是每次调用该函数时都会被初始化. 相反, 它们会使用最近分配的值作为默认值. 当我们明确的将 [] 作为参数传递给 some_func 的时候, 就不会使用 default_arg 的默认值, 所以函数会返回我们所期望的结果.
- >>> some_func.__defaults__ # 这里会显示函数的默认参数的值
- ([],)
- >>> some_func()
- >>> some_func.__defaults__
- (['some_string'],)
- >>> some_func()
- >>> some_func.__defaults__
- (['some_string', 'some_string'],)
- >>> some_func([])
- >>> some_func.__defaults__
- (['some_string', 'some_string'],)
避免可变参数导致的错误的常见做法是将 None 指定为参数的默认值, 然后检查是否有值传给对应的参数. 例:
- def some_func(default_arg=None):
- if not default_arg:
- default_arg = []
- default_arg.append("some_string")
- return default_arg
31. 捕获异常
这里讲的是 python2
- some_list = [1, 2, 3]
- try:
- # 这里会抛出异常 ``IndexError``
- print(some_list[4])
- except IndexError, ValueError:
- print("Caught!")
- try:
- # 这里会抛出异常 ``ValueError``
- some_list.remove(4)
- except IndexError, ValueError:
- print("Caught again!")
- Output:
- Caught!
- ValueError: list.remove(x): x not in list
说明: 如果你想要同时捕获多个不同类型的异常时, 你需要将它们用括号包成一个元组作为第一个参数传递. 第二个参数是可选名称, 如果你提供, 它将与被捕获的异常实例绑定. 也就是说, 代码原意是捕获 IndexError, ValueError 两种异常, 但在 python2 中, 必须写成(IndexError, ValueError), 示例中的写法解析器会将 ValueError 理解成绑定的异常实例名. 在 python3 中, 不会有这种误解, 因为必须使用 as 关键字.
32. += 就地修改
- 1
- a = [1, 2, 3, 4]
- b = a
- a = a + [5, 6, 7, 8]
- Output:
- >>> a
- [1, 2, 3, 4, 5, 6, 7, 8]
- >>> b
- [1, 2, 3, 4]
- 2
- a = [1, 2, 3, 4]
- b = a
- a += [5, 6, 7, 8]
- Output:
- >>> a
- [1, 2, 3, 4, 5, 6, 7, 8]
- >>> b
- [1, 2, 3, 4, 5, 6, 7, 8]
说明: a += b 并不总是与 a = a + b 表现相同. 表达式 a = a + [5,6,7,8] 会生成一个新列表, 并让 a 引用这个新列表, 同时保持 b 不变. 表达式 a += [5, 6, 7, 8] 实际上是使用的是 extend() 函数, 就地修改列表, 所以 a 和 b 仍然指向已被修改的同一列表.
33. 外部作用域变量
- a = 1
- def some_func():
- return a
- def another_func():
- a += 1
- return a
- Output:
- >>> some_func()
- 1
- >>> another_func()
- UnboundLocalError: local variable 'a' referenced before assignment
说明: 当在函数中引用外部作用域的变量时, 如果不对这个变量进行修改, 则可以直接引用, 如果要对其进行修改, 则必须使用 global 关键字, 否则解析器将认为这个变量是局部变量, 而做修改之前并没有定义它, 所以会报错.
- def another_func()
- global a
- a += 1
- return a
- Output:
- >>> another_func()
- 2
34. 小心链式操作
- >>> (False == False) in [False] # 可以理解
- False
- >>> False == (False in [False]) # 可以理解
- False
- >>> False == False in [False] # 为毛?
- True
- >>> True is False == False
- False
- >>> False is False is False
- True
- >>> 1> 0 <1
- True
- >>> (1> 0) <1
- False
- >>> 1> (0 <1)
- False
根据
形式上, 如果 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, 除了每个表达式最多被评估一次.
False == False in [False]
就相当于
False == False and False in [False]
1> 0 <1 就相当于 1> 0 and 0 <1
虽然上面的例子似乎很愚蠢, 但是像 a == b == c 或 0 <= x <= 100 就很棒了.
35. 忽略类作用域的名称解析
1 生成器表达式
- x = 5
- class SomeClass:
- x = 17
- y = (x for i in range(10))
- Output:
- >>> list(SomeClass.y)
- [5, 5, 5, 5, 5, 5, 5, 5, 5, 5]
2 列表推导式
- x = 5
- class SomeClass:
- x = 17
- y = [x for i in range(10)]
- Output(Python 2):
- >>> SomeClass.y
- [17, 17, 17, 17, 17, 17, 17, 17, 17, 17]
- Output(Python 3):
- >>> SomeClass.y
- [5, 5, 5, 5, 5, 5, 5, 5, 5, 5]
说明:
类定义中嵌套的作用域会忽略类内的名称绑定.
生成器表达式有它自己的作用域.
从 Python 3 开始, 列表推导式也有自己的作用域.
36. 元组
- 1
- x, y = (0, 1) if True else None, None
- Output:
- >>> x, y # 期望的结果是 (0, 1)
- ((0, 1), None)
- 2
- t = ('one', 'two')
- for i in t:
- print(i)
- t = ('one')
- for i in t:
- print(i)
- t = ()
- print(t)
- Output:
- one
- two
- o
- n
- e
- tuple()
说明:
对于 1, 正确的语句是
- x, y = (0, 1) if True else (None, None)
- .
对于 2, 正确的语句是 t = ('one',) 或者 t = 'one', (缺少逗号) 否则解释器会认为 t 是一个字符串, 并逐个字符对其进行迭代.
() 是一个特殊的标记, 表示空元组.
37. else
1 循环末尾的 else
- def does_exists_num(l, to_find):
- for num in l:
- if num == to_find:
- print("Exists!")
- break
- else:
- print("Does not exist")
- Output:
- >>> some_list = [1, 2, 3, 4, 5]
- >>> does_exists_num(some_list, 4)
- Exists!
- >>> does_exists_num(some_list, -1)
- Does not exist
2 try 末尾的 else
- try:
- pass
- except:
- print("Exception occurred!!!")
- else:
- print("Try block executed successfully...")
- Output:
- Try block executed successfully...
说明: 循环后的 else 子句只会在循环执行完成 (没有触发 break,return 语句) 的情况下才会执行. try 之后的 else 子句也被称为 "完成子句", 因为在 try 语句中到达 else 子句意味着 try 块实际上已成功完成.
38. 名称改写
- class Yo(object):
- def __init__(self):
- self.__honey = True
- self.bitch = True
- Output:
- >>> Yo().bitch
- True
- >>> Yo().__honey
- AttributeError: 'Yo' object has no attribute '__honey'
- >>> Yo()._Yo__honey
- True
说明: python 中不能像 Java 那样使用 private 修饰符创建私有属性. 但是, 解释器会通过给类中以 __(双下划线)开头且结尾最多只有一个下划线的类成员名称加上 __类名_ 来修饰. 这能避免子类意外覆盖父类的 "私有" 属性.
举个例子: 有人编写了一个名为 Dog 的类, 这个类的内部用到了 mood 实例属性, 但是没有将其开放. 现在, 你创建了 Dog 类的子类 Beagle, 如果你在毫不知情的情况下又创建了一个 mood 实例属性, 那么在继承的方法中就会把 Dog 类的 mood 属性覆盖掉.
为了避免这种情况, python 会将 __mood 变成 _Dog__mood, 而对于 Beagle 类来说, 会变成 _Beagle__mood. 这个语言特性就叫名称改写(name mangling).
39. += 更快
- >>> timeit.timeit("s1 = s1 + s2 + s3", setup="s1 =' '* 100000; s2 =' '* 100000; s3 =' '* 100000", number=100)
- 0.25748300552368164
- # 用 "+=" 连接三个字符串:
- >>> timeit.timeit("s1 += s2 + s3", setup="s1 =' '* 100000; s2 =' '* 100000; s3 =' '* 100000", number=100)
- 0.012188911437988281
说明: 连接两个以上的字符串时 += 比 + 更快, 因为在计算过程中第一个字符串 (例如, s1 += s2 + s3 中的 s1) 不会被销毁.(就是 += 执行的是追加操作, 少了一个销毁新建的动作.)
来源: https://juejin.im/post/5c7beef96fb9a049d44288f6