廖雪峯《python3 基礎教程》讀書筆記——第十一章 IO編程

第十一章 IO編程

1、IO在計算機中指Input/Output,也就是輸入和輸出。由於程序和運行時數據是在內存中駐留,由CPU這個超快的計算核心來執行,涉及到數據交換的地方,通常是磁盤、網絡等,就需要IO接口。

2、比如你打開瀏覽器,訪問新浪首頁,瀏覽器這個程序就需要通過網絡IO獲取新浪的網頁。瀏覽器首先會發送數據給新浪服務器,告訴它我想要首頁的HTML,這個動作是往外發數據,叫Output,隨後新浪服務器把網頁發過來,這個動作是從外面接收數據,叫Input。所以,通常,程序完成IO操作會有Input和Output兩個數據流。當然也有隻用一個的情況,比如,從磁盤讀取文件到內存,就只有Input操作,反過來,把數據寫到磁盤文件裏,就只是一個Output操作。

3、IO編程中,Stream(流)是一個很重要的概念,可以把流想象成一個水管,數據就是水管裏的水,但是隻能單向流動。Input Stream就是數據從外面(磁盤、網絡)流進內存,Output Stream就是數據從內存流到外面去。對於瀏覽網頁來說,瀏覽器和新浪服務器之間至少需要建立兩根水管,纔可以既能發數據,又能收數據。

4、由於CPU和內存的速度遠遠高於外設的速度,所以,在IO編程中,就存在速度嚴重不匹配的問題。舉個例子來說,比如要把100M的數據寫入磁盤,CPU輸出100M的數據只需要0.01秒,可是磁盤要接收這100M數據可能需要10秒,怎麼辦呢?有兩種辦法:

 

1)同步IO:第一種是CPU等着,也就是程序暫停執行後續代碼,等100M的數據在10秒後寫入磁盤,再接着往下執行,這種模式稱爲同步IO;

2)異步IO:另一種方法是CPU不等待,只是告訴磁盤,“您老慢慢寫,不着急,我接着幹別的事去了”,於是,後續代碼可以立刻接着執行,這種模式稱爲異步IO。

4、同步和異步的區別就在於是否等待IO執行的結果。好比你去麥當勞點餐,你說“來個漢堡”,服務員告訴你,對不起,漢堡要現做,需要等5分鐘,於是你站在收銀臺前面等了5分鐘,拿到漢堡再去逛商場,這是同步IO。

你說“來個漢堡”,服務員告訴你,漢堡需要等5分鐘,你可以先去逛商場,等做好了,我們再通知你,這樣你可以立刻去幹別的事情(逛商場),這是異步IO。

很明顯,使用異步IO來編寫程序性能會遠遠高於同步IO,但是異步IO的缺點是編程模型複雜。想想看,你得知道什麼時候通知你“漢堡做好了”,而通知你的方法也各不相同。如果是服務員跑過來找到你,這是回調模式,如果服務員發短信通知你,你就得不停地檢查手機,這是輪詢模式。總之,異步IO的複雜度遠遠高於同步IO。

5、IO封裝

操作IO的能力都是由操作系統提供的,每一種編程語言都會把操作系統提供的低級C接口封裝起來方便使用,Python也不例外。我們後面會詳細討論Python的IO編程接口。

 

注意,本章的IO編程都是同步模式,異步IO由於複雜度太高,後續涉及到服務器端程序開發時我們再討論。

 

11.1 文件讀寫

讀寫文件是最常見的IO操作。Python內置了讀寫文件的函數,用法和C是兼容的。

讀寫文件前,我們先必須瞭解一下,在磁盤上讀寫文件的功能都是由操作系統提供的,現代操作系統不允許普通的程序直接操作磁盤,

所以,讀寫文件就是請求操作系統打開一個文件對象(通常稱爲文件描述符),然後,通過操作系統提供的接口從這個文件對象中讀取數據(讀文件),或者把數據寫入這個文件對象(寫文件)。

一、讀文件——先後調用open()、read()、close()

1、要以讀文件的模式打開一個文件對象,使用Python內置的open()函數,傳入文件名和標示符:

>>> f = open('/Users/michael/test.txt', 'r')

標示符'r'表示讀,這樣,我們就成功地打開了一個文件。

