Python 完美詮釋"高內聚"概念的 IO 流 API 體系結構

一枚大果殼發表於2022-03-08

1. 前言

第一次接觸 Python 語言的 IO API 時,是驚豔的。相比較其它語言所提供的 IO 流 API 。

無論是站在使用者的角度還是站在底層設計者的角度,都可以稱得上無與倫比。

很多人在學習 JAVA 語言中的 IO 流 API 時,幾乎是崩潰的。其 API 太多、API 之間的關係過於複雜。類的層次結構需要花費很多時間才能搞明白。API 設計者未免有炫技之嫌。

Python 的 IO 流操作,才真正應了哪句話:人生苦短,我學 python 。

open( ) 函式 為操作起點,便捷、快速地完成所有操作,絕對算得上輕量級設計的典範,且高度詮釋了“高內聚”概念。使用起來頗有“四兩撥千金”的輕鬆。

通過了解 open( ) 函式的引數設計,其開閉設計思想可謂使用到了極致。

2. open( ) 函式

2.1 函式原型

def open(file, mode='r', buffering=None, encoding=None, errors=None, newline=None, closefd=True):
    ……

2.2 函式功能

開啟一個指定位置的檔案並返回 IO 流物件。

2.3 函式引數

Tip: open( ) 函式的引數看起來雖然有點多,在使用時,很多引數都可以採用預設設定,它會提供最優的工作方案。

  • file 引數: 指定檔案位置。可以是一個字串描述的檔案路徑,也可以是一個檔案描述符(int 型別)。

    Tip: 當使用字串描述時,可以是絕對路徑,也可以是相對路徑。

    絕對路徑: 以絕對位置作為路徑的起點。 不同的作業系統中會有差異性,windows 以邏輯碟符為絕對起點,Liunx 以 "/" 根目錄為絕對起點。

    file=open("d:/guoke.txt")
    

    Tip: 上述程式碼執行時,需要保證在系統的 d 盤下有一個名字 "guoke.txt" 的檔案。

    相對路徑: 所謂相對路徑指以某一個已經存在的路徑(或叫參照目錄、當前目錄)做起點。 預設情況下,相對路徑以當前專案目錄作為參照目錄。可以使用 os 模組 中的 getcwd( ) 方法獲取當前參照目錄的資訊。

    import os
    print(os.getcwd())
    # 本程式碼的測試專案放在 d:\myc 下;專案名稱:filedmeo
    # 輸出結果
    # D:\myc\filedmeo
    

    如下程式碼需要保證在專案目錄中存在 " guoke.txt "

    file = open("guoke.txt")
    # 執行時, python 直譯器會自動拼接一個完整路徑 D:\myc\filedmeo\guoke.txt
    

    參照目錄 可以是不固定的,而是可變的。

    改變相對路徑的參考目錄:

    import os
    # 把 d 盤作為當前目錄
    os.chdir("d:/")
    print(os.getcwd())
    file = open("guoke.txt")
    # python 直譯器會從 d 盤根目錄下查詢 guoke.txt 檔案
    

    描述符: 使用 open( ) 函式開啟一個檔案後,python 直譯器系統會為此檔案指定一個唯一的數字識別符號。可以用此描述符作為 open( ) 引數。

    file = open("guo_ke.txt")
    # fileno() 獲取檔案的描述符
    file1 = open(file.fileno())
    

    Tip: 使用者檔案的描述符從 3 開始。0,1,2 是系統保留檔案描述符。

    • 0:表示標準輸入(鍵盤)裝置描述符。
    file = open(0)
    print("請輸入一個數字:")
    res = file.readline()
    print("回顯:", res)
    '''
    輸出結果
    請輸入一個數字:
    88
    回顯: 88
    '''
    
    • 1:表示標準輸出裝置(顯示器)描述符。
    file = open(1, "w")
    file.write("you are welcome!")
    #類似於 print("you are welcome!") 的功能
    
    • 2:表示標準錯誤輸出裝置(顯示器)描述符。
    file = open(2, "w")
    file.write("you are welcome!")
    #輸出文字會以紅色亮顯
    
  • mode: 檔案操作模式。預設為 "r" ,表示只讀模式。

    模式關鍵字 描述 異常
    'r' 以只讀方式開啟檔案 檔案不存時,會丟擲 FileNotFoundError 異常
    ‘r+’ 以可讀、可寫方式開啟檔案 檔案不存時,會丟擲 FileNotFoundError 異常
    ‘w’ 以可寫方式開啟檔案 檔案不存在時,建立一個位元組 0 的空檔案
    ‘w+’ 以可寫、可讀方式開啟檔案(清空原內容) 檔案不存在時,建立一個位元組 0 的空檔案
    ‘a’ 以追加方式開啟檔案 檔案不存在時,建立一個位元組 0 的空檔案
    ‘a+’ 以可追加、可讀方式開啟檔案 檔案不存在時,建立一個位元組 0 的空檔案
    ‘t’ 以文字檔案格式開啟檔案 預設
    ‘b’ 以二進位制格式開啟檔案
    ‘x’ 建立空檔案並且可寫 檔案存在時,丟擲 FileExistsError 異常

    只要在模式組合中有 'r' 關鍵字,則檔案必須提前存在:

    file = open("guo_ke.txt")
    file = open("guo_ke.txt", 'r')
    file = open("guo_ke.txt", 'rt')
    file = open("guo_ke.txt", 'r+t')
    

    只要在模式組合中有 ‘w’ 關鍵字,則檔案可以不必預先存在,如果存在,則原檔案中內容會被清空。

    # 可寫
    file = open("guo_ke.txt", 'w')
    # 可寫、可讀
    file = open("guo_ke.txt", 'w+')
    

    只要在模式組合中有 ‘a’ 關鍵字,則檔案可以不必預先存在,如果存在,原檔案中內空不會被清空

    # 追加寫
    file = open("guo_ke.txt", 'a')
    # 追加寫、且可讀
    file = open("guo_ke.txt", 'a+')
    
  • buffering: 設定緩衝策略。可取值為 0、1、>1 。

    • 0: 在二進位制模式下關閉緩衝。

    • 1:在文字模式下使用行緩衝。

      行緩衝:以行資料為單位進行快取。

    • >1 的整數: 指定緩衝區的大小(以位元組為單位)。

    如果沒有指定 buffering 引數,則會提供預設緩衝策略:

    • 二進位制檔案使用固定大小的緩衝塊。

      在許多系統上,緩衝區的長度通常為 40968192 位元組。

    • "Interactive" 文字檔案( isatty() 返回 True 的檔案)使用行緩衝。其他文字檔案使用和二進位制檔案相同的緩衝策略。

      isatty( ) 方法檢測檔案是否連線到一個終端裝置。

  • encoding: 指定解碼或編碼檔案時使用的編碼名稱。

    只能用於文字檔案。預設使用平臺編碼。

  • errors: 指定如何處理編碼和解碼時丟擲的錯誤。可選項如下:

    • strict: 如果存在編碼錯誤,則引發 ValueError 異常。 預設值 None 具有相同的效果。
    • ignore: 忽略錯誤。有可能會資料丟失。
    • replace: 會將替換標記(例如 '?' )插入有錯誤資料的地方。
  • newline: 在讀或寫文字內容時如何處理換行符號。可取值 None,' ','\n','\r' 和 '\r\n'。

    OS 不同,換行符的描述也有差異。Unix 的行結束 '\n'、Windows 中為 '\r\n'

    • 從流中讀資料時,如果 newline 為 None,則啟用平臺約定換行模式。
    • 寫入流時,如果 newline 為 None,則寫入的任何 '\n' 字元都將轉換為系統預設行分隔符 。如果 newline 是 ' ' 或 '\n',則直接寫入。如果 newline 是任何其他合法值,則寫入的任何 '\n' 字元將被轉換為給定的字串。
  • closefd:

    file = open("guo_ke.txt",closefd=False)
    '''
    輸出結果
    Traceback (most recent call last):
      File "D:/myc/filedmeo/檔案巢狀.py", line 1, in <module>
        file = open("guo_ke.txt",closefd=False)
    ValueError: Cannot use closefd=False with file name
    '''
    

    如果通過一個字串路徑描述開啟檔案, closefd 必須為 True (預設值),否則將引發錯誤。

    file = open("guo_ke.txt", )
    # 通過 file 檔案的描述符開啟檔案
    file1 = open(file.fileno(), closefd=False)
    file1.close()
    print("先開啟檔案:", file.closed)
    print("後開啟檔案:", file1.closed)
    '''
    輸出結果
    先開啟檔案: False
    後開啟檔案: True
    '''
    

    當 open file1 檔案時設定為 closefd=False ,則當 file1 檔案關閉後,file 檔案將保持開啟狀態。

  • opener:可理解為 open( ) 函式是一個高階封裝物件,本質是通過 opener 引數接入了一個真正的具有底層檔案操作能力的介面。

    import os
    
    def opener(path, flags):
        return os.open(path, flags)
    # 呼叫 opener('guo_ke.txt','r') 時的引數來自於 open() 的第一個和第二個
    with open('guo_ke.txt', 'r', opener=opener) as f:
        print(f.read())
    

    預設 opener 引數引用的就是 os.open( ) 方法。

