I/O在計算機中是指Input/Output,也就是Stream(流)的輸入和輸出。這裏的輸入和輸出是相對於內存來講的,Input Stream(輸入流)是指數據從外(磁盤、網絡)流進內存,Output Stream是數據從內存流出到外面(磁盤、網絡)。程序運行時,數據都是在內存中駐留,由CPU這個超快的計算核心來執行,涉及到數據交換的地方(一般是磁盤、網絡操做)就須要IO接口。php
那麼這個IO接口是由誰提供呢?高級編程語言中的IO操做是如何實現的呢?html
操做系統是個通用的軟件程序,其通用目的以下:python
操做系統屏蔽了底層硬件,向上提供通用接口。所以,操做I/O的能力是由操做系統的提供的,每一種編程語言都會把操做系統提供的低級C接口封裝起來供開發者使用,Python也不例外。linux
文件讀寫就是一種常見的IO操做。那麼根據上面的描述,能夠推斷python也應該封裝操做系統的底層接口,直接提供了文件讀寫相關的操做方法。事實上,也確實如此,並且Java、PHP等其餘語言也是。nginx
那麼咱們要操做的對象是什麼呢?咱們又如何獲取要操做的對象呢?sql
因爲操做I/O的能力是由操做系統提供的,且現代操做系統不容許普通程序直接操做磁盤,因此讀寫文件時須要請求操做系統打開一個對象(一般被稱爲文件描述符--file descriptor, 簡稱fd),這就是咱們在程序中要操做的文件對象。編程
一般高級編程語言中會提供一個內置的函數,經過接收"文件路徑"以及「文件打開模式」等參數來打開一個文件對象,並返回該文件對象的文件描述符。所以經過這個函數咱們就能夠獲取要操做的文件對象了。這個內置函數在Python中叫open(), 在PHP中叫fopen(),windows
不一樣的編程語言讀寫文件的操做步驟大致都是同樣的,都分爲如下幾個步驟:api
1)打開文件,獲取文件描述符 2)操做文件描述符--讀/寫 3)關閉文件
只是不一樣的編程語言提供的讀寫文件的api是不同的,有些提供的功能比較豐富,有些比較簡陋。安全
須要注意的是:文件讀寫操做完成後,應該及時關閉。一方面,文件對象會佔用操做系統的資源;另一方面,操做系統對同一時間能打開的文件描述符的數量是有限制的,在Linux操做系統上能夠經過ulimit -n
來查看這個顯示數量。若是不及時關閉文件,還可能會形成數據丟失。由於我將數據寫入文件時,操做系統不會馬上把數據寫入磁盤,而是先把數據放到內存緩衝區異步寫入磁盤。當調用close方法時,操做系統會保證把沒有寫入磁盤的數據所有寫到磁盤上,不然可能會丟失數據。
咱們先來看下在Python、PHP和C語言中打開文件的函數定義
# Python2 open(name[, mode[, buffering]]) # Python3 open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
resource fopen ( string $filename , string $mode [, bool $use_include_path = false [, resource $context ]] )
int open(const char * pathname, int flags);
會發現以上3種編程語言內置的打開文件的方法接收的參數中,除了都包含一個「文件路徑名稱」,還會包含一個mode參數(C語言的open函數中的flags參數做用類似)。這麼mode參數定義的是打開文件時的模式,常見的文件打開模式有:只讀、只寫、可讀可寫、只追加。不一樣的編程語言中對文件打開模式的定義有些微小的差異,咱們來看下Python中的文件打開模式有哪些。
文件打開模式 | 描述 |
---|---|
r | 以只讀模式打開文件,並將文件指針指向文件頭;若是文件不存在會報錯 |
w | 以只寫模式打開文件,並將文件指針指向文件頭;若是文件存在則將其內容清空,若是文件不存在則建立 |
a | 以只追加可寫模式打開文件,並將文件指針指向文件尾部;若是文件不存在則建立 |
r+ | 在r的基礎上增長了可寫功能 |
w+ | 在w的基礎上增長了可讀功能 |
a+ | 在a的基礎上增長了可讀功能 |
b | 讀寫二進制文件(默認是t,表示文本),須要與上面幾種模式搭配使用,如ab,wb, ab, ab+(POSIX系統,包括Linux都會忽略該字符) |
思考1: r+、w+和a+均可以實現對文件的讀寫,那麼他們有什麼區別呢?
思考2: 爲何要定義這些模式呢?爲何不能像咱們用word打開一篇文檔同樣既能夠讀,又能夠寫,還可修改呢?
關於這個問題,我查了不少資料,也沒找到很權威的說明。在跟同行朋友交流過程當中,發現你們主要有兩種觀點:
咱們來讀取這樣一個文本文件:song.txt,該文件的字符編碼爲utf-8。
匆匆那年咱們 究竟說了幾遍 再見以後再拖延 惋惜誰有沒有 愛過不是一場 七情上面的雄辯 匆匆那年咱們 一時匆忙撂下 難以承受的諾言 只有等別人兌現
Python3實現:
# 第一步:(以只讀模式)打開文件 f = open('song.txt', 'r', encoding='utf-8') # 第二步:讀取文件內容 print(f.read()) # 第三步:關閉文件 f.close()
這裏說下Python2的實現
# 第一步:(以只讀模式)打開文件 f = open('song.txt', 'r') # 第二步:讀取文件內容 print(f.read().decode('utf-8')) # 第三步:關閉文件 f.close()
說明:
Python3中已經內置對Unicode的支持,字符串str已是真正的Unicode字符串。也就是說Python3中的文件讀取方法已經自動完成了解碼處理,所以無需再手動進行解碼,能夠直接將讀取的文件中的內容進行打印;Python2中的字符串str是字節串,讀取文件獲得的也是字節串,在打印以前應該手動將其解碼成Unicode字符串。關於這部分的說明,能夠參考以前這篇文章<<再談Python中的字符串與字符編碼>>。
在實現基本功能的前提下,考慮一些可能的意外因素。由於文件讀寫時都有可能產生IO錯誤(IOError),一旦出錯,後面包括f.close()在內的全部代碼都不會執行了。所以咱們要保證文件不管如何都能被關閉。那麼能夠用try...finally來實現,這實際上就是try...except..finally的簡化版(咱們只用Python3來進行示例演示):
f = '' try: f = open('song.txt', 'r', encoding='utf-8') print(f.read()) num = 10 / 0 finally: print('>>>>>>finally') if f: f.close()
輸出結果:
匆匆那年咱們 究竟說了幾遍 再見以後再拖延 惋惜誰有沒有 愛過不是一場 七情上面的雄辯 匆匆那年咱們 一時匆忙撂下 難以承受的諾言 只有等別人兌現 >>>>>>finally Traceback (most recent call last): File "<stdin>", line 4, in <module> ZeroDivisionError: division by zero
輸出結果說明,儘管with代碼塊中出現了異常,可是」>>>>>>finally「 信息仍是被打印了,說明finally代碼塊被執行,即文件關閉操做被執行。可是結果中錯誤信息仍是被輸出了,所以仍是建議用一個完成的try...except...finally語句對異常信息進行捕獲和處理。
爲了不忘記或者爲了不每次都要手動關閉文件,咱們可使用with語句(一種語法糖,語法糖語句一般是爲了簡化某些操做而設計的)。with語句會在其代碼塊執行完畢以後自動關閉文件。所以咱們能夠這樣來改寫上面的程序:
with open('song.txt', 'r', encoding='utf-8') as f: print(f.read()) print(f.closed)
輸出結果:
匆匆那年咱們 究竟說了幾遍 再見以後再拖延 惋惜誰有沒有 愛過不是一場 七情上面的雄辯 匆匆那年咱們 一時匆忙撂下 難以承受的諾言 只有等別人兌現 True
是否是變得簡介多了,代碼結構也比較清晰了。with以後打印的f.closed屬性值爲True,說明文件確實被關閉了。
思考:
with語句會幫咱們自動處理異常信息嗎?
要回答這個問題就要提到「上下文管理器」 和 with語句的工做流程。
with語句不只僅能夠用於文件操做,它其實是一個很通用的結構,容許使用所謂的上下文管理器(context manager)。上下文管理器是一種支持__enter__()和__exit__()這兩個方法的對象。__enter__()方法不帶任何參數,它在進入with語句塊的時候被調用,該方法的返回值會被賦值給as關鍵字以後的變量。__exit__()方法帶有3個參數:type(異常類型), value(異常信息), trace(異常棧),當with語句的代碼塊執行完畢或執行過程當中由於異常而被終止都會調用__exit__()方法。正常退出時該方法的3個參數都爲None,異常退出時該方法的3個參數會被分別賦值。若是__exit__()方法返回值(真值測試結果)爲True則表示異常已經被處理,命令執行結果中就不會拋出異常信息了;反之,若是__exit__()方法返回值(真值測試結果)爲False,則表示異常沒有被處理而且會向外拋出該異常。
如今咱們應該明白了,異常信息會不會被處理是由with後的語句返回對象的__exit__()方法決定的。文件能夠被用做上下文管理器。它的__enter__方法返回文件對象自己,__exit__方法會關閉文件並返回None。咱們看下file類中關於這兩個方法的實現:
def __enter__(self): # real signature unknown; restored from __doc__ """ __enter__() -> self. """ return self def __exit__(self, *excinfo): # real signature unknown; restored from __doc__ """ __exit__(*excinfo) -> None. Closes the file. """ pass
可見,file類的__exit__()方法的返回值爲None,None的真值測試結果爲False,所以用於文件讀寫的with語句代碼塊中的異常信息仍是會被拋出來,須要咱們本身去捕獲並處理。
with open('song.txt', 'r', encoding='utf-8') as f: print(f.read()) num = 10 / 0
輸出結果:
匆匆那年咱們 究竟說了幾遍 再見以後再拖延 惋惜誰有沒有 愛過不是一場 七情上面的雄辯 匆匆那年咱們 一時匆忙撂下 難以承受的諾言 只有等別人兌現 Traceback (most recent call last): File "<stdin>", line 3, in <module> ZeroDivisionError: division by zero
注意: 上面所說的__exit__()方法返回值(真值測試結果)爲True則表示異常已經被處理,指的是with代碼塊中出現的異常。它對於with關鍵字以後的代碼中出現的異常是不起做用的,由於尚未進入上下文管理器就已經發生異常了。所以,不管如何,仍是建議在必要的時候在with語句外面套上一層try...except來捕獲和處理異常。
有關「上下文管理器」這個強大且高級的特性的更多信息,請參看Python參考手冊中的上下文管理器部分。或者能夠在Python庫參考中查看上下文管理器和contextlib部分。
咱們知道,對文件的讀取操做須要將文件中的數據加載到內存中,而上面所用到的read()方法會一次性把文件中全部的內容所有加載到內存中。這明顯是不合理的,當遇到一個幾個G的的文件時,必然會耗光機器的內存。這裏咱們來介紹下Python中讀取文件的相關方法:
方法 | 描述 |
---|---|
read() | 一次讀取文件全部內容,返回一個str |
read(size) | 每次最多讀取指定長度的內容,返回一個str;在Python2中size指定的是字節長度,在Python3中size指定的是字符長度 |
readlines() | 一次讀取文件全部內容,按行返回一個list |
readline() | 每次只讀取一行內容 |
此外,還要兩個與文件指針位置相關的方法
方法 | 描述 |
---|---|
seek(n) | 將文件指針移動到指定字節的位置 |
tell() | 獲取當前文件指針所在字節位置 |
下面來看下操做實例
with open('song.txt', 'r') as f: print(f.read(12).decode('utf-8'))
輸出結果:
匆匆那年
結果說明:Python2中read(size)方法的size參數指定的要讀取的字節數,而song.txt文件是UTF-8編碼的內容,一個漢字佔3個字節,所以12個字節恰好是4個漢字。
with open('song.txt', 'r', encoding='utf-8') as f: print(f.read(12))
輸出結果:
匆匆那年咱們 究竟說
結果說明:Python3中read(size)方法的size參數指定的要讀取的字符數,這與文件的字符編碼無關,就是返回12個字符。
with open('song.txt', 'r', encoding='utf-8') as f: print(f.readline())
with open('song.txt', 'r') as f: print(f.readline().decode('utf-8'))
輸出結果都同樣:
匆匆那年咱們 究竟說了幾遍 再見以後再拖延
這裏咱們只以Python3來進行實例操做,Python2僅僅是須要在讀取到內容後進行手動解碼而已,上面已經有示例。
with open('song.txt', 'r', encoding='utf-8') as f: for line in f.readlines(): print(line)
輸出結果:
匆匆那年咱們 究竟說了幾遍 再見以後再拖延 惋惜誰有沒有 愛過不是一場 七情上面的雄辯 匆匆那年咱們 一時匆忙撂下 難以承受的諾言 只有等別人兌現
這種方式的缺點與read()方法是同樣的,都是會消耗大量的內存空間。
with open('song.txt', 'r', encoding='utf-8', newline='') as f: for line in f: print(line)
輸出結果:
匆匆那年咱們 究竟說了幾遍 再見以後再拖延 惋惜誰有沒有 愛過不是一場 七情上面的雄辯 匆匆那年咱們 一時匆忙撂下 難以承受的諾言 只有等別人兌現
另外,發現上面的輸出結果中行與行之間多了一個空行。這是由於文件每一行的默認都有換行符,而print()方法也會輸出換行,所以就多了一個空行。去掉空行也比較簡單:能夠用line.rstrip()
去除字符串右邊的換行符,也能夠經過print(line, end='')避免print方法形成的換行。
file類的其餘方法:
方法 | 描述 |
---|---|
flush() | 刷新緩衝區數據,將緩衝區中的數據馬上寫入文件 |
next() | 返回文件下一行,這個方法也是file對象實例能夠被當作迭代器使用的緣由 |
truncate([size]) | 截取文件中指定字節數的內容,並覆蓋保存到文件中,若是不指定size參數則文件將被清空; Python2無返回值,Python3返回新文件的內容字節數 |
write(str) | 將字符串寫入文件,沒有返回值 |
writelines(sequence) | 向文件寫入一個字符串或一個字符串列表,若是字符串列表中的元素須要換行要本身加入換行符 |
fileno() | 返回一個整型的文件描述符,能夠用於一些底層IO操做上(如,os模塊的read方法) |
isatty() | 判斷文件是否被鏈接到一個虛擬終端,是則返回True,不然返回False |
前面已經寫過一篇介紹Python中字符編碼的相關文件<<再談Python中的字符串與字符編碼>> 裏面花了很大的篇幅介紹Python中字符串與字符編碼的關係以及轉換過程。其中談到過兩個指定的字符編碼的地方,及其做用:
*-* coding:utf-8 -*-
: 它的主要做用是告訴Python解釋器當前python代碼文件保存時所使用的字符編碼,Python解釋器在執行代碼以前,須要先從磁盤讀取該代碼文件中的字節而後經過這裏指定的字符編碼將其解碼爲unicode字符。Python解釋器執行Python代碼的過程與IDE開發工具是沒有什麼關聯性的。那麼這裏爲何又要談起字符編碼的問題呢?
或者換個問法,既然從上面已經指定了字符編碼,爲何對文件進行讀寫時還要指定字符編碼呢?從前面的描述能夠看出:上面兩個地方指定的是Python代碼文件的字符編碼,是給Python解釋器和Pycharm等程序軟件用的;而被讀寫文件的字符編碼與Python代碼文件的字符編碼沒有必然聯繫,讀寫文件時指定的字符編碼是給咱們寫的程序軟件用的。這是不一樣的主體和過程,但願我說明白了。
讀寫文件時怎樣指定字符編碼呢?
上面解釋了讀寫文件爲何要指定字符編碼,這裏要說下怎樣指定字符編碼(其實這裏主要討論是讀取外部數據時的情形)。這個問題其實在上面的文件讀取示例中已經使用過了,這裏咱們再詳細的說一下。
首先,再次看一下Python2和Python3中open函數的定義:
# Python2 open(name[, mode[, buffering]]) # Python3 open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
能夠看到,Python3的open函數中多了幾個參數,其中包括一個encoding參數。是的,這個encoding就是用來指定被操做文件的字符編碼的。
# 讀操做 with open('song.txt', 'r', encoding='utf-8') as f: print(f.read()) # 寫操做 with open('song.txt', 'w', encoding='utf-8') as f: print(f.write('你好'))
那麼Python2中怎樣指定呢?Python2中的對文件的read和write操做都是字節,也就說Python2中文件的read相關方法讀取的是字節串(若是包含中文字符,會發現len()方法的結果不等於讀取到的字符個數,而是字節數)。若是咱們要獲得 正確的字符串,須要手動將讀取到的結果decode(解碼)爲字符串;相反,要以特定的字符編碼保存要寫入的數據時,須要手動encode(編碼)爲字節串。這個encode()和decode()函數能夠接收一個字符編碼參數。Python3中read和write操做的都是字符串,其實是Python解釋器幫咱們自動完成了寫入時的encode(編碼)和讀取時的decode(解碼)操做,所以咱們只須要在打開文件(open函數)時指定字符編碼就能夠了。
# 讀操做 with open('song.txt', 'r') as f: print(f.read().decode('utf-8')) # 寫操做 with open('song2.txt', 'w') as f: # f.write(u'你好'.encode('utf-8')) # f.write('你好'.decode('utf-8').encode('utf-8')) f.write('你好')
Python3中open函數的encoding參數顯然是能夠不指定的,這時候就會用一個「默認字符編碼」。
看下Python3中open函數文檔對encoding參數的說明:
encoding is the name of the encoding used to decode or encode the file. This should only be used in text mode. The default encoding is platform dependent, but any encoding supported by Python can be passed. See the codecs module for the list of supported encodings.
也就是說,encoding參數的默認值是與平臺有關的,好比Window上默認字符編碼爲GBK,Linux上默認字符編碼爲UTF-8。
而對於Python2來講,在進行文件寫操做時,字節會被直接保存;在進行文件讀操做時,若是不手動進行來decode操做天然也就用不着默認字符編碼了。可是這時候在不一樣的字符終端打印的時候,會用當前平臺的字符編碼自動將字節解碼爲字符,此時可能會出現亂碼。如song.txt文件時UTF-8編碼的,在windows(字符編碼爲GBK)的命令行終端進行以下操做就會出現亂碼:
>>> with open('song.txt', 'r') as f: ... print(f.read()) ... 鍖嗗寙閭e勾鎴戜滑 絀剁珶璇翠簡鍑犻亶 鍐嶈涔嬪悗鍐嶆嫋寤? 鍙儨璋佹湁娌℃湁 鐖辮繃涓嶆槸涓€鍦?涓冩儏涓婇潰鐨勯泟杈? 鍖嗗寙閭e勾鎴戜滑 涓€鏃跺寙蹇欐拏涓?闅句互鎵垮彈鐨勮璦€ 鍙湁絳夊埆浜哄厬鐜
咱們應該儘量的獲取被操做文件的字符編碼,並明確指定encoding參數的值。
本文轉載自:https://www.cnblogs.com/yyds/p/6186621.html