推荐wtfPython:一组有趣的、微妙的、复杂的Python代码片段

wtfPython就是「What the f*ck Python?
」的意思,这个项目列举了一些代码片段,可能结果和你想到的是不一致的,并且作者会告诉你为什么。本来将展示最有意义的一部分:

混合Tab和空格

1
2
3
4
5
6
7
8

def square(x):
sum_so_far = 0
for counter in range(x):
sum_so_far = sum_so_far + x
return sum_so_far

print(square(10))

结果是10??不是应该100么?
其实这种错误的结果的原因,所有书籍和开发者都说过,就是不要混Tab和空格,源代码你可以看项目中的mixed_tabs_and_spaces.py。

字典键的隐式转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

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

In [2]: some_dict[5.5]
Out[2]: 'Ruby'

In [3]: some_dict[5.0]
Out[3]: 'Python'

In [4]: some_dict[5]
Out[4]: 'Python'

这样的原因是键被隐式的转换了:

1
2
3

In [5]: hash(5) == hash(5.0)
Out[5]: True

生成器执行时间的差异

1
2
3
4
5
6
7
8

In [6]: array = [1, 8, 15]
...: g = (x for x in array if array.count(x) > 0)
...: array = [2, 8, 22]
...:

In [7]: print(list(g))
[8]

这种隐式的非预期结果在实际开发中是可能出现的。原因是in的操作是在申明时求值的,而if是在运行期求值的。

在字典迭代时修改该字典

1
2
3
4
5
6
7
8

In [8]: x = {0: None}
...:
...: for i in x:
...: del x[i]
...: x[i+1] = None
...: print(i)
...:

首先说的时候在迭代过程中是不能修改字典的长度的:

1
2
3
4
5
6
7
8
9
10
11
12

In [13]: for i in x:
...: del x[i]
...:
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
<ipython-input-13-a5c6e73be64f> in <module>()
----> 1 for i in x:
2 del x[i]


RuntimeError: dictionary changed size during iteration

但是删掉一个添加一个是可以的。运行了5次才结束是因为字典会定期重新设置以便接受更多的键,但是和项目中的运行8次是不一样的。

在列表迭代时删除条目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

In [14]: 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)
...:

In [15]: list_1, list_2
Out[15]: ([1, 2, 3, 4], [2, 4])

In [16]: list_3, list_4
Out[16]: ([], [2, 4])

其中只有list_3是正确的行为。但是为什么会出现[2, 4]的结果呢?第一次删掉了index是0的1,就剩[2, 3, 4],然后移除index 1,
就是3,剩下了[2, 4],但是现在只有2个元素,循环就结束了。

is

1
2
3
4
5
6
7
8
9
10
11
12
13
14

>>> a = 256
>>> b = 256
>>> a is b
True

>>> a = 257
>>> b = 257
>>> a is b
False

>>> a = 257; b = 257
>>> a is b
True

is用来对比身份,而==用来对比值。通常is为True,==就是True,但是反之不一定:

1
2
3
4
5

>>> [] == []
True
>>> [] is [] # 2个列表使用了不同的内存位置
False

上面的例子中,-5 - 256由于太经常使用,所以设计成固定存在的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

>>> id(256)

>>> a = 256
>>> b = 256
>>> id(a)

>>> id(b)

>>> id(257)

>>> x = 257
>>> y = 257
>>> id(x)

>>> id(y)

is not … 和 is (not …)

1
2
3
4
5

>>> 'something' is not None
True
>>> 'something' is (not None)
False

其中(not None)优先执行,最后其实变成了'something' is True

循环中的函数也会输出到相同的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14

In [17]: funcs = []
...: results = []
...: for x in range(7):
...: def some_func():
...: return x
...: funcs.append(some_func)
...: results.append(some_func())
...:
...: funcs_results = [func() for func in funcs]
...:

In [18]: results, funcs_results
Out[18]: ([0, 1, 2, 3, 4, 5, 6], [6, 6, 6, 6, 6, 6, 6])

我之前在[Expert-Python](https://github.com/dongweiming/Expert-
Python)这个PPT中介绍过「开发陷阱,闭包变量绑定」,其实这个例子就是因为这个问题。解决方法就是把循环的变量传到some_func里面去:

1
2
3
4
5
6
7
8
9
10

In [19]: funcs = []
...: for x in range(7):
...: def some_func(x=x):
...: return x
...: funcs.append(some_func)
...:

In [20]: [func() for func in funcs]
Out[20]: [0, 1, 2, 3, 4, 5, 6]

循环中的局部变量泄露

1
2
3
4
5
6

>>> x = 1
>>> print([x for x in range(5)])
[0, 1, 2, 3, 4]
>>> print(x, ': x in global')
(4, ': x in global')

在Python 2中x的值在一个循环执行之后被改变了。不过再Python 3这个问题解决了。

可变默认参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

In [1]: def some_func(default_arg=[]):
...: default_arg.append("some_string")
...: return default_arg
...:

In [2]: some_func()
Out[2]: ['some_string']

In [3]: some_func()
Out[3]: ['some_string', 'some_string']

In [4]: some_func([])
Out[4]: ['some_string']

In [5]: some_func()
Out[5]: ['some_string', 'some_string', 'some_string']

