Python:不爲人知的功能特性(下)

GitHub 上有一個名爲《What the f*ck Python!》的項目,這個有趣的項目意在收集 Python 中那些難以理解和反人類直覺的例子以及不爲人知的功能特性,並嘗試討論這些現象背後真正的原理! 原版地址:github.com/satwikkansa…。 最近,一位名爲「暮晨」的貢獻者將其翻譯成了中文。 中文版地址:github.com/leisurelich…html

上一篇 Python:不爲人知的功能特性(上)python

本來每一個的標題都是原版中的英文,有些取名比較奇怪,不直觀,我換成了能夠描述主題的中文形式,有些是本身想的,不足之處請指正。另一些 Python 中的彩蛋被我去掉了。git

我將全部代碼都親自試過了,加入了一些本身的理解和例子,因此會和原文稍有不一樣。github

21. 子類關係

>>> from collections import Hashable
>>> issubclass(list, object)
True
>>> issubclass(object, Hashable)
True
>>> issubclass(list, Hashable)
False
複製代碼

子類關係應該是可傳遞的,對吧?即,若是 AB 的子類,BC 的子類,那麼 A 應該 是 C 的子類。 說明:express

  • 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'>
複製代碼

說明:bash

  • 因爲 SomeClass 會從 str 自動繼承 __hash__() 方法,因此 s 對象和 's' 字符串的哈希值是相同的。
  • SomeClass('s') == 's'True 是由於 SomeClass 也繼承了 str__eq__() 方法。
  • 因爲二者的哈希值相同且相等,因此它們在字典中表示相同的鍵。

若是想要實現指望的功能, 咱們能夠重定義 SomeClass__eq__() 方法.app

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 語言參考,賦值語句的形式以下:ide

(target_list "=")+ (expression_list | yield_expression)
複製代碼

賦值語句計算表達式列表(expression list)(請記住,這能夠是單個表達式或以逗號分隔的列表,後者返回元組)並將單個結果對象從左到右分配給目標列表中的每一項。

(target_list "=")+ 中的 + 意味着能夠有一個或多個目標列表。在這個例子中,目標列表是 a, ba[b]。表達式列表只能有一個,是 {}, 5

這話看着很是的晦澀,咱們來看一個簡單的例子:

a, b = b, c = 1, 2
print(a, b, c)
複製代碼

Output:

1 1 2
複製代碼

在這個簡單的例子中,目標列表是 a, bb, 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 中,這種狀況是容許的,listdict 的實現方式是不同的,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 xPython 會將 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_2list_4 中刪除 1 時,列表的內容就變成了[2, 3, 4]。剩餘元素會依次位移,也就是說,2的索引會變爲 0,3會變爲 1。因爲下一次迭代將獲取索引爲 1 的元素(即3), 所以2將被完全的跳過。相似的狀況會交替發生在列表中的每一個元素上。

29. 循環變量泄漏!

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
複製代碼

# 此次咱們先初始化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
複製代碼

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.xPython 3.x 解釋器在列表推導式示例中的輸出差別,在文檔 What’s New In Python 3.0 中能夠找到相關的解釋:

"列表推導再也不支持句法形式[... 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. +=就地修改

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]
複製代碼

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() 函數,就地修改列表,因此 ab 仍然指向已被修改的同一列表。

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
複製代碼

根據 docs.python.org/2/reference…

形式上,若是 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 == c0 <= x <= 100 就很棒了。

35. 忽略類做用域的名稱解析

① 生成器表達式

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]
複製代碼

② 列表推導式

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. 元組

x, y = (0, 1) if True else None, None
複製代碼

Output:

>>> x, y  # 指望的結果是 (0, 1)
((0, 1), None)
複製代碼

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

① 循環末尾的 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
複製代碼

② 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)不會被銷燬。(就是 += 執行的是追加操做,少了一個銷燬新建的動做。)


掃碼關注個人我的公衆號
相關文章
相關標籤/搜索