如果文件不存在,open()函數就會拋出一個IOError的錯誤,並且給出錯誤碼和詳細的信息告訴你文件不存在:

>>> f=open('/Users/michael/notfound.txt', 'r')

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

FileNotFoundError: [Errno 2] No such file or directory: '/Users/michael/notfound.txt'

如果文件打開成功,接下來,調用read()方法可以一次讀取文件的全部內容,Python把內容讀到內存,用一個str對象表示:

>>> f.read()

'Hello, world!'

最後一步是調用close()方法關閉文件。文件使用完畢後必須關閉,因爲文件對象會佔用操作系統的資源,並且操作系統同一時間能打開的文件數量也是有限的:

 

>>> f.close()

由於文件讀寫時都有可能產生IOError,一旦出錯,後面的f.close()就不會調用。

2、所以,爲了保證無論是否出錯都能正確地關閉文件,我們可以使用try ... finally來實現:

try:

    f = open('/path/to/file', 'r')

    print(f.read())

finally:

    if f:

        f.close()

但是每次都這麼寫實在太繁瑣,所以,

3、Python引入了with語句來自動幫我們調用close()方法:

with open('/path/to/file', 'r') as f:

    print(f.read())

這和前面的try ... finally是一樣的,但是代碼更佳簡潔,並且不必調用f.close()方法。

調用read()會一次性讀取文件的全部內容,如果文件有10G,內存就爆了,所以,要保險起見,

4、可以反覆調用read(size)方法,每次最多讀取size個字節的內容。另外,調用readline()可以每次讀取一行內容,調用readlines()一次讀取所有內容並按行返回list。因此,要根據需要決定怎麼調用。

5、如果文件很小,read()一次性讀取最方便;如果不能確定文件大小,反覆調用read(size)比較保險;如果是配置文件,調用readlines()最方便:

for line in f.readlines():

    print(line.strip()) # 把末尾的'\n'刪掉

 

6、file-like Object

open()函數返回的這種有個read()方法的對象,在Python中統稱爲file-like Object。除了file外,還可以是內存的字節流,網絡流,自定義流等等。file-like Object不要求從特定類繼承,只要寫個read()方法就行。

StringIO就是在內存中創建的file-like Object,常用作臨時緩衝。

 

7、二進制文件

前面講的默認都是讀取文本文件,並且是UTF-8編碼的文本文件。要讀取二進制文件,比如圖片、視頻等等,用'rb'模式打開文件即可:

>>> f = open('/Users/michael/test.jpg', 'rb')

>>> f.read()

b'\xff\xd8\xff\xe1\x00\x18Exif\x00\x00...' # 十六進制表示的字節

8、字符編碼

要讀取非UTF-8編碼的文本文件,需要給open()函數傳入encoding參數,例如,讀取GBK編碼的文件:

>>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk')

>>> f.read()

'測試'

遇到有些編碼不規範的文件,你可能會遇到UnicodeDecodeError,因爲在文本文件中可能夾雜了一些非法編碼的字符。遇到這種情況,open()函數還接收一個errors參數,表示如果遇到編碼錯誤後如何處理。最簡單的方式是直接忽略:

 

>>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk', errors='ignore')

 

二、寫文件

1、open()、write()、close()函數

寫文件和讀文件是一樣的,唯一區別是調用open()函數時,傳入標識符'w'或者'wb'表示寫文本文件或寫二進制文件:

>>> f = open('/Users/michael/test.txt', 'w')

>>> f.write('Hello, world!')

>>> f.close()

你可以反覆調用write()來寫入文件,但是務必要調用f.close()來關閉文件。當我們寫文件時,操作系統往往不會立刻把數據寫入磁盤,而是放到內存緩存起來,空閒的時候再慢慢寫入。只有調用close()方法時,操作系統才保證把沒有寫入的數據全部寫入磁盤。忘記調用close()的後果是數據可能只寫了一部分到磁盤,剩下的丟失了。

2、所以,還是用with語句來得保險:

with open('/Users/michael/test.txt', 'w') as f:

    f.write('Hello, world!')

要寫入特定編碼的文本文件,請給open()函數傳入encoding參數,將字符串自動轉換成指定編碼。

 

小結

