你的位置: 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'。關于這個文件名,有五件值得一講的事情:

  1. 它不僅是一個文件的名字;實際上,它是文件路徑和文件名的組合;一般來說,文件打開函數應該有兩個參數 — 路徑和文件名 — 但是函數open()只使用一個參數。在Python里,當你使用“filename,”作為參數的時候,你可以將部分或者全部的路徑也包括進去。
  2. 在這個例子中,目錄路徑中使用的是斜杠(forward slash),但是我并沒有說明我正在使用的操作系統。Windows使用反斜杠來表示子目錄,但是Mac OS X和Linux使用斜杠。但是,在Python中,斜杠永遠都是正確的,即使是在Windows環境下。
  3. 不使用斜杠或者反斜杠的路徑被稱作相對路徑(relative path)。你也許會問,相對于什么呢?耐心一些,伙計。
  4. “filename,”參數是一個字符串。所有現代的操作系統(甚至Windows!)使用Unicode編碼方式來存儲文件名和目錄名。Python 3全面支持非ASCII編碼的路徑。
  5. 文件不一定需要在本地磁盤上。也許你掛載了一個網絡驅動器。它也可以是一個完全虛擬的文件系統(an entirely virtual filesystem)上的文件。只要你的操作系統認為它是一個文件,并且能夠以文件的方式訪問,那么,Python就能打開它。

但是對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'
  1. name屬性反映的是當你打開文件時傳遞給open()函數的文件名。它沒有被標準化(normalize)成絕對路徑。
  2. 同樣的,encoding屬性反映的是在你調用open()函數時指定的編碼方式。如果你在打開文件的時候沒有指定編碼方式(不好的開發人員!),那么encoding屬性反映的是locale.getpreferredencoding()的返回值。
  3. 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()                                            
''
  1. 只要成功打開了一個文件(并且指定了正確的編碼方式),你只需要調用流對象的read()方法即可以讀取它。返回的結果是文件的一個字符串表示。
  2. 也許你會感到意外,再次讀取文件不會產生一個異常。Python不認為到達了文件末尾(end-of-file)還繼續執行讀取操作是一個錯誤;這種情況下,它只是簡單地返回一個空字符串。

如果想要重新讀取文件呢?

# 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
  1. 由于你依舊在文件的末尾,繼續調用read()方法只會返回一個空字符串。
  2. seek()方法使定位到文件中的特定字節。
  3. read()方法可以使用一個可選的參數,即所要讀取的字符個數。
  4. 只要愿意,你甚至可以一次讀取一個字符。
  5. 16 + 1 + 1 = … 20?

我們再來做一遍。

# continued from the previous example
# 繼續上一示例
>>> a_file.seek(17)                    
17
>>> a_file.read(1)                     
'是'
>>> a_file.tell()                      
20
  1. 移動到第17th個字節位置。
  2. 讀取一個字符。
  3. 當前在第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
  1. 定位到第18th個字節,然后試圖讀取一個字符。
  2. 為什么這里會失敗?因為在第18個字節處不存在字符。距離此處最近的字符從第17個字節開始(長度為三個字節)。試圖從一個字符的中間位置讀取會導致程序以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
  1. 不能讀取已經關閉了的文件;那樣會引發一個IOError異常。
  2. 也不能對一個已經關閉了的文件執行定位操作。
  3. 由于文件已經關閉了,所以也就不存在所謂當前的位置了,所以tell()也會失敗。
  4. 也許你會有些意外,文件已經關閉,調用原來流對象的close()方法并沒有引發異常。其實那只是一個空操作(no-op)而已。
  5. 已經關閉了的流對象確實還有一個有用的屬性: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)使用哪種新行標記符,你可以傳遞一個可選的參數newlineopen()函數。請閱讀open()函數的文檔以獲取更多細節。

那么,實際中你會怎樣做呢?我是指一次讀取文件的一行。它如此簡單優美…

[download oneline.py]

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()))          
  1. 使用with語句,安全地打開這個文件,然后讓Python為你關閉它。
  2. 為了一次讀取文件的一行,使用for循環。是的,除了像read()這樣顯式的方法,流對象也是一個迭代器(iterator),它能在你每次請求一個值時分離出單獨的一行。
  3. 使用字符串的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()函數,并且指定寫入模式。有兩種文件模式用于寫入:

