你的位置: Home ‣ Dive Into Python 3 ‣
難度等級: ♦♦♦♢♢
❝ A nine mile walk is no joke, especially in the rain.
❞
— Harry Kemelman, The Nine Mile Walk
在沒有安裝任何一個應用程序之前,我的筆記本上Windows系統有38,493個文件。安裝Python 3后,大約增加了3,000個文件。文件是每一個主流操作系統的主要存儲模型;這種觀念如此根深蒂固以至于難以想出一種替代物。打個比方,你的電腦實際上就是泡在文件里了。
在讀取文件之前,你需要先打開它。在Python里打開一個文件很簡單:
a_file = open('examples/chinese.txt', encoding='utf-8')
Python有一個內置函數 open(),它使用一個文件名作為其參數。在以上代碼中,文件名是 'examples/chinese.txt'。關于這個文件名,有五件值得一講的事情:
open()只使用一個參數。在Python里,當你使用“filename,”作為參數的時候,你可以將部分或者全部的路徑也包括進去。
但是對open()函數的調用不局限于filename。還有另外一個叫做encoding參數。天哪,似乎非常耳熟的樣子!
字節即字節;字符是一種抽象。字符串由使用Unicode編碼的字符序列構成。但是磁盤上的文件不是Unicode編碼的字符序列。文件是字節序列。所以你可能會想,如果從磁盤上讀取一個“文本文件”,Python是怎樣把那個字節序列轉化為字符序列的呢?實際上,它是根據特定的字符解碼算法來解釋這些字節序列,然后返回一串使用Unicode編碼的字符(或者也稱為字符串)。
# This example was created on Windows. Other platforms may
# behave differently, for reasons outlined below.
# 這個樣例在Windows平臺上創建。其他平臺可能會有不同的表現,理由描述在下邊
>>> file = open('examples/chinese.txt')
>>> a_string = file.read()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Python31\lib\encodings\cp1252.py", line 23, in decode
return codecs.charmap_decode(input,self.errors,decoding_table)[0]
UnicodeDecodeError: 'charmap' codec can't decode byte 0x8f in position 28: character maps to <undefined>
>>>
剛才發生了什么?由于你沒有指定字符編碼的方式,所以Python被迫使用默認的編碼。那么默認的編碼方式是什么呢?如果你仔細看了跟蹤信息(traceback),錯誤出現在cp1252.py,這意味著Python此時正在使用CP-1252作為默認的編碼方式。(在運行微軟視窗操作系統的機器上,CP-1252是一種常用的編碼方式。)CP-1252的字符集不支持這個文件上的字符編碼,所以它以這個可惡的UnicodeDecodeError錯誤讀取失敗。
但是,還有更糟糕的!因為默認的編碼方式是平臺相關的(platform-dependent),所以,當前的代碼也許能夠在你的電腦上運行(如果你的機器的默認編碼方式是UTF-8),但是當你把這份代碼分發給其他人的時候可能就會失敗(因為他們的默認編碼方式可能跟你的不一樣,比如說CP-1252)。
☞如果你需要獲得默認編碼的信息,則導入
locale模塊,然后調用locale.getpreferredencoding()。在我安裝了Windows的筆記本上,它的返回值是'cp1252',但是在我樓上安裝了Linux的臺式機上邊,它返回'UTF8'。你看,即使在我自己家里我都不能保證一致性(consistency)!你的運行結果也許不一樣(即使在Windows平臺上),這依賴于操作系統的版本和區域/語言選項的設置。這就是為什么每次打開一個文件的時候指定編碼方式是如此重要了。
到目前為止,我們都知道Python有一個內置的函數叫做open()。open()函數返回一個流對象(stream object),它擁有一些用來獲取信息和操作字符流的方法和屬性。
>>> a_file = open('examples/chinese.txt', encoding='utf-8')
>>> a_file.name ①
'examples/chinese.txt'
>>> a_file.encoding ②
'utf-8'
>>> a_file.mode ③
'r'
name屬性反映的是當你打開文件時傳遞給open()函數的文件名。它沒有被標準化(normalize)成絕對路徑。
encoding屬性反映的是在你調用open()函數時指定的編碼方式。如果你在打開文件的時候沒有指定編碼方式(不好的開發人員!),那么encoding屬性反映的是locale.getpreferredencoding()的返回值。
mode屬性會告訴你被打開文件的訪問模式。你可以傳遞一個可選的mode參數給open()函數。如果在打開文件的時候沒有指定訪問模式,Python默認設置模式為'r',意思是“在文本模式下以只讀的方式打開。”在這章的后面你會看到,文件的訪問模式有各種用途;不同模式能夠使你寫入一個文件,追加到一個文件,或者以二進制模式打開一個文件(在這種情況下,你處理的是字節,不再是字符)。
☞
open()函數的文檔列出了所有可用的文件訪問模式。
在打開文件以后,你可能想要從某處開始讀取它。
>>> a_file = open('examples/chinese.txt', encoding='utf-8')
>>> a_file.read() ①
'Dive Into Python 是為有經驗的程序員編寫的一本 Python 書。\n'
>>> a_file.read() ②
''
read()方法即可以讀取它。返回的結果是文件的一個字符串表示。
如果想要重新讀取文件呢?
# continued from the previous example # 接著前一個例子 >>> a_file.read() ① '' >>> a_file.seek(0) ② 0 >>> a_file.read(16) ③ 'Dive Into Python' >>> a_file.read(1) ④ ' ' >>> a_file.read(1) '是' >>> a_file.tell() ⑤ 20
read()方法只會返回一個空字符串。
seek()方法使定位到文件中的特定字節。
read()方法可以使用一個可選的參數,即所要讀取的字符個數。
我們再來做一遍。
# continued from the previous example # 繼續上一示例 >>> a_file.seek(17) ① 17 >>> a_file.read(1) ② '是' >>> a_file.tell() ③ 20
你是否已經注意到了?seek()和tell()方法總是以字節的方式計數,但是,由于你是以文本文件的方式打開的,read()方法以字符的個數計數。中文字符的UTF-8編碼需要多個字節。而文件里的英文字符每一個只需要一個字節來存儲,所以你可能會產生這樣的誤解:seek()和read()方法對相同的目標計數。而實際上,只有對部分字符的情況是這樣的。
但是,還有更糟的!
>>> a_file.seek(18) ① 18 >>> a_file.read(1) ② Traceback (most recent call last): File "<pyshell#12>", line 1, in <module> a_file.read(1) File "C:\Python31\lib\codecs.py", line 300, in decode (result, consumed) = self._buffer_decode(data, self.errors, final) UnicodeDecodeError: 'utf8' codec can't decode byte 0x98 in position 0: unexpected code byte
UnicodeDecodeError錯誤失敗。
打開文件會占用系統資源,根據文件的打開模式不同,其他的程序也許不能夠訪問它們。當已經完成了對文件的操作后就立即關閉它們,這很重要。
# continued from the previous example # 繼續前面的例子 >>> a_file.close()
然而,這還不夠(anticlimactic)。
流對象a_file仍然存在;調用close()方法并沒有把對象本身銷毀。所以這并不是非常有效。
# continued from the previous example # 接著上一示例 >>> a_file.read() ① Traceback (most recent call last): File "<pyshell#24>", line 1, in <module> a_file.read() ValueError: I/O operation on closed file. >>> a_file.seek(0) ② Traceback (most recent call last): File "<pyshell#25>", line 1, in <module> a_file.seek(0) ValueError: I/O operation on closed file. >>> a_file.tell() ③ Traceback (most recent call last): File "<pyshell#26>", line 1, in <module> a_file.tell() ValueError: I/O operation on closed file. >>> a_file.close() ④ >>> a_file.closed ⑤ True
IOError異常。
tell()也會失敗。
close()方法并沒有引發異常。其實那只是一個空操作(no-op)而已。
closed用來確認文件是否已經被關閉了。
流對象有一個顯式的close()方法,但是如果代碼有缺陷,在調用close()方法以前就崩潰了呢?理論上,那個文件會在相當長的一段時間內一直打開著,這是沒有必要地。當你在自己的機器上調試的時候,這不算什么大問題。但是當這種代碼被移植到服務器上運行,也許就得三思了。
對于這種情況,Python 2有一種解決辦法:try..finally塊。這種方法在Python 3里仍然有效,也許你可以在其他人的代碼,或者從比較老的被移植到Python 3的代碼中看到它。但是Python 2.5引入了一種更加簡潔的解決方案,并且Python 3將它作為首選方案:with語句。
with open('examples/chinese.txt', encoding='utf-8') as a_file:
a_file.seek(17)
a_character = a_file.read(1)
print(a_character)
這段代碼調用了open()函數,但是它卻一直沒有調用a_file.close()。with語句引出一個代碼塊,就像if語句或者for循環一樣。在這個代碼塊里,你可以使用變量a_file作為open()函數返回的流對象的引用。所以流對象的常規方法都是可用的 — seek(),read(),無論你想要調用什么。當with塊結束時,Python自動調用a_file.close()。
這就是它與眾不同的地方:無論你以何種方式跳出with塊,Python會自動關閉那個文件…即使是因為未處理的異常而“exit”。是的,即使代碼中引發了一個異常,整個程序突然中止了,Python也能夠保證那個文件能被關閉掉。
☞從技術上說,
with語句創建了一個運行時環境(runtime context)。在這幾個樣例中,流對象的行為就像一個上下文管理器(context manager)。Python創建了a_file,并且告訴它正進入一個運行時環境。當with塊結束的時候,Python告訴流對象它正在退出這個運行時環境,然后流對象就會調用它的close()方法。請閱讀 附錄B,“能夠在with塊中使用的類”以獲取更多細節。
with語句不只是針對文件而言的;它是一個用來創建運行時環境的通用框架(generic framework),告訴對象它們正在進入和離開一個運行時環境。如果該對象是流對象,那么它就會做一些類似文件對象一樣有用的動作(就像自動關閉文件!)。但是那個行為是被流對象自身定義的,而不是在with語句中。還有許多跟文件無關的使用上下文管理器(context manager)的方法。在這章的后面可以看到,你甚至可以自己創建它們。
正如你所想的,一行數據就是這樣 — 輸入一些單詞,按ENTER鍵,然后就在新的一行了。一行文本就是一串被某種東西分隔的字符,到底是被什么分隔的呢?好吧,這有些復雜,因為文本文件可以使用幾個不同的字符來標記行末(end of a line)。每種操作系統都有自己的規矩。有一些使用回車符(carriage return),另外一些使用換行符(line feed),還有一些在行末同時使用這兩個字符來標記。
其實你可以舒口氣了,因為Python默認會自動處理行的結束符。如果你告訴它,“我想從這個文本文件一次讀取一行,”Python自己會弄明白這個文本文件到底使用哪種方式標記新行,然后正確工作。
☞如果想要細粒度地控制(fine-grained control)使用哪種新行標記符,你可以傳遞一個可選的參數
newline給open()函數。請閱讀open()函數的文檔以獲取更多細節。
那么,實際中你會怎樣做呢?我是指一次讀取文件的一行。它如此簡單優美…
line_number = 0
with open('examples/favorite-people.txt', encoding='utf-8') as a_file: ①
for a_line in a_file: ②
line_number += 1
print('{:>4} {}'.format(line_number, a_line.rstrip())) ③
with語句,安全地打開這個文件,然后讓Python為你關閉它。
for循環。是的,除了像read()這樣顯式的方法,流對象也是一個迭代器(iterator),它能在你每次請求一個值時分離出單獨的一行。
format()方法,你可以打印出行號和行自身。格式說明符{:>4}的意思是“使用最多四個空格使之右對齊,然后打印此參數。”變量a_line是包括回車符等在內的完整的一行。字符串方法rstrip()可以去掉尾隨的空白符,包括回車符。
you@localhost:~/diveintopython3$ python3 examples/oneline.py 1 Dora 2 Ethan 3 Wesley 4 John 5 Anne 6 Mike 7 Chris 8 Sarah 9 Alex 10 Lizzie
是否遇到了這個錯誤?
you@localhost:~/diveintopython3$ python3 examples/oneline.py Traceback (most recent call last): File "examples/oneline.py", line 4, in <module> print('{:>4} {}'.format(line_number, a_line.rstrip())) ValueError: zero length field name in format如果結果是這樣,也許你正在使用Python 3.0。你真的應該升級到Python 3.1。
Python 3.0支持字符串格式化,但是只支持顯式編號了的格式說明符。Python 3.1允許你在格式說明符里省略參數索引號。作為比照,下面是一個Python 3.0兼容的版本。
print('{0:>4} {1}'.format(line_number, a_line.rstrip()))
⁂
寫入文件的方式和從它們那兒讀取很相似。首先打開一個文件,獲取流對象,然后你調用一些方法作用在流對象上來寫入數據到文件,最后關閉文件。
為了寫入而打開一個文件,可以使用open()函數,并且指定寫入模式。有兩種文件模式用于寫入:
mode='w'參數給open()函數。
mode='a'參數給open()函數。
如果文件不存在,兩種模式下都會自動創建新文件,所以就不需要“如果文件還不存在,創建一個新的空白文件以能夠打開它”這種瑣碎的過程了。所以,只需要打開一個文件,然后開始寫入即可。
在完成寫入后你應該馬上關閉文件,釋放文件句柄(file handle),并且保證數據被完整地寫入到了磁盤。跟讀取文件一樣,可以調用流對象的close()方法,或者你也可以使用with語句讓Python為你關閉文件。我敢打賭,你肯定能猜到我推薦哪種方案。
>>> with open('test.log', mode='w', encoding='utf-8') as a_file: ①
... a_file.write('test succeeded') ②
>>> with open('test.log', encoding='utf-8') as a_file:
... print(a_file.read())
test succeeded
>>> with open('test.log', mode='a', encoding='utf-8') as a_file: ③
... a_file.write('and again')
>>> with open('test.log', encoding='utf-8') as a_file:
... print(a_file.read())
test succeededand again ④
test.log(或者重寫已經存在的文件),然后以寫入方式打開文件。參數mode='w'的意思是文件以寫入的模式打開。是的,這聽起來似乎比較危險。我希望你確定不再關心那個文件以前的內容(如果有的話),因為那份數據已經沒了。
open()函數返回的流對象的write()方法來給新打開的文件添加數據。當with塊結束的時候,Python自動關閉文件。
with='a'參數來添加數據到文件末尾,而不是重寫它。追加模式絕不會破壞現有文件的內容。
test.log里了。同時請注意,回車符沒有被包括進去。你可以通過'\n'寫入一個回車符。由于一開始沒有這樣做,所有寫入到文件的數據現在都在同一行。
你是否注意到當你在打開文件用于寫入數據的時候傳遞給open()函數的encoding參數。它“非常重要”,不要忽略了!就如你在這章開頭看到的,文件中并不存在字符串,它們由字節組成。只有當你告訴Python使用何種編碼方式把字節流轉換為字符串,從文件讀取“字符串”才成為可能。相反地,寫入文本到文件面臨同樣的問題。實際上你不能直接把字符寫入到文件;字符只是一種抽象。為了寫入字符到文件,Python需要知道如何將字符串轉換為字節序列。唯一能保證正確地執行轉換的方法就是當你為寫入而打開一個文件的時候,指定encoding參數。
⁂
不是所有的文件都包含文本內容。有一些還包含了我可愛的狗的照片。
>>> an_image = open('examples/beauregard.jpg', mode='rb') ①
>>> an_image.mode ②
'rb'
>>> an_image.name ③
'examples/beauregard.jpg'
>>> an_image.encoding ④
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: '_io.BufferedReader' object has no attribute 'encoding'
mode參數包含一個字符'b'。
mode屬性,它記錄了你調用open()函數時指定的mode參數的值。
name屬性,就如文本文件的流對象一樣。
encoding屬性。你能明白其中的道理的,對吧?現在你讀寫的是字節,而不是字符串,所以Python不需要做轉換工作。從二進制文件里讀出的跟你所寫入的是完全一樣的,所以沒有執行轉換的必要。
我是否提到當前正在讀取字節?噢,的確如此。
# continued from the previous example # 繼續前一樣例 >>> an_image.tell() 0 >>> data = an_image.read(3) ① >>> data b'\xff\xd8\xff' >>> type(data) ② <class 'bytes'> >>> an_image.tell() ③ 3 >>> an_image.seek(0) 0 >>> data = an_image.read() >>> len(data) 3150
read()方法每次讀取指定的字節數,而非字符數。
read()方法的數目和你從tell()方法得到的位置序號不會出現意料之外的不匹配(unexpected mismatch)
⁂
想象一下你正在編寫一個庫(library),其中有一庫函數用來從文件讀取數據。它使用文件名作為參數,以只讀的方式打開文件,讀取數據,關閉文件,返回。但是你不應該只做到這個程度。你的API應該能夠接納任意的類型的流對象。
最簡單的情況,只要對象包含read()方法,這個方法使用一個可選參數size并且返回值為一個串,它就是是流對象。不使用size參數調用read()的時候,這個方法應該從輸入源讀取所有可讀的信息然后以單獨的一個值返回所有數據。當使用size參數調用read()時,它從輸入源讀取并返回指定量的數據。當再一次被調用時,它從上一次離開的地方開始讀取并返回下一個數據塊。
這聽起來跟你從打開一個真實文件得到的流對象一樣。不同之處在于你不再受限于真實的文件。能夠“讀取”的輸入源可以是任何東西:網頁,內存中的字符串,甚至是另外一個程序的輸出。只要你的函數使用的是流對象,調用對象的read()方法,你可以處理任何行為與文件類似的輸入源,而不需要為每種類型的輸入指定特別的代碼。
>>> a_string = 'PapayaWhip is the new black.' >>> import io ① >>> a_file = io.StringIO(a_string) ② >>> a_file.read() ③ 'PapayaWhip is the new black.' >>> a_file.read() ④ '' >>> a_file.seek(0) ⑤ 0 >>> a_file.read(10) ⑥ 'PapayaWhip' >>> a_file.tell() 10 >>> a_file.seek(18) 18 >>> a_file.read() 'new black.'
io模塊定義了StringIO類,你可以使用它來把內存中的字符串當作文件來處理。
io.StringIO()來創建一個StringIO的實例。
read()方法“讀取”整個“文件”,以StringIO對象為例即返回原字符串。
read()方法返回一個空串。
StringIO對象的seek()方法,你可以顯式地定位到字符串的開頭,就像在一個真實的文件中定位一樣。
read()方法,你也可以以數據塊的形式讀取字符串。
☞
io.StringIO讓你能夠將一個字符串作為文本文件來看待。另外還有一個io.ByteIO類,它允許你將字節數組當做二進制文件來處理。
Python標準庫包含支持讀寫壓縮文件的模塊。有許多種不同的壓縮方案;其中,gzip和bzip2是非Windows操作系統下最流行的兩種壓縮方式。
gzip模塊允許你創建用來讀寫gzip壓縮文件的流對象。該流對象支持read()方法(如果你以讀取模式打開)或者write()方法(如果你以寫入模式打開)。這就意味著,你可以使用從普通文件那兒學到的技術來直接讀寫gzip壓縮文件,而不需要創建臨時文件來保存解壓縮了的數據。
作為額外的功能,它也支持with語句,所以當你完成了對gzip壓縮文件的操作,Python可以為你自動關閉它。
you@localhost:~$ python3
>>> import gzip
>>> with gzip.open('out.log.gz', mode='wb') as z_file: ①
... z_file.write('A nine mile walk is no joke, especially in the rain.'.encode('utf-8'))
...
>>> exit()
you@localhost:~$ ls -l out.log.gz ②
-rw-r--r-- 1 mark mark 79 2009-07-19 14:29 out.log.gz
you@localhost:~$ gunzip out.log.gz ③
you@localhost:~$ cat out.log ④
A nine mile walk is no joke, especially in the rain.
mode參數里的'b'字符。)
gunzip命令(發音:“gee-unzip”)解壓縮文件然后保存其內容到一個與原來壓縮文件同名的新文件中,并去掉其.gz擴展名。
cat命令顯示文件的內容。當前文件包含了原來你從Python shell直接寫入到壓縮文件out.log.gz的那個字符串。
⁂
命令行高手已經對標準輸入,標準輸出和標準錯誤的概念相當熟悉了。這部分內容是對另一部分還不熟悉的人員準備的。
標準輸出和標準錯誤(通常縮寫為stdout和stderr)是被集成到每一個類UNIX操作系統中的兩個管道(pipe),包括Mac OS X和Linux。當你調用print()的時候,需要打印的內容即被發送到stdout管道。當你的程序出錯并且需要打印跟蹤信息(traceback)時,它們被發送到stderr管道。默認地,這兩個管道都被連接到你正在工作的終端窗口上(terminal window);當你的程序打印某些東西,你可以在終端上看到這些輸出,當程序出錯,你也可以從終端上看到這些錯誤信息。在圖形化的Python shell里,stdout和stderr管道默認連接到“交互式窗口(Interactive Window)”
>>> for i in range(3):
... print('PapayaWhip') ①
PapayaWhip
PapayaWhip
PapayaWhip
>>> import sys
>>> for i in range(3):
... sys.stdout.write('is the') ②
is theis theis the
>>> for i in range(3):
... sys.stderr.write('new black') ③
new blacknew blacknew black
print()函數。沒有什么特別的。
stdout被定義在sys模塊里,它是一個流對象(stream object)。使用任意字符串調用其write()函數會按原樣輸出。事實上,這就是print()函數實際在做的事情;它在串的結尾添加一個回車符,然后調用sys.stdout.write。
sys.stdout和sys.stderr把他們的輸出發送到同一個位置:Python IDE(如果你在那里執行操作),或者終端(如果你從命令行執行Python指令)。跟標準輸出一樣,標準錯誤也不會自動為你添加回車符。如果你需要回車符,你需要手工寫入回車符到標準錯誤。
sys.stdout和sys.stderr都是流對象,但是他們都只支持寫入。試圖調用他們的read()方法會引發IOError異常。
>>> import sys >>> sys.stdout.read() Traceback (most recent call last): File "<stdin>", line 1, in <module> IOError: not readable
sys.stdout和sys.stderr都是流對象,盡管他們只支持寫入。但是他們是變量而不是常量。這就意味著你可以給它們賦上新值 — 任意其他流對象 — 來重定向他們的輸出。
import sys
class RedirectStdoutTo:
def __init__(self, out_new):
self.out_new = out_new
def __enter__(self):
self.out_old = sys.stdout
sys.stdout = self.out_new
def __exit__(self, *args):
sys.stdout = self.out_old
print('A')
with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file):
print('B')
print('C')
驗證一下:
you@localhost:~/diveintopython3/examples$ python3 stdout.py A C you@localhost:~/diveintopython3/examples$ cat out.log B
你是否遇到了以下錯誤?
you@localhost:~/diveintopython3/examples$ python3 stdout.py File "stdout.py", line 15 with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file): ^ SyntaxError: invalid syntax如果是這樣,你可能正在使用Python 3.0。應該升級到Python 3.1。
Python 3.0支持
with語句,但是每個語句只能使用一個上下文管理器。Python 3.1允許你在一條with語句中鏈接多個上下文件管理器。
我們先來處理最后那一部分。
print('A')
with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file):
print('B')
print('C')
這是一個復雜的with語句。讓我改寫它使之更有可讀性。
with open('out.log', mode='w', encoding='utf-8') as a_file:
with RedirectStdoutTo(a_file):
print('B')
正如改動后的代碼所展示的,實際上你使用了兩個with語句,其中一個嵌套在另外一個的作用域(scope)里。“外層的”with語句你應該已經熟悉了:它打開一個使用UTF-8編碼的叫做out.log的文本文件用來寫入,然后把返回的流對象賦給一個叫做a_file的變量。但是,在此處,它并不是唯一顯得古怪的事情。
with RedirectStdoutTo(a_file):
那么,這些邊際效應都是些什么呢?我們來看一看 放到一起:
重定向標準錯誤的原理跟這個完全一樣,將 ⁂
© 2001–9 Mark Pilgrim
as子句(clause)到哪里去了?其實with語句并不一定需要as子句。就像你調用一個函數然后忽略其返回值一樣,你也可以不把with語句的上下文環境賦給一個變量。在這種情況下,我們只關心RedirectStdoutTo上下文環境的邊際效應(side effect)。
和RedirectStdoutTo類的內部結構。這是一個用戶自定義的上下文管理器(context manager)。任何類只要定義了兩個特殊方法:code>__enter__()__exit__()就可以變成上下文管理器。
class RedirectStdoutTo:
def __init__(self, out_new): ①
self.out_new = out_new
def __enter__(self): ②
self.out_old = sys.stdout
sys.stdout = self.out_new
def __exit__(self, *args): ③
sys.stdout = self.out_old
__init__()方法馬上被調用。它使用一個參數,即在上下文環境的生命周期內你想用做標準輸出的流對象。這個方法只是把該流對象保存在一個實例變量里(instance variable)以使其他方法在后邊能夠使用到它。
__enter__()方法是一個特殊的類方法(special class method);在進入一個上下文環境時Python會調用它(即,在with語句的開始處)。該方法把當前sys.stdout的值保存在self.out_old內,然后通過把self.out_new賦給sys.stdout來重定向標準輸出。
__exit__()是另外一個特殊類方法;當離開一個上下文環境時(即,在with語句的末尾)Python會調用它。這個方法通過把保存的self.out_old的值賦給sys.stdout來恢復標準輸出到原來的狀態。
print('A') ①
with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file): ②
print('B') ③
print('C') ④
with語句使用逗號分隔的上下文環境列表。這個列表就像一系列相互嵌套的with塊。先列出的是“外層”的塊;后列出的是“內層”的塊。第一個上下文環境打開一個文件;第二個重定向sys.stdout到由第一個上下環境創建的流對象。
print()函數在with語句創建的上下文環境里執行,所以它不會輸出到屏幕;它會寫入到文件out.log。
with語句塊結束了。Python告訴每一個上下文管理器完成他們應該在離開上下文環境時應該做的事。這些上下文環境形成一個后進先出的棧。當離開一個上下文環境的時候,第二個上下文環境將sys.stdout的值恢復到它的原來狀態,然后第一個上下文環境關閉那個叫做out.log的文件。由于標準輸出已經被恢復到原來的狀態,再次調用print()函數會馬上輸出到屏幕上。
sys.stdout替換為sys.stderr即可。
進一步閱讀
io 模塊
sys.stdout and sys.stderr