給Python學習者的檔案讀寫指南(含基礎與進階,建議收藏)

機器之心發表於2018-10-11

對於初學者來說,一份詳盡又清晰明白的指南很重要。今天,貓貓跟大家一起,好好學習Python檔案讀寫的內容,這部分內容特別常用,掌握後對工作和實戰都大有益處。學習是循序漸進的過程,欲速則不達。文章較長,建議大家收藏,以備複習查閱哦。

1、如何將列表資料寫入檔案?
2、如何從檔案中讀取內容?
3、多樣需求的讀寫任務
4、從with語句到上下文管理器

如何將列表資料寫入檔案?

首先,我們來看看下面這段程式碼,並思考:這段程式碼有沒有問題,如果有問題的話,要怎麼改?

li = ['python',' is',' a',' cat']
with open('test.txt','w') as f:
    f.write(li)

現在公佈答案,這段程式碼會報錯:

TypeError  Traceback (most recent call last)
<ipython-input-6-57e0c2f5a453> in <module>()
      1 with open('test.txt','w') as f:
----> 2     f.write(li)

TypeError: write() argument must be str, not list

以上程式碼的想法是將list列表內容寫入txt檔案中,但是報錯 TypeError: write() argument must be str。就是說,write()方法必須接受字串(str)型別的引數

Python中內建了str()方法,可以返回字串版本的物件(Return a string version of object)。所以,上面的例子中,我們試試把 f.write(li) 改為 f.write(str(li)) ,先做一下字串型別的轉化看看。程式碼略。

這次沒有報錯了,但是開啟檔案就傻眼了吧,寫入的內容是“['python',' is',' a',' cat']”。怎麼才能寫成“python is a cat”呢?

檔案寫操作還有一個writelines()方法,它接收的引數是由字串組成的序列(sequence),實際寫入的效果是將全部字串拼接在一起。字串本身也是一種序列,所以當引數是字串的時候,writelines()方法等價於write()。

# 以下3種寫法等價,都是寫入字串“python is a cat”
In [20]:  with open('test.txt','w') as f:
    ...:      f.writelines(['python',' is',' a',' cat'])
    ...:      f.writelines('python is a cat')
    ...:      f.write('python is a cat')

# 以下2種寫法等價,都是寫入列表的字串版本“['python',' is',' a',' cat']”
In [21]:  with open('test.txt','w') as f:
    ...:      f.write(str(['python',' is',' a',' cat']))
    ...:      f.writelines(str(['python',' is',' a',' cat']))

# 作為反例,以下寫法都是錯誤的:
In [22]:  with open('test.txt','w') as f:
    ...:      f.writelines([2018,'is','a','cat']) # 含非字串
    ...:      f.write(['python','is','a','cat']) # 非字串

由上可知,當多段分散的字串存在於列表中的時候,要用writelines()方法,如果字串是一整段,那直接使用write()方法。如果要以整個列表的形式寫入檔案,就使用str()方法做下轉化。

這個問題還沒結束,如果列表中就是有元素不是字串,而且要把全部元素取出來,怎麼辦呢?

那就不能直接使用write()和writelines()了,需要先用for迴圈,把每個元素取出來,逐一str()處理。

In [37]: content=[1,' is',' everything']
In [38]: with open('test.txt','w') as f:
    ...:     for i in content:
    ...:         f.write(str(i))

需要注意的是,writelines()不會自動換行。如果要實現列表元素間的換行,一個辦法是在每個元素後面加上換行符“\n”,如果不想改變元素,最好是用for迴圈,在寫入的時候加在末尾:for i in content:  f.writelines(str(i)+“\n”).

引申一下,經過實驗,數字及元祖型別也可以作為write()的引數,不需轉化。但是dict字典型別不可以,需要先用str()處理一下。字典型別比較特殊,最好是用json.dump()方法寫到檔案,具體操作方法以及注意事項,請看喵喵之前發的《假期玩得開心也不忘充電,學習Python操作JSON,網路資料交換不用愁》.

總結一下,write()接收字串引數,適用於一次性將全部內容寫入檔案;writelines()接收引數是由字串組成的序列,適用於將列表內容逐行寫入檔案。str()返回Python物件的字串版本,使用需注意。

如何從檔案中讀取內容?

從檔案中讀取內容有如下方法:

file.read([size])
從檔案讀取指定的位元組數,如果未給定或為負則讀取所有。

file.readline([size])
讀取整行,包括 "\n" 字元。

file.readlines([sizeint])
讀取所有行並返回列表,若給定sizeint>0,則是設定一次讀多少位元組,這是為了減輕讀取壓力。

簡而言之,在不傳引數的情況下,read()對應write(),讀取全部內容;readlines()對應writelines(),讀取全部內容(含換行符)並以列表形式返回,每個換行的內容作為列表的一個元素。

In [47]: with open('test.txt','r') as f:
    ...:     print(f.read())
1 is everything.
python is a cat.
this is the end.

