標籤:

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 是否可行?

TAG:Python | mongo |