pymongo 查詢時,顯示循環不同,為何效率相差百倍?
剛接觸python和pymongo,在使用過程中發現一個奇怪的問題,想不通原因!先看代碼。
一.
for item in db.Account.find():
item
二.
data = db.Account.find()
for item in data:
item
結果, 第一個的執行時間是毫秒級甚至更快的,第二個的執行時間即便沒有數據,至少也要0.5秒以上才能完成。
在內部,他們都幹了什麼?為什麼第二個這麼慢,時間耗在了什麼地方?第二個比第一個應該只是多了一個內存申請,賦值給data的時候沒花時間,for循環的時候, 執行方式有何區別?好像問題都是重複的~ ====================================
以上問題的結論是錯誤的!得出問題中錯誤結論,是因為我將資料庫查詢進行了for循環,提取出結果給了一個list類型進行return,發現問題解決,就想當然的以為不能間接循環data~我將資料庫查詢寫成函數,封裝成包,由其他文件調用,函數直接將db.Account.find()賦值給data,之後直接把data return給調用方進行循環。由於我以為data是一個內存地址,或者是一個字典類型,可以直接這樣使用,其實不是,data是一個Cursor對象,導致調用方對data進行了其他非常複雜的操作。
我猜測,return出data之後,其他地方for循環data要0.5秒以上,可能是因為,data對象在資料庫查詢函數運行完之後,內存就被釋放掉了,在調用方操作data對象的時候,python進行了各種查證和恢複數據,所以,耗時0.5秒得出結果與pymongo的操作方式無關。 錯誤很好重現。
截至目前,問題和其他答案都是錯的。
如果你去看位元組碼的話,就知道兩種寫法其實沒多大差異:
import dis
import pymongo
client = pymongo.MongoClient()
db = client["test"]
def test1():
for item in db.Account.find():
continue
def test2():
data = db.Account.find()
for item in data:
continue
dis.dis(test1)
print "---"
dis.dis(test2)
輸出:
11 0 SETUP_LOOP 26 (to 29)
3 LOAD_GLOBAL 0 (db)
6 LOAD_ATTR 1 (Account)
9 LOAD_ATTR 2 (find)
12 CALL_FUNCTION 0
15 GET_ITER
&>&> 16 FOR_ITER 9 (to 28)
19 STORE_FAST 0 (item)
12 22 JUMP_ABSOLUTE 16
25 JUMP_ABSOLUTE 16
&>&> 28 POP_BLOCK
&>&> 29 LOAD_CONST 0 (None)
32 RETURN_VALUE
---
16 0 LOAD_GLOBAL 0 (db)
3 LOAD_ATTR 1 (Account)
6 LOAD_ATTR 2 (find)
9 CALL_FUNCTION 0
12 STORE_FAST 0 (data)
17 15 SETUP_LOOP 17 (to 35)
18 LOAD_FAST 0 (data)
21 GET_ITER
&>&> 22 FOR_ITER 9 (to 34)
25 STORE_FAST 1 (item)
18 28 JUMP_ABSOLUTE 22
31 JUMP_ABSOLUTE 22
&>&> 34 POP_BLOCK
&>&> 35 LOAD_CONST 0 (None)
38 RETURN_VALUE
後者因為多了個變數 data,所以需要多執行一次 STORE_FAST 和 LOAD_FAST。
其他差異則是前者在 SETUP_LOOP 之後才獲取到 db.Account.find,並執行它,但循環中並沒有多次獲取(16 ~ 28 行);後者是在 SETUP_LOOP 之前對 db.Account.find 求值的,循環中也不需要多次獲取 data(22 ~ 34 行)。至於 pymongo 的源碼,也沒用什麼黑魔法。db.Account.find() 會返回一個 Cursor 對象,遍歷時會調用它的 next 方法。這個方法在有數據時,popleft 第一個數據;否則會調用 _refresh 方法,從資料庫獲取數據。但在整個循環過程中,只有第一次和最後一次需要需要調用 _refresh 方法,且第二次會檢查到查詢已經完成,因此不再進行實際的資料庫查詢。
實測時也沒有顯著差異。@SimonS 的測試代碼是有問題的:MongoDB 是有緩存的,第二次查詢比第一次快是很正常的。如果把第一種方式執行兩次,再和第二種方式比較,你會發現結果也是差不多的。首先呢第二個並沒有額外申請內存,只不過是加了個名字綁定,這一點應該是一致的。
find返回一個Cursor,這是個可迭代對象,因此可以當作循環範圍。另外python的循環範圍不會每次循環都求一次,某答主需要加強學習。
所以關鍵在於第二個為什麼會比第一個慢。按你的說法要半秒以上,那麼只有可能是這個過程中發生了IO事件,因為內存複製和運算啥的半秒可以擼太多事情了,時間與現象不符。
推測大概是後者的cursor創建之後沒有使用導致鏈接關閉,在循環中又重新建立了資料庫連接。0.5秒上下算上網路延遲,看起來基本符合…同意@孫竟的答案。這個地方兩者是等價的。跟內存分配無關,跟for...in...iterator還是list也無關。題主已經承認是其他地方出錯了。
for item in db.Account.find():
每次循環都 .... 請自己補完。
真誠感激熱心的朋友百忙中抽出時間解答,經過再三查證,確實是我判斷錯了地方。效率低是在編碼中傳值不當造成的。
推薦閱讀:
※abaqus的二次開發為何用python語言?
※看完了廖雪峰的Python教程 ,只學會了初步,接下來怎麼學習?
※用Python写了个函数,解决酒瓶换酒的问题,求大牛们指点?(问题已解决,感谢各位!)
※Python 如何列印出中文字元?
※三十歲從電氣轉行 IT 是否可行?