In [48]: with open('test.txt','r') as f:
    ...:     print(f.readlines())
['1 is everything.\n', 'python is a cat.\n', 'this is the end.']

但是,以上兩個方法有個缺點,當檔案過大的時候,一次性讀取太多內容,會對記憶體造成極大壓力。讀操作還有一個readline()方法,可以逐行讀取。

In [49]: with open('test.txt','r') as f:
    ...:     print(f.readline())
1 is everything.

readline()讀取第一行就返回,再次呼叫f.readline(),會讀取下一行。

喵喵,是否感覺跟《超強彙總:學習Python列表,只需這篇文章就夠了》學習過的生成器很像,需要不停呼叫next()獲取下一行。

這麼看來,readline()太笨拙了。那麼,有什麼辦法可以優雅地讀取檔案內容呢?

回過頭來看readlines()方法,它返回的是一個列表。這不奇怪麼,好端端的內容為啥要返回成列表呢?

再想想writelines()方法,把字串列表寫入檔案正是這傢伙乾的事,readlines()方法恰恰是它的逆操作!而writelines()方法要配合for迴圈,所以我們把readlines()與for迴圈結合,看看會怎樣。

In [61]: with open('test.txt','r') as f:
    ...:     for line in f.readlines():
    ...:         print(line)
1 is everything.

python is a cat.

this is the end.

# 讀取內容包含換行符,所以要strip()去掉換行符
In [62]: with open('test.txt','r') as f:
    ...:     for line in f.readlines():
    ...:         print(line.strip())
1 is everything.
python is a cat.
this is the end.

總結一下,readline()比較雞肋,不咋用;read()適合讀取內容較少的情況,或者是需要一次性處理全部內容的情況;而readlines()用的較多,比較靈活,因為for迴圈是一種迭代器,每次載入部分內容,既減少記憶體壓力,又方便逐行對資料處理。

多樣需求的讀寫任務

前兩部分講了檔案讀寫的幾大核心方法,它們能夠起作用的前提就是,需要先開啟一個檔案物件,因為只有在檔案運算子的基礎上才可以進行讀或者寫的操作。

開啟檔案用的是open()方法,所以我們再繼續講講這個方法。open() 方法用於開啟一個檔案,並返回檔案物件,在對檔案進行處理過程都需要使用到這個函式,如果該檔案無法被開啟,會丟擲 OSError。

open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)

open()方法的引數裡file(檔案)是必需的,其它引數最常用的是mode(模式)和encoding(編碼)。

先說說encoding,一般來說,開啟檔案的編碼方式以作業系統的預設編碼為準,中文可能會出現亂碼,需要加encoding='utf-8'。

In [63]: with open('test.txt','r') as f:
    ...:     for line in f.readlines():
    ...:         print(line.strip())
-----------------------
UnicodeDecodeError     Traceback (most recent call last)
<ipython-input-63-731a4f9cf707> in <module>()
      1 with open('test.txt','r') as f:
----> 2     for line in f.readlines():
      3         print(line.strip())
UnicodeDecodeError: 'gbk' codec can't decode byte 0xa4 in position 26: illegal multibyte sequence

In [65]: with open('test.txt','r',encoding='utf-8') as f:
    ...:     for line in f.readlines():
    ...:         print(line.strip())
愛貓貓
python is a cat.

再說mode,它指定檔案開啟的模式。

'r': 以只讀模式開啟(預設模式,必須保證檔案存在)
'w':以只寫模式開啟。若檔案存在,則清空檔案,然後重新建立;若不存在,則新建
'a':以追加模式開啟。若檔案存在,則會追加到檔案的末尾;若檔案不存在,則新建

常見的mode組合
'r'或'rt':   預設模式,文字讀模式
'w'或'wt':以文字寫模式開啟(開啟前檔案被清空)
'rb':       以二進位制讀模式開啟
'ab':      以二進位制追加模式開啟
'wb':      以二進位制寫模式開啟(開啟前檔案被清空)
'r+':       以文字讀寫模式開啟,預設寫的指標開始指在檔案開頭, 因此會覆寫檔案
'w+':      以文字讀寫模式開啟(開啟前檔案被清空)
'a+':      以文字讀寫模式開啟(只能寫在檔案末尾)
'rb+':     以二進位制讀寫模式開啟
'wb+':   以二進位制讀寫模式開啟(開啟前被清空)
'ab+':    以二進位制讀寫模式開啟

喵喵,初看起來,模式很多,但是,它們只是相互組合罷了。建議記住最基本的w、r、a,遇到特殊場景,再翻看一下就好了。

從with語句到上下文管理器

基礎部分講完了,下面是進階部分。知其然,更要知其所以然。

1、with語句是初學者必會常識

首先,要解釋一下為啥前文直接就用了with語句。with語句是讀寫檔案時的優雅寫法,這已經預設是Python初學者必會的常識了。如果你還不會,先看看用和不用with語句的對比:

# 不用with語句的正確寫法
try:
    f = open('test.txt','w')
    f.writelines(['python',' is',' a',' cat'])