Python中,文件讀寫是通過open()函數打開的文件對象完成的。使用with語句操作文件IO是個好習慣。

 

11.2 StringIO和BytesIO

一:StringIO

1、StringIO顧名思義就是在內存中讀寫str。

2、要把str寫入StringIO,我們需要先創建一個StringIO,然後,像文件一樣寫入即可:

>>> from io import StringIO

>>> f = StringIO()

>>> f.write('hello')

5

>>> f.write(' ')

1

>>> f.write('world!')

6

>>> print(f.getvalue())

hello world!

3、getvalue()方法用於獲得寫入後的str。

要讀取StringIO,可以用一個str初始化StringIO,然後,像讀文件一樣讀取:

>>> from io import StringIO

>>> f = StringIO('Hello!\nHi!\nGoodbye!')

>>> while True:

    s = f.readline()

     if s == '':

         break

     print(s.strip())

Hello!

Hi!

Goodbye!

 

二、BytesIO

StringIO操作的只能是str,如果要操作二進制數據,就需要使用

BytesIO。

BytesIO實現了在內存中讀寫bytes,我們創建一個BytesIO,然後寫入一些bytes:

>>> from io import BytesIO

>>> f = BytesIO()

>>> f.write('中文'.encode('utf-8'))

6

>>> print(f.getvalue())

b'\xe4\xb8\xad\xe6\x96\x87'

請注意,寫入的不是str,而是經過UTF-8編碼的bytes。

StringIO類似,可以用一個bytes初始化BytesIO,然後,像讀文件一樣讀取:

>>> from io import BytesIO

>>> f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')

>>> f.read()

b'\xe4\xb8\xad\xe6\x96\x87'

【小結】

StringIO和BytesIO是在內存中操作str和bytes的方法,使得和讀寫文件具有一致的接口。

 

11.3 操作文件和目錄

如果我們要操作文件、目錄,可以在命令行下面輸入操作系統提供的各種命令來完成。比如dircp等命令。

Python內置的os模塊也可以直接調用操作系統提供的接口函數。

使用os模塊的基本功能:

>>> import os

>>> os.name # 操作系統類型

'posix'

如果是posix,說明系統是Linux、Unix或Mac OS X,如果是nt,就是Windows系統。

1、環境變量

在操作系統中定義的環境變量,全部保存在os.environ這個變量中,可以直接查看:

>>> os.environ

要獲取某個環境變量的值,可以調用os.environ.get('key'):

 

>>> os.environ.get('PATH')

>>> os.environ.get('x', 'default')

'default'

2、操作文件和目錄

操作文件和目錄的函數一部分放在os模塊中,一部分放在os.path模塊中,這一點要注意一下。查看、創建和刪除目錄可以這麼調用:

# 查看當前目錄的絕對路徑:

>>> os.path.abspath('.')

'/Users/michael'

# 在某個目錄下創建一個新目錄,首先把新目錄的完整路徑表示出來:

>>> os.path.join('/Users/michael', 'testdir')

'/Users/michael/testdir'

# 然後創建一個目錄:

>>> os.mkdir('/Users/michael/testdir')

# 刪掉一個目錄:

>>> os.rmdir('/Users/michael/testdir')

把兩個路徑合成一個時,不要直接拼字符串,而要通過os.path.join()函數,這樣可以正確處理不同操作系統的路徑分隔符。Linux/Unix/Mac下,os.path.join()返回這樣的字符串:

 

part-1/part-2

Windows下會返回這樣的字符串:

part-1\part-2

同樣的道理,要拆分路徑時,也不要直接去拆字符串,而要通過os.path.split()函數,這樣可以把一個路徑拆分爲兩部分,後一部分總是最後級別的目錄或文件名:

>>> os.path.split('/Users/michael/testdir/file.txt')

('/Users/michael/testdir', 'file.txt')

os.path.splitext()可以直接讓你得到文件擴展名,很多時候非常方便:

>>> os.path.splitext('/path/to/file.txt')

('/path/to/file', '.txt')

這些合併、拆分路徑的函數並不要求目錄和文件要真實存在,它們只對字符串進行操作。

文件操作使用下面的函數。

假定當前目錄下有一個test.txt文件:

# 對文件重命名:

>>> os.rename('test.txt', 'test.py')