3. 讀寫操作

呼叫 open( ) 函式後會返回一個 IO 流物件。IO 流物件中提供了常規的與讀寫相關的屬性和方法。

class IO(Generic[AnyStr]):
    #返回檔案的讀寫模式
    @abstractproperty
    def mode(self) -> str:
        pass
    #返回檔案的名稱
    @abstractproperty
    def name(self) -> str:
        pass
    #關閉檔案
    @abstractmethod
    def close(self) -> None:
        pass
    #判斷檔案是否關閉
    @abstractproperty
    def closed(self) -> bool:
        pass
    #返回檔案描述符號,每開啟一個檔案,python 會分配一個唯一的數字描述符號
    @abstractmethod
    def fileno(self) -> int:
        pass
    #重新整理快取中的內容
    @abstractmethod
    def flush(self) -> None:
        pass
    #是否連線到一個終端裝置
    @abstractmethod
    def isatty(self) -> bool:
        pass
    # 引數 n 為 -1 或不傳遞時,一次性讀取檔案中的所有內容,如果檔案內容過多,可分多次讀取
    # 讀取到檔案末尾時,返回一個空字串 ('')
    @abstractmethod
    def read(self, n: int = -1) -> AnyStr:
        pass
    # 檔案是否可讀
    @abstractmethod
    def readable(self) -> bool:
        pass
    # 從檔案中讀取一行;換行符(\n)留在字串的末尾
    # 返回一個空的字串時,表示已經到達了檔案末尾
    # 空行使用 '\n' 表示
    @abstractmethod
    def readline(self, limit: int = -1) -> AnyStr:
        pass
    # 讀取所有行並儲存到列表中
    # 也可以使用 list(f) 
    @abstractmethod
    def readlines(self, hint: int = -1) -> List[AnyStr]:
        pass
    # 移動讀寫游標,改變檔案的讀寫位置
    # 通過向一個參考點新增 offset 來計算位置;參考點由 whence 引數指定。
    # whence 的 0 值表示從檔案開頭起算,1 表示使用當前檔案位置,2 表示使用檔案末尾作為參考點。 
    # whence 如果省略則預設值為 0,即使用檔案開頭作為參考點。
    @abstractmethod
    def seek(self, offset: int, whence: int = 0) -> int:
        pass
   # 是否可以移動游標
    @abstractmethod
    def seekable(self) -> bool:
        pass
    # 返回檔案的當前位置
    @abstractmethod
    def tell(self) -> int:
        pass
    # 清除內容
    @abstractmethod
    def truncate(self, size: int = None) -> int:
        pass
    # 是否可寫
    @abstractmethod
    def writable(self) -> bool:
        pass
    # 向檔案寫入內容
    @abstractmethod
    def write(self, s: AnyStr) -> int:
        pass
    #向檔案寫入一行資料 
    @abstractmethod
    def writelines(self, lines: List[AnyStr]) -> None:
        pass

