1. 程式人生 > >【wtfPython】一組有趣的、微妙的、複雜的Python程式碼片段

【wtfPython】一組有趣的、微妙的、複雜的Python程式碼片段

原作者:董偉明 (Dongweiming)
原文連結:推薦wtfPython: 一組有趣的、微妙的、複雜的Python程式碼片段
本文有細微改動


wtfPython 1 就是「What the f**k Python?」的意思,這個專案列舉了一些程式碼片段,可能結果和你想到的是不一致的,並且作者會告訴你為什麼。本文將展示最有意義的一部分:

混合Tab和空格

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

字典鍵的隱式轉換

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'

這樣的原因是鍵被隱式的轉換了:

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

生成器執行時間的差異

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是在執行期求值的。

在字典迭代時修改該字典

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

首先說的時候在迭代過程中是不能修改字典的長度的:

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]
      3

RuntimeError: dictionary changed size during iteration

但是刪掉一個新增一個是可以,運行了5次才結束是因為字典會定期重新設定以便接受更多的鍵,但是和專案中的執行8次是不一樣的

在列表迭代時刪除條目

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

>>> 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,但是反之不一定:

>>> [] == []
True
>>> [] is [] # 2個列表使用了不同的記憶體位置
False

上面的例子中,-5 - 256由於太經常使用,所以設計成固定存在的物件:

>>> id(256)
10922528
>>> a = 256
>>> b = 256
>>> id(a)
10922528
>>> id(b)
10922528
>>> id(257)
140084850247312
>>> x = 257
>>> y = 257
>>> id(x)
140084850247440
>>> id(y)
140084850247344

is not … 和 is (not …)

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

其中(not None)優先執行,最後其實變成了 ‘something’ is True

迴圈中的函式也會輸出到相同的輸出

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])

開發陷阱,閉包變數繫結,解決方法就是把迴圈的變數傳到some_func裡面去:

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]

迴圈中的區域性變數洩露

>>> 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這個問題解決了

可變預設引數

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']

Python是引用傳遞,上面例子的引數是一個列表,它所指向的物件可以被修改。通用的解決辦法是在函式內判斷:

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

+和+=的差別

>>> 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操作

元組賦值

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])

在我們的印象裡面元組是不可變的呀?簡單的說對list的賦值成功了,但是賦值失敗了,不過由於值是引用的,所以才會出現這個執行失敗實際成功的效果

使用在範圍內未定義的變數

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()
      4
      5 def another_func():
----> 6     a += 1
      7     return a

UnboundLocalError: local variable 'a' referenced before assignment

這是由於在another_func中的賦值操作會把a變成一個本地變數,但是在相同範圍內並沒有初始化它。如果希望它能正確執行可以加global:

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

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

使用finally的return

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語句永遠是最後一個執行

忽略類範圍的名稱解析

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

列表中的布林值

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的子類:

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

自引用迴圈巢狀賦值

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

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

看起來有點懵吧,我們拆一下:

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

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

這樣b是5,而a[5]的值是({}, 5),所以a是{5: ({}, 5)。接著看:

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

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

這其實是一個對自己的「自引用」,看個例子:

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用…來表示了


  1. wtfpython - A collection of surprising Python snippets and lesser-known features.