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>
為什麼會出現以上的現象呢?因為編譯器的優化特性(很多語言的不同編譯器都有相應的優化策略),對於不可變對象,在某些情況下並不會創建新的對象,而是會嘗試使用已存在的對象,從而節省內存,可以稱之為**字符串駐留**。字符串的駐留是隱式的,不受我們控制,但是我們可以根據一些規律來猜測是否發生字符串駐留:
**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在很多語言當中表示跳出當前的執行模塊,但是在這裡就有些顛覆我們的認知了,所以必須重點關注。
出人意料的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 變量時, 下面這張圖展示了內存中的情況。
- 而當通過對 row 做乘法來初始化 board 時, 內存中的情況則如下圖所示 (每個元素 board[0], board[1] 和 board[2] 都和 row 一樣引用了同一列表.)
- 我們可以通過不使用變量 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>
不存在的零點
<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函數我們返回了一個空的對象,但是結果是上一個數組的值,為什麼呢?
閱讀更多 Python集結號 的文章