finally:
    if f:
        f.close()

# 使用with語句的正確寫法
with open('test.txt','w') as f:
    f.writelines(['python',' is',' a',' cat'])

因為檔案物件會佔用作業系統的資源,並且作業系統同一時間能開啟的檔案數量是有限的,所以open()方法之後一定要呼叫close()方法。另外,讀寫操作可能出現IO異常的情況,所以要加try…finally,保證無論如何,都會呼叫到close()方法。

這樣寫萬無一失,但是實在繁瑣,一不小心還可能漏寫或者寫錯。而with語句會保證呼叫close(),只需一行程式碼,簡直不要太優雅!所以,with語句是Python初學者必會技能。

2、什麼是上下文管理器?

下面,重頭戲來了,什麼是上下文管理器(context manager)?

上下文管理器是這樣一個物件:它定義程式執行時需要建立的上下文,處理程式的進入和退出,實現了上下文管理協議,即在物件中定義了 __enter__() 和 __exit__() 方法。


__enter__():進入執行時的上下文,返回執行時上下文相關的物件,with 語句中會將這個返回值繫結到目標物件。 


__exit__(exception_type, exception_value, traceback):退出執行時的上下文,定義在塊執行(或終止)之後上下文管理器應該做什麼。它可以處理異常、清理現場或者處理 with 塊中語句執行完成之後需要處理的動作。

注意enter和exit的前後有兩個下劃線,Python中自帶了很多類似的方法,它們是很神秘又很強大的存在,江湖人常常稱其為“黑魔法”。例如,迭代器協議就實現了__iter__方法。

在Python的內建型別中,很多型別都是支援上下文管理協議的,例如file,thread.LockType,threading.Lock等等。

上下文管理器無法獨立使用,它們要與with相結合,with語句可以在程式碼塊執行前進入一個執行時上下文(執行_enter_方法),並在程式碼塊結束後退出該上下文(執行__exit__方法)。

with 語句適用於對資源進行訪問的場合,確保不管使用過程中是否發生異常都會執行必要的“清理”操作,釋放資源,比如檔案使用後自動關閉、執行緒中鎖的自動獲取和釋放等。

3、自定義上下文管理器

除了Python的內建型別,任何人都可以定義自己的上下文管理器。下面是一個示例:

class OpenFile(object):
    def __init__(self,filename,mode):
        self.filename=filename
        self.mode=mode
    def __enter__(self):
        self.f=open(self.filename,self.mode)
        self.f.write("enter now\n")
        return self.f  #作為as說明符指定的變數的值
    def __exit__(self,type,value,tb):
        self.f.write("exit now")
        self.f.close()
        return False   #異常會被傳遞出上下文
with OpenFile('test.txt','w') as f:
    f.write('Hello World!\n')

最終寫入檔案的結果是:

enter now
Hello World!
exit now

上下文管理器必須同時提供 __enter__() 和 _exit_() 方法的定義,缺少任何一個都會導致 AttributeError。 

上下文管理器在執行過程中可能會出現異常,_exit_() 的返回值會決定異常的處理方式:返回值等於 False,那麼這個異常將被重新丟擲到上層;返回值等於 True,那麼這個異常就被忽略,繼續執行後面的程式碼。__exit()__ 有三個引數(exception_type, exception_value, traceback),即是異常的相關資訊。

4、contextlib實現上下文管理器

上例中,自定義上下文管理器的寫法還是挺繁瑣的,而且只能用於類級別。為了更好地輔助上下文管理,Python 內建提供了 contextlib 模組,進而可以很方便地實現函式級別的上下文管理器。

該模組本質上是透過裝飾器(decorators)和生成器(generators)來實現上下文管理器,可以直接作用於函式/物件,而不用去關心 __enter__() 和 __exit()__ 方法的具體實現。

先把上面的例子改造一下,然後我們再對照著解釋:

from contextlib import contextmanager

@contextmanager
def open_file(name):
    ff = open(name, 'w')
    ff.write("enter now\n")
    try:
        yield ff
    except RuntimeError:
        pass
    ff.write("exit now")
    ff.close()

with open_file('test.txt') as f:
    f.write('Hello World!\n')

contextmanager是要使用的裝飾器,yield關鍵字將普通的函式變成了生成器。yield的返回值(ff)等於上例__enter__()的返回值,也就是as語句的值(f),而yield前後的內容,分別是_enter_() 和 _exit_() 方法裡的內容。

使用contextlib,可以避免類定義、_enter_() 和 __exit()__方法,但是需要我們捕捉可能的異常(例如,yield只能返回一個值,否則會導致異常 RuntimeError),所以try…except語句不能忽略。

喵喵喵,今天的分享就到這啦。看官們,覺得有用的話,分享給其他同樣好學的胖友們吧~~~~

本文經授權轉載自Python貓(ID:python_cat)。

原文連結:https://mp.weixin.qq.com/s/Md07VoaULda7qnMO4ob7Ww

相關文章