如果文件不存在,兩種模式下都會自動創建新文件,所以就不需要“如果文件還不存在,創建一個新的空白文件以能夠打開它”這種瑣碎的過程了。所以,只需要打開一個文件,然后開始寫入即可。

在完成寫入后你應該馬上關閉文件,釋放文件句柄(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                                           
  1. 大膽地創建新文件test.log(或者重寫已經存在的文件),然后以寫入方式打開文件。參數mode='w'的意思是文件以寫入的模式打開。是的,這聽起來似乎比較危險。我希望你確定不再關心那個文件以前的內容(如果有的話),因為那份數據已經沒了。
  2. 你可以通過open()函數返回的流對象的write()方法來給新打開的文件添加數據。當with塊結束的時候,Python自動關閉文件。
  3. 多么有趣,我們再試一次。這一次,使用with='a'參數來添加數據到文件末尾,而不是重寫它。追加模式絕不會破壞現有文件的內容。
  4. 原來寫入的行,還有追加上去的第二行現在都在文件test.log里了。同時請注意,回車符沒有被包括進去。你可以通過'\n'寫入一個回車符。由于一開始沒有這樣做,所有寫入到文件的數據現在都在同一行。

再次討論字符編碼

你是否注意到當你在打開文件用于寫入數據的時候傳遞給open()函數的encoding參數。它“非常重要”,不要忽略了!就如你在這章開頭看到的,文件中并不存在字符串,它們由字節組成。只有當你告訴Python使用何種編碼方式把字節流轉換為字符串,從文件讀取“字符串”才成為可能。相反地,寫入文本到文件面臨同樣的問題。實際上你不能直接把字符寫入到文件;字符只是一種抽象。為了寫入字符到文件,Python需要知道如何將字符串轉換為字節序列。唯一能保證正確地執行轉換的方法就是當你為寫入而打開一個文件的時候,指定encoding參數。

二進制文件

my dog Beauregard

不是所有的文件都包含文本內容。有一些還包含了我可愛的狗的照片。

>>> 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'
  1. 用二進制模式打開文件很簡單,但是很精細。與文本模式唯一不同的是mode參數包含一個字符'b'
  2. 以二進制模式打開文件得到的流對象與之前的有很多相同的屬性,包括mode屬性,它記錄了你調用open()函數時指定的mode參數的值。
  3. 二進制文件的流對象也有name屬性,就如文本文件的流對象一樣。
  4. 然而,確實有不同之處:二進制的流對象沒有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
  1. 跟讀取文本文件一樣,你也可以從二進制文件一次讀一點兒。但是它們之間有一個重大的不同之處&#hellip;
  2. &#hellip;你正在讀取字節,而不是字符串。由于你以二進制模式打開文件,read()方法每次讀取指定的字節數,而非字符數。
  3. 這就意味著,你傳遞給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.'
  1. io模塊定義了StringIO類,你可以使用它來把內存中的字符串當作文件來處理。
  2. 為了從字符串創建一個流對象,可以把想要作為“文件”使用的字符串傳遞給io.StringIO()來創建一個StringIO的實例。
  3. 調用read()方法“讀取”整個“文件”,以StringIO對象為例即返回原字符串。
  4. 就像一個真實的文件一樣,再次調用read()方法返回一個空串。
  5. 通過使用StringIO對象的seek()方法,你可以顯式地定位到字符串的開頭,就像在一個真實的文件中定位一樣。
  6. 通過傳遞size參數給read()方法,你也可以以數據塊的形式讀取字符串。

io.StringIO讓你能夠將一個字符串作為文本文件來看待。另外還有一個io.ByteIO類,它允許你將字節數組當做二進制文件來處理。

處理壓縮文件

Python標準庫包含支持讀寫壓縮文件的模塊。有許多種不同的壓縮方案;其中,gzipbzip2是非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.
  1. 你應該問題以二進制模式打開gzip壓縮文件。(注意mode參數里的'b'字符。)
  2. 我在Linux系統上完成的這個例子。如果你對命令行不熟悉,這條命令用來顯示剛才你在Python shell創建的gzip壓縮文件的“長清單(long listings)”,你可以看到,它有79個字節長。而實際上這個值比一開始的字符串還要長!由于gzip文件包括了一個固定長度的文件頭來存放一些關于文件的元數據(metadata),所以它對于極小的文件來說效率不高。
  3. gunzip命令(發音:“gee-unzip”)解壓縮文件然后保存其內容到一個與原來壓縮文件同名的新文件中,并去掉其.gz擴展名。
  4. cat命令顯示文件的內容。當前文件包含了原來你從Python shell直接寫入到壓縮文件out.log.gz的那個字符串。

標準輸入、輸出和錯誤

命令行高手已經對標準輸入,標準輸出和標準錯誤的概念相當熟悉了。這部分內容是對另一部分還不熟悉的人員準備的。

標準輸出和標準錯誤(通常縮寫為stdoutstderr)是被集成到每一個類UNIX操作系統中的兩個管道(pipe),包括Mac OS X和Linux。當你調用print()的時候,需要打印的內容即被發送到stdout管道。當你的程序出錯并且需要打印跟蹤信息(traceback)時,它們被發送到stderr管道。默認地,這兩個管道都被連接到你正在工作的終端窗口上(terminal window);當你的程序打印某些東西,你可以在終端上看到這些輸出,當程序出錯,你也可以從終端上看到這些錯誤信息。在圖形化的Python shell里,stdoutstderr管道默認連接到“交互式窗口(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
  1. 循環調用print()函數。沒有什么特別的。
  2. stdout被定義在sys模塊里,它是一個流對象(stream object)。使用任意字符串調用其write()函數會按原樣輸出。事實上,這就是print()函數實際在做的事情;它在串的結尾添加一個回車符,然后調用sys.stdout.write
  3. 最簡單的情況下,sys.stdoutsys.stderr把他們的輸出發送到同一個位置:Python IDE(如果你在那里執行操作),或者終端(如果你從命令行執行Python指令)。跟標準輸出一樣,標準錯誤也不會自動為你添加回車符。如果你需要回車符,你需要手工寫入回車符到標準錯誤。

sys.stdoutsys.stderr都是流對象,但是他們都只支持寫入。試圖調用他們的read()方法會引發IOError異常。

>>> import sys
>>> sys.stdout.read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: not readable

標準輸出重定向

sys.stdoutsys.stderr都是流對象,盡管他們只支持寫入。但是他們是變量而不是常量。這就意味著你可以給它們賦上新值 — 任意其他流對象 — 來重定向他們的輸出。

[download stdout.py]

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):

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
  1. 在實例被創建后__init__()方法馬上被調用。它使用一個參數,即在上下文環境的生命周期內你想用做標準輸出的流對象。這個方法只是把該流對象保存在一個實例變量里(instance variable)以使其他方法在后邊能夠使用到它。
  2. __enter__()方法是一個特殊的類方法(special class method);在進入一個上下文環境時Python會調用它(,在with語句的開始處)。該方法把當前sys.stdout的值保存在self.out_old內,然后通過把self.out_new賦給sys.stdout來重定向標準輸出。
  3. __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')                                                                             
  1. 這條代碼會輸出到IDE的“交互式窗口(Interactive Window)”(或者終端,如果你從命令行運行這段腳本)。
  2. 這條with語句使用逗號分隔的上下文環境列表。這個列表就像一系列相互嵌套的with塊。先列出的是“外層”的塊;后列出的是“內層”的塊。第一個上下文環境打開一個文件;第二個重定向sys.stdout到由第一個上下環境創建的流對象。
  3. 由于這個print()函數在with語句創建的上下文環境里執行,所以它不會輸出到屏幕;它會寫入到文件out.log
  4. with語句塊結束了。Python告訴每一個上下文管理器完成他們應該在離開上下文環境時應該做的事。這些上下文環境形成一個后進先出的棧。當離開一個上下文環境的時候,第二個上下文環境將sys.stdout的值恢復到它的原來狀態,然后第一個上下文環境關閉那個叫做out.log的文件。由于標準輸出已經被恢復到原來的狀態,再次調用print()函數會馬上輸出到屏幕上。

重定向標準錯誤的原理跟這個完全一樣,將sys.stdout替換為sys.stderr即可。

進一步閱讀

© 2001–9 Mark Pilgrim

<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

            亚洲欧美在线