在[Expert-Python](https://github.com/dongweiming/Expert-
Python)这个PPT中同样介绍过。Python是引用传递,上面例子的参数是一个列表,它所指向的对象可以被修改。通用的解决办法是在函数内判断:

1
2
3
4
5
6

def some_func(default_arg=None):
if not default_arg:
default_arg = []
default_arg.append("some_string")
return default_arg

+ 和 +=的差别

1
2
3
4
5
6
7
8
9
10
11
12

>>> a = [1, 2, 3, 4]
>>> b = a
>>> a = a + [5, 6, 7, 8]
>>> a, b
([1, 2, 3, 4, 5, 6, 7, 8], [1, 2, 3, 4])

>>> a = [1, 2, 3, 4]
>>> b = a
>>> a += [5, 6, 7, 8]
>>> a, b
([1, 2, 3, 4, 5, 6, 7, 8], [1, 2, 3, 4, 5, 6, 7, 8])

通常的运算过程,区别就是a = a + X 和 a += X。这是因为a = a + X是重新创建一个对象a,而a += X是在a这个list上面做extend操作。

元组赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

In [6]: another_tuple = ([1, 2], [3, 4], [5, 6])
...:

In [7]: another_tuple[2].append(1000)

In [8]: another_tuple
Out[8]: ([1, 2], [3, 4], [5, 6, 1000])

In [9]: another_tuple[2] += [99, 999]
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-9-d07c65f24a63> in <module>()
----> 1 another_tuple[2] += [99, 999]

TypeError: 'tuple' object does not support item assignment

In [10]: another_tuple
Out[10]: ([1, 2], [3, 4], [5, 6, 1000, 99, 999])

在我们的印象里面元组是不可变的呀?其实我之前还专门写过一篇Python元组的赋值谜题讲这个问题,简单的说对list的赋值成功了,但是赋值失败了,不过由于值是引用的,所以才会出现这个执行失败实际成功的效果。

使用在范围内未定义的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

In [11]: a = 1
...: def some_func():
...: return a
...:
...: def another_func():
...: a += 1
...: return a
...:

In [12]: some_func()
Out[12]: 1

In [13]: another_func()
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-13-703bd168975f> in <module>()
----> 1 another_func()

<ipython-input-11-cff7ceae4600> in another_func()

5 def another_func():
----> 6 a += 1
7 return a

UnboundLocalError: local variable 'a' referenced before assignment

这是由于在another_func中的赋值操作会把a变成一个本地变量,但是在相同范围内并没有初始化它。如果希望它能正确运行可以加global:

1
2
3
4
5
6
7
8
9

In [17]: def another_func():
...: global a
...: a += 1
...: return a
...:

In [18]: another_func()
Out[18]: 2

使用finally的return

1
2
3
4
5
6
7
8
9
10

In [19]: def some_func():
...: try:
...: return 'from_try'
...: finally:
...: return 'from_finally'
...:

In [20]: some_func()
Out[20]: 'from_finally'

try…finally这种写法里面,finally中的return语句永远是最后一个执行

忽略类范围的名称解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

In [21]: x = 5
...: class SomeClass:
...: x = 17
...: y = (x for i in range(10))
...:

In [22]: list(SomeClass.y)[0]
Out[22]: 5

In [23]: x = 5
...: class SomeClass:
...: x = 17
...: y = [x for i in range(10)]
...:

In [24]: SomeClass.y[0]
Out[24]: 5

这是由于类范围的名称解析被忽略了,而生成器有它自己的本地范围,而在Python3中列表解析也有自己的范围,所以x的值是5。不过,第二个例子在Python2中SomeClass.y[0]的值是17。

列表中的布尔值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

In [34]: mixed_list = [False, 1.0, "some_string", 3, True, [], False]
...: integers_found_so_far = 0
...: booleans_found_so_far = 0
...:
...: for item in mixed_list:
...: if isinstance(item, int):
...: integers_found_so_far += 1
...: elif isinstance(item, bool):
...: booleans_found_so_far += 1
...:

In [35]: booleans_found_so_far
Out[35]: 0

In [36]: integers_found_so_far
Out[36]: 4

这是由于布尔也是int的子类:

1
2
3

In [41]: isinstance(True, int)
Out[41]: True

###

1
2
3
4
5
6

In [42]: a, b = a[b] = {}, 5
...:

In [43]: a, b
Out[43]: ({5: ({...}, 5)}, 5)

看起来有点懵吧,我们拆一下:

1
2
3
4
5

In [44]: a[b] = {}, 5

In [47]: a, b
Out[47]: ({5: ({}, 5)}, 5)

这样b是5,而a[5]的值是({}, 5),所以a是{5: ({}, 5)。接着看:

1
2
3
4
5

In [48]: a[b] = a, b

In [49]: a
Out[49]: {5: ({...}, 5)}

这其实是一个对自己的「自引用」,看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13

In [50]: a = {}

In [51]: a[5] = a

In [52]: a
Out[52]: {5: {...}}

In [53]: a[5] == a
Out[53]: True

In [54]: a[5][5][5]
Out[54]: {5: {...}}

看,a[5]就是a,这可以是一个永久循环,Python用…来表示了

版权声明:本文由 董伟明 原创,未经作者授权禁止任何微信公众号和向掘金(juejin.im)转载,技术博客转载采用 保留署名-非商业性使用-禁止演绎 4.0-国际许可协议
python