呼叫 open( ) 函式中使用文字模式時返回的是 TextIO 物件,相比較父類,多了幾個特定於文字操作的屬性。

class TextIO(IO[str]):
    # 快取資訊
    @abstractproperty
    def buffer(self) -> BinaryIO:
        pass
    # 設定編碼
    @abstractproperty
    def encoding(self) -> str:
        pass
    # 裝置錯誤處理方案
    @abstractproperty
    def errors(self) -> Optional[str]:
        pass
    # 設定行快取
    @abstractproperty
    def line_buffering(self) -> bool:
        pass
    # 換行符的設定方案
    @abstractproperty
    def newlines(self) -> Any:
        pass

3.1 文字檔案讀操作

  1. 基本操作
file = open("guo_ke.txt", mode='r')
print("讀寫模式:", file.mode)
print("檔名:", file.name)
print("檔案是否關閉:", file.closed)
print("檔案描述符號:", file.fileno())
print("檔案是否可讀", file.readable())
print("是否是標準輸入流:", file.isatty())
print("檔案是否可寫:", file.writable())
print("快取方案", file.buffer)
print("檔案預設編碼:", file.encoding)
print("程式設計錯誤處理方案", file.errors)
print("是否設定行快取", file.line_buffering)
print("換行符的設定方案", file.newlines)