# 刪掉文件:

>>> os.remove('test.py')

但是複製文件的函數居然在os模塊中不存在!原因是複製文件並非由操作系統提供的系統調用。理論上講,我們通過上一節的讀寫文件可以完成文件複製,只不過要多寫很多代碼。

幸運的是shutil模塊提供了copyfile()的函數,你還可以在shutil模塊中找到很多實用函數,它們可以看做是os模塊的補充。

最後看看如何利用Python的特性來過濾文件。比如我們要列出當前目錄下的所有目錄,只需要一行代碼:

>>> [x for x in os.listdir('.') if os.path.isdir(x)]

['.lein', '.local', '.m2', '.npm', '.ssh', '.Trash', '.vim', 'Applications', 'Desktop', ...]

要列出所有的.py文件,也只需一行代碼:

>>> [x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1]=='.py']

['apis.py', 'config.py', 'models.py', 'pymonitor.py', 'test_db.py', 'urls.py', 'wsgiapp.py']

是不是非常簡潔?

【小結】

Python的os模塊封裝了操作系統的目錄和文件操作,要注意這些函數有的在os模塊中,有的在os.path模塊中。

shutil模塊提供了copyfile()的函數,你還可以在shutil模塊中找到很多實用函數,它們可以看做是os模塊的補充。

 

 

 

11.4 序列化

在程序運行的過程中,所有的變量都是在內存中,比如,定義一個dict:

d = dict(name='Bob', age=20, score=88)

可以隨時修改變量,比如把name改成'Bill',但是一旦程序結束,變量所佔用的內存就被操作系統全部回收。如果沒有把修改後的'Bill'存儲到磁盤上,下次重新運行程序,變量又被初始化爲'Bob'。

1、我們把變量從內存中變成可存儲或傳輸的過程稱之爲序列化,在Python中叫pickling,

在其他語言中也被稱之爲serialization,marshalling,flattening等等,都是一個意思。

序列化之後,就可以把序列化後的內容寫入磁盤,或者通過網絡傳輸到別的機器上。

反過來,把變量內容從序列化的對象重新讀到內存裏稱之爲反序列化,即unpickling。

2、Python提供了pickle模塊來實現序列化。

首先,我們嘗試把一個對象序列化並寫入文件:

>>> import pickle

>>> d = dict(name='Bob', age=20, score=88)

>>> pickle.dumps(d)

b'\x80\x03}q\x00(X\x03\x00\x00\x00ageq\x01K\x14X\x05\x00\x00\x00scoreq\x02KXX\x04\x00\x00\x00nameq\x03X\x03\x00\x00\x00Bobq\x04u.'

3、pickle.dumps()方法把任意對象序列化成一個bytes,然後,就可以把這個bytes寫入文件。

4、或者用另一個方法pickle.dump()直接把對象序列化後寫入一個file-like Object:

>>> f = open('dump.txt', 'wb')

>>> pickle.dump(d, f)

>>> f.close()

看看寫入的dump.txt文件,一堆亂七八糟的內容,這些都是Python保存的對象內部信息。

5、當我們要把對象從磁盤讀到內存時,可以先把內容讀到一個bytes,然後用pickle.loads()方法反序列化出對象,也可以直接用pickle.load()方法從一個file-like Object中直接反序列化出對象。我們打開另一個Python命令行來反序列化剛纔保存的對象:

>>> f = open('dump.txt', 'rb')

>>> d = pickle.load(f)

>>> f.close()

>>> d

{'age': 20, 'score': 88, 'name': 'Bob'}

變量的內容又回來了!

當然,這個變量和原來的變量是完全不相干的對象,它們只是內容相同而已。

6、Pickle的問題和所有其他編程語言特有的序列化問題一樣,就是它只能用於Python,並且可能不同版本的Python彼此都不兼容,因此,只能用Pickle保存那些不重要的數據,不能成功地反序列化也沒關係。

 

二、JSON

1、如果我們要在不同的編程語言之間傳遞對象,就必須把對象序列化爲標準格式,比如XML,但更好的方法是序列化爲JSON,因爲JSON表示出來就是一個字符串,可以被所有語言讀取,也可以方便地存儲到磁盤或者通過網絡傳輸。JSON不僅是標準格式,並且比XML更快,而且可以直接在Web頁面中讀取,非常方便。

JSON表示的對象就是標準的JavaScript語言的對象,JSON和Python內置的數據類型對應如下:

JSON類型 Python類型

{} dict

[] list

"string" str

1234.56 int或float

true/false True/False

null None

2、Python內置的json模塊提供了非常完善的Python對象到JSON格式的轉換。我們先看看如何把Python對象變成一個JSON:

 

>>> import json

>>> d = dict(name='Bob', age=20, score=88)

>>> json.dumps(d)

'{"age": 20, "score": 88, "name": "Bob"}'

dumps()方法返回一個str,內容就是標準的JSON。類似的,dump()方法可以直接把JSON寫入一個file-like Object。

3、要把JSON反序列化爲Python對象,用loads()或者對應的load()方法,前者把JSON的字符串反序列化,後者從file-like Object中讀取字符串並反序列化:

>>> json_str = '{"age": 20, "score": 88, "name": "Bob"}'

>>> json.loads(json_str)

{'age': 20, 'score': 88, 'name': 'Bob'}

由於JSON標準規定JSON編碼是UTF-8,所以我們總是能正確地在Python的str與JSON的字符串之間轉換。

4、JSON進階

Python的dict對象可以直接序列化爲JSON的{},不過,很多時候,我們更喜歡用class表示對象,比如定義Student類,然後序列化:

import json

class Student(object):

    def __init__(self, name, age, score):

        self.name = name

        self.age = age

        self.score = score

s = Student('Bob', 20, 88)

print(json.dumps(s))

運行代碼,毫不留情地得到一個TypeError:

Traceback (most recent call last):

  ...

TypeError: <__main__.Student object at 0x10603cc50> is not JSON serializable

錯誤的原因是Student對象不是一個可序列化爲JSON的對象。

如果連class的實例對象都無法序列化爲JSON,這肯定不合理!

別急,我們仔細看看dumps()方法的參數列表,可以發現,除了第一個必須的obj參數外,dumps()方法還提供了一大堆的可選參數:

https://docs.python.org/3/library/json.html#json.dumps

這些可選參數就是讓我們來定製JSON序列化。前面的代碼之所以無法把Student類實例序列化爲JSON,是因爲默認情況下,dumps()方法不知道如何將Student實例變爲一個JSON的{}對象。

可選參數default就是把任意一個對象變成一個可序列爲JSON的對象,我們只需要爲Student專門寫一個轉換函數,再把函數傳進去即可:

def student2dict(std):

    return {

        'name': std.name,

        'age': std.age,

        'score': std.score

    }

這樣,Student實例首先被student2dict()函數轉換成dict,然後再被順利序列化爲JSON:

>>> print(json.dumps(s, default=student2dict))

{"age": 20, "name": "Bob", "score": 88}

不過,下次如果遇到一個Teacher類的實例,照樣無法序列化爲JSON。我們可以偷個懶,把任意class的實例變爲dict:

print(json.dumps(s, default=lambda obj: obj.__dict__))

因爲通常class的實例都有一個__dict__屬性,它就是一個dict,用來存儲實例變量。也有少數例外,比如定義了__slots__的class。

 

同樣的道理,如果我們要把JSON反序列化爲一個Student對象實例,loads()方法首先轉換出一個dict對象,然後,我們傳入的object_hook函數負責把dict轉換爲Student實例:

 

def dict2student(d):

    return Student(d['name'], d['age'], d['score'])

運行結果如下:

 

>>> json_str = '{"age": 20, "score": 88, "name": "Bob"}'

>>> print(json.loads(json_str, object_hook=dict2student))

<__main__.Student object at 0x10cd3c190>

打印出的是反序列化的Student實例對象。

 

【小結】

1) Python語言特定的序列化模塊是pickle,

2) 但如果要把序列化搞得更通用、更符合Web標準,就可以使用json模塊。json模塊的dumps()和loads()函數是定義得非常好的接口的典範。

3) 當我們使用時,只需要傳入一個必須的參數。但是,當默認的序列化或反序列機制不滿足我們的要求時,我們又可以傳入更多的參數來定製序列化或反序列化的規則,既做到了接口簡單易用,又做到了充分的擴展性和靈活性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章