標籤:

Python3 Buffered I/O淺析

f = open("apache.log", "r+")nprint(f.tell())nf.read(1)nprint(f.tell())nf.write("Love")nf.close()n

控制太輸出的結果是0,1

文件操作後的結果是

Hello World!!!Loven

問題:代碼讀了一個字元,指針應該到第二個字元的位置,為什麼寫內容還是被追加到了文件的最後?

太長不看版結論:Python3之後的New I/O行為,實現了bufferd I/O ,導致了這個看似奇怪的行為

看到這個問題,首先試了試Python2.7 和Python3,發現Python3的open的行為和Python2確實是有所區別的。

f = open("apache.log", "r+")nprint(f.tell())nf.read(1)nprint(f.tell())nf.write("Love")n#<-在這裡加一句f.tell()nf.close()n

在write後,f.tell()已經是原來的文件長+新寫入的字元長了

這個問題在文件操作中,也就是APUE里的一個普通的seek問題,我們可以看看為什麼在Python3、在哪裡導致的這樣的問題

在Python3中,open實際上是io標準庫里的io.open:

16.2. io - Core tools for working with streams - Python 3.5.2 documentation

io.open(file, mode=r, buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)nThis is an alias for the builtin open() function.n

而不再是Python2里的內建函數了

open返回的是:

In [2]: fnOut[2]: <_io.TextIOWrapper name=test.txt mode=r+ encoding=UTF-8>n

下劃線開頭的這個_io是一個c模塊:

In [4]: _io?nType: modulenString form: <module io (built-in)>nDocstring:nThe io module provides the Python interfaces to stream handling. Thenbuiltin open function is defined in this module.n

它是用來提供流處理的介面的,繼承關係什麼的在上面的鏈接里有說。

看了下Python3.5的代碼,TextIOWrapper的write實現在textio.c:_io_TextIOWrapper_write_impl這個函數里

看了一下PEP 3116 -- New I/O,和裡面的代碼,確定導致這個問題的邏輯核心是:

if (self->pending_bytes_count > self->chunk_size || needflush ||n text_needflush) {n if (_textiowrapper_writeflush(self) < 0)n return NULL;n }nn if (needflush) {n ret = PyObject_CallMethodObjArgs(self->buffer, _PyIO_str_flush, NULL);n if (ret == NULL)n return NULL;n Py_DECREF(ret);n }n

這篇PEP設計了一個三層的Python3 IO實現:raw層、Buffer層、Text層,我們通常直接用的Text層。告訴我們,打開一個流對象後(例如Open),這個對象會有一個buffer屬性。作為緩衝層對應的對象。

上面貼的代碼描述了當write調用時,實際上調用的是self->buffer的寫相關的方法。

我們可以實驗一下:

$ cat test.txt nhello worldnn# zhutou @ Phoenix in ~ [1:19:01] n$ cat test.txt|wcn 1 2 12n

In [1]: f = open("test.txt","r+")nOut[1]: <_io.TextIOWrapper name=test.txt mode=r+ encoding=UTF-8>nnIn [2]: f.read(1)nOut[2]: hnnIn [3]: f.buffernOut[3]: <_io.BufferedRandom name=test.txt>nnIn [4]: f.buffer.tell()nOut[4]: 12n

可以看到,雖然我們只讀了一個字元,但實際上這個文件對應的buffer對象,指針已經指到了這個文件的末尾,所以當調用write方法時,對這個buffer進行寫也就寫到了文件末尾。在常見的操作系統上,這個buffer預取的長度是8k,以減少磁碟的隨機讀取,增加磁碟的順序讀取

_textiowrapper_writeflush的實現核心如下,可以清晰地看到這個對應的邏輯

static intn_textiowrapper_writeflush(textio *self)n{n PyObject *pending, *b, *ret;n pending = self->pending_bytes;n b = _PyBytes_Join(_PyIO_empty_bytes, pending);nn do {n ret = PyObject_CallMethodObjArgs(self->buffer,n _PyIO_str_write, b, NULL);n } while (ret == NULL && _PyIO_trap_eintr());n return 0;n}n

這一點在Python中可以得到驗證:

In [5]: f.buffer.seek(1)nOut[5]: 1nnIn [6]: f.write("Love")nOut[6]: 4nnIn [7]: f.close()n

現在符合直覺了,文件內容已經是hLove world 了~

由此得到了開頭的結論:Python3和Python2的文件行為不一致是文件buffer造成的,buffered IO本身是為了解決讀取文件的效率問題。(PEP中說是為了和Java看齊。。俺不懂什麼java,不知道java會不會有這個問題。。

The new I/O spec is intended to be similar to the Java I/O libraries, but generally less confusing

然後還更加優越2333

在io.TextIOWrapper類中,我們可以構造時指定write_through=True 避免寫的時候經過buffer

但我找了一下open函數的文檔和對應的實現,沒有找到改這個參數的地方~


推薦閱讀:

Python入門進階推薦書單
Python實踐20-閉包簡介

TAG:Python |