'''
輸出結果
讀寫模式: r
檔名: guo_ke.txt
檔案是否關閉: False
檔案描述符號: 3
檔案是否可讀 True
是否是標準輸入流: False
檔案是否可寫: False
快取方案 <_io.BufferedReader name='guo_ke.txt'>
檔案預設編碼: cp936
程式設計錯誤處理方案 strict
是否設定行快取 False
換行符的設定方案 None
'''

cp936 指的是系統的第 936 號編碼方案,即 GBK 編碼。

  1. 多樣化的讀方法:

    無論是讀還是寫時,需要理解一個檔案指標(游標)的概念,也可理解為檔案位置。讀或寫時,只能從當前位置向前移動。

    提前準備好一個文字檔案,在檔案中寫入如下內容

You hide in my heart deeply.
Happiness! There is only you and I together time...
With you just I don't want to give anyone the chance.
Honey, can you marry me, I'll marry you!
Don't know love you count is a close reason?
  • read( ) 方法的使用

    file = open("guo_ke.txt", "r")
    print("----------讀取所有內容--------------")
    res = file.read()
    print(res)
    print("----------讀取部分內容--------------")
    # 重新回到檔案頭
    file.seek(0)
    res = file.read(100)
    print(res)
    # 關閉檔案資源
    file.close()
    '''
    輸出結果
    ----------讀取所有內容--------------
    You hide in my heart deeply.
    Happiness! There is only you and I together time...
    With you just I don't want to give anyone the chance.
    Honey, can you marry me, I'll marry you!
    Don't know love you count is a close reason?
    ----------讀取部分內容--------------
    You hide in my heart deeply.
    Happiness! There is only you and I together time...
    With you just I don
    '''
    

    這裡有一個細節要注意:

    第一次讀取完所有檔案內容後,讀取位置已經移到了檔案尾部。繼續讀取時是不能讀到資料的。

    可通過 seek( ) 方法,把游標移到檔案頭部。

  • readline( ) 方法的使用

file = open("guo_ke.txt", "r")

