Python中反人類直覺的特性,你踩過坑嗎?

Python是一個基於C語言實現的解釋型高級語言, 提供了很多舒適的功能特性,使用起來非常方便。 但有的時候, Python的輸出結果,讓我們感覺一頭霧水,其中原因自然是Python語言內部實現導致的,下面我們就給大家總結一些難以理解和反人類直覺的例子。

奇妙的字符串

  • 普通相同字符
<code>a = 'small_tom'
id(a)

# 輸出: 140232182302576/<code>
<code>b = 'small' + '_' + 'tom'
id(b)
# 輸出:140232182302576/<code>
<code>id(a) == id(b)
# 輸出: True/<code>
  • 包含特殊字符
<code>a = 'tom'
b = 'tom'
a is b
# 輸出:True/<code>
<code>a = 'tom!'
b = 'tom!'
a is b
# 輸出:False/<code>
<code>a, b = 'tom!', 'tom!'
a is b
# 輸出:False Python3.7以下為True/<code>
<code>'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'
# 輸出:True
'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
# 輸出:True Python3.7以下為False/<code>
<code>a = 'tom'
b = ''.join(['t', 'o', 'm'])
a is b
# 輸出:/<code>

為什麼會出現以上的現象呢?因為編譯器的優化特性(很多語言的不同編譯器都有相應的優化策略),對於不可變對象,在某些情況下並不會創建新的對象,而是會嘗試使用已存在的對象,從而節省內存,可以稱之為**字符串駐留**。字符串的駐留是隱式的,不受我們控制,但是我們可以根據一些規律來猜測是否發生字符串駐留:

  • 所有長度為 0 和長度為 1 的字符串都被駐留
  • 字符串中只包含字母,數字或下劃線時將會駐留。所以 'tom!' 由於包含 ! 而未被駐留。
  • 'tom'將被駐留,而''.join(['t', 'o', 'm'])不被駐留
  • 當在同一行將 a 和 b 的值設置為 "tom!" 的時候, Python 解釋器會創建一個新對象, 然後同時引用第二個變量(譯: 僅適用於3.7以下). 如果你在不同的行上進行賦值操作, 它就不會“知道”已經有一個 wtf! 對象 (因為 "wtf!" 不是按照上面提到的方式被隱式駐留的). 它是一種編譯器優化, 特別適用於交互式環境
  • 當在同一行將 a 和 b 的值設置為 "tom!" 的時候, Python 解釋器會創建一個新對象, 然後同時引用第二個變量(僅適用於3.7以下). 如果你在不同的行上進行賦值操作, 它就不會“知道”已經有一個 tom! 對象 (因為 "tom!" 不是按照上面提到的方式被隱式駐留的). 它是一種編譯器優化, 特別適用於交互式環境.
  • 常量摺疊(constant folding) 是 Python 中的一種 窺孔優化(peephole optimization) 技術. 這意味著在編譯時表達式 'a'*20 會被替換為 'aaaaaaaaaaaaaaaaaaaa' 以減少運行時的時鐘週期. 只有長度小於 20 的字符串才會發生常量摺疊. 為什麼呢?想象一下由於表達式 'a'*10**10 而生成的.pyc 文件的大小)。
  • **PS**:如果是在Python3.7中會發現部分執行結果會不一樣,因為3.7版本中常量摺疊已經從窺孔優化器遷移至新的AST優化器,後者可以以更高的一致性來執行優化。但是在3.8中結果又不一樣了,他們都是用了AST優化器,可能是3.8中有一些其他的調整。

    字典的魔法

    <code>some_dict = {}
    some_dict[5.5] = "Ruby"
    some_dict[5.0] = "JavaScript"
    some_dict[5] = "Python"/<code>
    <code>some_dict[5.5]
    # 輸出:Ruby
    some_dict[5.0]
    # 輸出:Python
    some_dict[5]
    # 輸出:Python/<code>
    • Python字典通過檢查鍵值是否相等和比較哈希值來確定兩個鍵是否相同
    • 具有相同值的不可變對象在Python中始終具有相同的哈希值

    雖然5.0和5好像是不一樣,但實際上是一樣的,在python中是不存在整型和浮點型的,只有一個數值型

    <code>5 == 5.0
    # 輸出:True
    hash(5) == hash(5.0)
    # 輸出:True/<code>

    注意: 具有不同值的對象也可能具有相同的哈希值(哈希衝突)

    • 當執行 some_dict[5] = "Python" 語句時, 因為Python將5和5.0識別為some_dict 的同一個鍵, 所以已有值 "JavaScript" 就被 "Python" 覆蓋了.

    到處都返回

    <code>def some_func():
    try:
    return 'from_try'
    finally:
    return 'from_finally'
    some_func()
    # 始終輸出:from_finally/<code>

    這是一個非常嚴重的問題,而且也非常常見,也很長用到,需要格外的注意。在異常捕獲的時候,我們經常會用到finally來執行異常捕獲後必須執行的處理。但是return在很多語言當中表示跳出當前的執行模塊,但是在這裡就有些顛覆我們的認知了,所以必須重點關注。

  • 當在 "try...finally" 語句的 try 中執行 return, break 或 continue 後, finally 子句依然會執行.
  • 函數的返回值由最後執行的 return 語句決定. 由於 finally 子句一定會執行, 所以 finally 子句中的 return 將始終是最後執行的語句
  • 出人意料的is

    下面是一個在網上非常有名的例子.

    <code>a = 256
    b = 256
    a is b
    # 輸出:True

    a = 257
    b = 257
    a is b
    # 輸出:False

    a = 257; b = 257
    a is b
    # 輸出:True

    a, b = 257, 257
    a is b
    # 輸出:True/<code>

    1.我們要說一下is和==的區別

    • is 運算符檢查兩個運算對象是否引用自同一對象 (即, 它檢查兩個運算對象地址是否相同)
    • ==運算符比較兩個運算對象的值是否相等
    <code>a = 257
    b = 257
    a is b
    # 輸出:False
    a == b
    # 輸出:True/<code>

    2.為什麼256和257的結果不一樣?

    當你啟動Python的時候, 數值為-5到256 的對象就已經被分配好了. 這些數字因為經常被使用, 所以會被提前準備好。Python通過這種創建小整數池的方式來避免小整數頻繁的申請和銷燬內存空間,從而造成內存洩漏和碎片。

    3.當a和b在同一行中使用相同的值初始化時,會指向同一個對象.

    <code>a, b = 257, 257
    id(a)
    # 輸出:4391026960
    id(b)
    # 輸出:4391026960

    a = 257
    b = 257
    id(a)
    # 輸出:140232163575152
    id(b)
    # 輸出:140232163574768/<code>
    • 當 a 和 b 在同一行中被設置為 257 時, Python 解釋器會創建一個新對象, 然後同時引用第二個變量. 如果你在不同的行上進行, 它就不會 "知道" 已經存在一個 257 對象
    • 必須要注意的是這是一種特別為交互式環境做的編譯器優化. 當你在實時解釋器中輸入兩行的時候, 他們會單獨編譯, 因此也會單獨進行優化. 如果你在 .py 文件中嘗試這個例子, 則不會看到相同的行為, 因為文件是一次性編譯的,如果是運行py文件將得到不同的結果

    test.py

    <code>a, b = 257, 257
    print(id(a))
    print(id(b))
    # 輸出:
    /<code>

    列表複製

    <code>row = [""]*3
    # 並創建一個變量board
    board = [row]*3
    print(row)
    print(board)
    # 輸出:['', '', '']
    # 輸出:[['', '', ''], ['', '', ''], ['', '', '']]

    board[0][0] = 'X'
    print(board)
    # 輸出:[['X', '', ''], ['X', '', ''], ['X', '', '']]/<code>
    • 當我們初始化 row 變量時, 下面這張圖展示了內存中的情況。
    Python中反人類直覺的特性,你踩過坑嗎?

    • 而當通過對 row 做乘法來初始化 board 時, 內存中的情況則如下圖所示 (每個元素 board[0], board[1] 和 board[2] 都和 row 一樣引用了同一列表.)
    Python中反人類直覺的特性,你踩過坑嗎?

    • 我們可以通過不使用變量 row 生成 board 來避免這種情況
    <code>board = [['']*3 for _ in range(3)]
    board[0][0] = "X"
    board
    # 輸出:[['X', '', ''], ['', '', ''], ['', '', '']]/<code>

    這樣就會創建三個[''] * 3,而不是把[''] * 3標記三次

    閉包

    <code>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]
    print(results)
    print(funcs_results)
    # 輸出:[0, 1, 2, 3, 4, 5, 6]
    # 輸出:[6, 6, 6, 6, 6, 6, 6]/<code>

    即使每次在迭代中some_func中的x值都不相同,所有的函數還是都返回6.

    <code>powers_of_x = [lambda x: x**i for i in range(10)]
    [f(2) for f in powers_of_x]
    # 輸出:[512, 512, 512, 512, 512, 512, 512, 512, 512, 512]/<code>
  • 當在循環內部定義一個函數時, 如果該函數在其主體中使用了循環變量, 則閉包函數將與循環變量綁定, 而不是它的值.
    因此, 所有的函數都是使用最後分配給變量的值來進行計算的
  • 可以通過將循環變量作為命名變量傳遞給函數來獲得預期的結果. 為什麼這樣可行? 因為這會在函數內再次定義一個局部變量
  • <code>funcs = []
    for x in range(7):
    def some_func(x=x):
    return x
    funcs.append(some_func)
    funcs_results = [func() for func in funcs]
    print(funcs_results)
    # 輸出:[0, 1, 2, 3, 4, 5, 6]/<code>

    is not ... 不是 is (not ...)

    <code>'something' is not None
    # 輸出:True
    'something' is (not None)
    # 輸出:False/<code>
  • is not 是個單獨的二元運算符, 與分別使用 is 和 not 不同.
  • 如果操作符兩側的變量指向同一個對象, 則 is not 的結果為 False, 否則結果為 True.
  • 不存在的零點

    <code>from datetime import datetime

    midnight = datetime(2018, 1, 1, 0, 0)
    midnight_time = midnight.time()

    noon = datetime(2018, 1, 1, 12, 0)
    noon_time = noon.time()

    if midnight_time:
    print("Time at midnight is", midnight_time)

    if noon_time:
    print("Time at noon is", noon_time)
    # 輸出:Time at midnight is 00:00:00
    # 輸出:Time at noon is 12:00:00/<code>

    以上代碼如果是在python3.5之前的版本,只會輸出Time at noon is 12:00:00,在Python 3.5之前, 如果 datetime.time 對象存儲的UTC的午夜時間(譯: 就是 00:00), 那麼它的布爾值會被認為是 False. 當使用 if obj: 語句來檢查 obj 是否為 null 或者某些“空”值的時候, 很容易出錯.

    類屬性和實例屬性

    <code>class A:
    x = 1

    class B(A):
    pass

    class C(A):
    pass
    print(A.x, B.x, C.x)
    # 輸出:1 1 1

    B.x = 2
    print(A.x, B.x, C.x)
    # 輸出:1 2 1

    A.x = 3
    print(A.x, B.x, C.x)
    # 輸出:3 2 3

    a = A()
    print(a.x, A.x)
    # 輸出:3 3

    a.x += 1
    print(a.x, A.x)
    # 輸出:4 3\t/<code>
    <code>class SomeClass: 

    some_var = 15
    some_list = [5]
    another_list = [5]
    def __init__(self, x):
    self.some_var = x + 1
    self.some_list = self.some_list + [x]
    self.another_list += [x]

    some_obj = SomeClass(420)
    print(some_obj.some_list)

    print(some_obj.another_list)
    another_obj = SomeClass(111)
    print(another_obj.some_list)
    print(another_obj.another_list)
    print(another_obj.another_list is SomeClass.another_list)
    print(another_obj.another_list is some_obj.another_list)/<code>
    • 類變量和實例變量在內部是通過類對象的字典來處理. 如果在當前類的字典中找不到的話就去它的父類中尋找
    • += 運算符會在原地修改可變對象, 而不是創建新對象. 因此, 在這種情況下, 修改一個實例的屬性會影響其他實例和類屬性.

    從有到無

    <code>some_list = [1, 2, 3]
    some_dict = {
    "key_1": 1,
    "key_2": 2,
    "key_3": 3
    }

    some_list = some_list.append(4)
    some_dict = some_dict.update({"key_4": 4})
    print(some_list)
    print(some_dict)
    # 輸出:None

    # 輸出:None/<code>

    不知道有沒有人能一眼看出問題所在,這是一個寫法錯誤,並不是特殊用法。因為列表和字典的操作函數,比如list.append、list.extend、dict.update等都是原地修改變量,不創建也不返還新的變量

    子類繼承關係

    <code>from collections import Hashable
    print(issubclass(list, object))
    print(issubclass(object, Hashable))
    print(issubclass(list, Hashable))
    # 輸出:True
    # 輸出:True
    # 輸出:False/<code>

    子類關係是可以傳遞的,A是B的子類,B是C的子類,那麼A應該也是C的子類,但是在python中就不一定了,因為在python中使用__subclasscheck__函數進行判斷,而任何人都可以定義自己的__subclasscheck__函數

    • 當 issubclass(cls, Hashable) 被調用時, 它只是在 cls 中尋找 __hash__ 方法或者從繼承的父類中尋找 __hash__ 方法.
    • 由於 object is 可散列的(hashable), 但是 list 是不可散列的, 所以它打破了這種傳遞關係
    <code>class MyMetaClass(type):
    def __subclasscheck__(cls, subclass):
    print("Whateva, I do what I want!")
    import random
    return random.choice([True, False])


    class MyClass(metaclass=MyMetaClass):
    pass

    print(issubclass(list, MyClass))
    # 輸出:Whateva, I do what I want!
    # 輸出:True 或者 False 因為是隨機取的/<code>

    元類在python中是比較深入的知識點,後面我們有時間再講

    斗轉星移

    <code>import numpy as np

    def energy_send(x):
    # 初始化一個 numpy 數組
    np.array([float(x)])

    def energy_receive():
    # 返回一個空的 numpy 數組
    return np.empty((), dtype=np.float).tolist()

    energy_send(123.456)
    print(energy_receive())
    # 輸出:123.456/<code>

    這到底是無中生有還是斗轉星移呢?energy_receive函數我們返回了一個空的對象,但是結果是上一個數組的值,為什麼呢?

  • 在energy_send函數中創建的numpy數組並沒有返回, 因此內存空間被釋放並可以被重新分配.
  • numpy.empty()直接返回下一段空閒內存,而不重新初始化. 而這個內存點恰好就是剛剛釋放的那個但是這並不是絕對的.

  • 分享到:


    相關文章: