無論看哪個文件,無論翻開哪本書籍,談到Python的檔案處理,一律就是介紹open函式的使用,再搭配一個開檔模式列表。
承認吧!每隔一段時間沒用,你就總是得再次確認那些r、w、x、a、+等模式,到底代表哪些操作。
若厭倦了老是重複查閱、記憶相關文件的這個過程,何妨來探索一下open函式的背後到底是怎麼一回事呢?
初看Python的open函式
檔案處理在程式設計中是很基本的需求,在Python中無論要讀取、寫入、更新檔案或處理二進位資料,基本上只要運用open函式,其中最常使用的就是file與mode兩個參數,最多就是在處理文字檔案,而檔案編碼與作業系統預設編碼(locale.getpreferredencoding()傳回值)不同時,多指定個encoding參數。
看了一篇又一篇的文件之後,發現裡面都是如此介紹就結束了,這讓我覺得有些不安,檔案處理真的只有這麼簡單嗎?
回頭看看我熟悉的Java,單是古老的I/O串流設計,除了InputStream、OutputStream各自的子類別外,還有著各種的裝飾器(Decorator),像是BufferedInputStream、BufferedOutputStream等,較新的I/O處理,則有NIO、NIO2等,那麼Python中有類似的東西嗎?
當搜尋到的文件或可查閱的書籍無法滿足疑問時,耐著性子查閱無趣的官方文件總是正確的決定。實際上在Python 3中,open函式共有file、mode、buffering、encoding、errors、newline、closefd、opener八個參數,雖然除了file是必要的參數之外,其他都有預設值,然而對照Python的簡明風格來說,一個函式會有這麼多的參數,依舊顯得很不尋常。
如果曾經試著使用type函式,測試在不同參數下open函式的傳回物件型態,那麼就會稍微有點答案。使用r、w、a時,預設會以文字模式讀取,open函式傳回的是_io.TextIOWrapper;若加上b模式,會以二進位方式讀取,open函式會傳回_io.BufferedReader(rb模式),或者_io.BufferedWriter(wb模式);如果再加上+模式的話,傳回的會是_io.BufferedRandom;如果將buffering設為0,那麼會傳回_io.FileIO。
顯然地,open函式是作為一個工廠(Factory)函式,在指定不同的參數值下,隱藏了不同型態的實例建構的細節,直接傳回建構好的物件。由於Python中可以指定預設引數,然而,作用不見得是真的要指定什麼預設值,有時是要在動態定型語言中模擬出重載的特性,例如,對於預設引數被設定為None的情況,經常是此作用。
若將指定不同模式或參數值的open函式,看成像是靜態定型語言中重載出來的不同open函式,那麼open函式會有這麼多參數,基本上,就可以理解為數個工廠函式重載的集合體了。
檔案物件繼承架構
無論open傳回的物件是何種型態,在Python的官方文件中給了它們一個名稱:file object。因為,無論底層實際上是連接至磁碟檔案、記憶體,或者是網路上某個資源,都可以透過file object公開的檔案導向(file-oriented)API,像是使用read、write等方法進行存取。
因此對於常見的需求,使用open工廠函式搭配file object,開發者讀寫檔案的程式碼流程也就幾乎大同小異,無怪乎許多文件或書籍都只介紹open函式怎麼使用。然而,在需要進一步細部操作檔案時,就得知道file object在Python中,基本上分為三個大類:文字檔案、緩衝二進位(buffered binary)檔案與原始二進位檔案(raw binary)。
這三個大類的對應型態,分別是_io模組中的TextIOBase、BufferedIOBase與RawIOBase,它們都繼承了IOBase,而函式readline、readlines、writelines、close等,就是定義在IOBase中。不過IOBase並沒有定義read、write,而是定義在各自的子類別之中,因為實際上這要依TextIOBase、BufferedIOBase與RawIOBase不同的存取模式,而有不同的簽署定義。
方才看到的TextIOWrapper,實際上是TextIOBase的子類別,FileIO則是RawIOBase的子類別,BufferedReader、BufferedWriter都是BufferedIOBase的子類別,而BufferedRandom同時繼承了BufferedReader、BufferedWriter。
雖然_io是Python的內建模組,然而,可以察看io模組的原始碼,會發現它只是在做名稱空間管理,從內建模組_io中from import(匯入)了TextIOWrapper、BufferedReader等名稱,因此,相關的類別說明,也就可以在io模組的說明文件中一探究竟。
回到open工廠函式
知道了open傳回的file object,實際上會是什麼樣的繼承架構之後,對於open函式的魔法,就比較能知道其底細了。例如說,open(r'c:\workspace\test.py')的話,底層大概就是執行f = io.TextIOWrapper(io.FileIO(r'c:\workspace\test.py'))並將f參考的實例傳回,若直接執行f.readlines(),也就可以讀取test.py的內容。
然而,相對於方才的程式碼來說,使用open函式還是簡明多了,這讓我想起在Stack Overflow上有人問到,Java中有沒有python-like的IO程式庫(http://goo.gl/kqGrYu),其實,可以有類似以下的風格:
File f = Open('file.txt', 'w') for(String line:f){ //do something with the line from file }
當時Java 7還沒有出現,不然的話,Java 7的NIO2中,確實有個Files類別提供了類似的功能,例如搭配lambda語法的話,可以撰寫成這樣的形式:
Files.lines(Paths.get(args[0])).forEach(line -> { //do something with the line from file });
探索工廠的複雜度
實際上,在Java的NIO2中,也還有像Files.newBufferedReader這類的方法存在,用以取代過去面臨的一種情況——想取得一個BufferedReader實例時,我們往往必須使用new BufferedReader(new InputStreamSReader(new FileInputStream("..."))。而現在,當你習慣了又臭又長的語法,看到這類需求被封裝為工廠方法,並列入標準API,無疑是件令人高興的事。
然而,工廠畢竟是工廠,充其量只是代勞一些常見的物件製作過程,直接給你最後的成品,當你必須要掌握更多細節的時候,就有必要探索工廠背後的運作機制,瞭解到建立各個物件時的複雜度。
那麼,暫時要你忘了open函式,情況會如何呢?你有辦法在不使用open函式下,自行建立open指定了r、w、x、a、+等模式下,原本會傳回的物件嗎?
這會是個有趣的嘗試,過程中,你除了能夠更加瞭解Python的IO設計方式之外,也能反過來更加認識open函式上各參數之作用,不用只是死背那些r、w、x、a、+等模式,對於工廠交給你的物件,也就不會再覺得疑慮而不安了。