print("---------讀取一行--------")
res = file.readline()
print("資料長度:", len(res))
print(res)
print("-----------限制內容-------------")
res = file.readline(10)
print("資料長度:", len(res))
print(res)
print("-----------以行為單位讀取所有資料-------------")
#回到檔案頭部位置
file.seek(0)
while True:
    res = file.readline()
    print(res)
    if res == "":
        break

file.close()
'''
輸出結果
---------讀取一行--------
資料長度: 29
You hide in my heart deeply.

-----------限制內容-------------
資料長度: 10
Happiness!
-----------以行為單位讀取所有資料-------------
You hide in my heart deeply.

Happiness! There is only you and I together time...

With you just I don't want to give anyone the chance.

Honey, can you marry me, I'll marry you!

Don't know love you count is a close reason?
'''

一行一行讀取所有內容時,輸出時會在行與行之間產生一個空行。原因是行結束符號 'n' 會被當成一個空行輸出。

  • readline( ) 還有一個兄弟 readlines() 。把資料以行為單位一次性儲存一個列表中.
file = open("guo_ke.txt", "r")

print("-----------把檔案中資料以行為單位儲存在列表中---------")
res = file.readlines()
print(res)
file.close()
'''
輸出結果
-----------把檔案中資料以行為單位儲存在列表中---------
['You hide in my heart deeply.\n', 'Happiness! There is only you and I together time...\n', "With you just I don't want to give anyone the chance.\n", "Honey, can you marry me, I'll marry you!\n", "Don't know love you count is a close reason?"]
'''

注意使用資料時換行符號的影響。

讀取所有行也可以使用 ist(f) 方式。

file = open("guo_ke.txt", "r")
print(list(file))
  • 檔案物件支援以行為單位進行迭代操作。
file = open("guo_ke.txt", "r")
print("-----------迭代方式輸出檔案內容---------")
for f in file:
    print(f)
file.close()

3.2 文字檔案寫操作

如果使用 "w" 模式進行寫操作時,會丟失原來資料。如果不希望這樣的事情 發生,可使用 "a" 模式對文寫操作。

file = open("guo_ke_0.txt", "w")
file.write("this is a test")
# 新增新行
file.write("\n")
file.write("who are you?")
# 把列表中資料一次寫入檔案
lst = ["food\n", "fish\n", "cat\n"]
file.write("\n")
file.writelines(lst)
file.close()

3.3 編碼的問題

對檔案同時做讀寫操作時,請務必保證編碼的一致性。

如下面的程式碼就會出現 UnicodeDecodeError 異常。

file = open("guo_ke_1.txt", mode="w", encoding="utf-8")
file.write("你好!果殼……")
file.close()

file_ = open("guo_ke_1.txt", mode="r", encoding="gbk")
res = file_.read()
print(res)
'''
輸出結果
Traceback (most recent call last):
  File "D:/myc/filedmeo/亂碼問題.py", line 6, in <module>
    res = file_.read()
UnicodeDecodeError: 'gbk' codec can't decode byte 0x80 in position 16: illegal multibyte sequence
'''

3.4 二進位制文字件操作

呼叫 open( ) 函式時使用 mode='rb' 返回的是 BinaryIO 物件。此物件提供了對二進位制檔案的讀寫,對二進位制檔案的讀寫操作和文字的沒有什麼太多區別。

文字檔案與二進位制文字的操作使用一個引數就能靈活切換。

class BinaryIO(IO[bytes]):
    
    @abstractmethod
    def write(self, s: Union[bytes, bytearray]) -> int:
        pass

4. 總結

open( ) 函式是一個神奇的存在。無論是對文字檔案還是二進進位制檔案,無論是讀還是寫,它都能工作的很好。不得不佩服 python 設計者的簡潔設計理念。

像通過檔案描述符開啟檔案,使用 opener 引數自定義底層實現,都可稱得上神來之筆。

另使用檔案後一定要關閉,除了可以直接呼叫 close( ) 方法外,還可能使用 with 語句,此語法結構能自動呼叫 close().

with open("guo_ke.txt") as f:
    pass

相關文章