文件處理
讀寫文件是最常見的IO操作。Python內(nèi)置了讀寫文件的函數(shù)刹孔,用法和C是兼容的啡省。
讀寫文件前,我們先必須了解一下芦疏,在磁盤上讀寫文件的功能都是由操作系統(tǒng)提供的冕杠,現(xiàn)代操作系統(tǒng)不允許普通的程序直接操作磁盤,所以酸茴,讀寫文件就是請(qǐng)求操作系統(tǒng)打開一個(gè)文件對(duì)象(通常稱為文件描述符)分预,然后,通過操作系統(tǒng)提供的接口從這個(gè)文件對(duì)象中讀取數(shù)據(jù)(讀文件)薪捍,或者把數(shù)據(jù)寫入這個(gè)文件對(duì)象(寫文件)笼痹。
讀文件
要以讀文件的模式打開一個(gè)文件對(duì)象配喳,使用Python內(nèi)置的open()
函數(shù),傳入文件名和標(biāo)示符:
>>> f = open('/Users/michael/test.txt', 'r')
標(biāo)示符'r'表示讀凳干,這樣晴裹,我們就成功地打開了一個(gè)文件。
如果文件不存在救赐,open()
函數(shù)就會(huì)拋出一個(gè)IOError
的錯(cuò)誤涧团,并且給出錯(cuò)誤碼和詳細(xì)的信息告訴你文件不存在:
>>> f=open('/Users/michael/notfound.txt', 'r')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IOError: [Errno 2] No such file or directory: '/Users/michael/notfound.txt'
如果文件打開成功,接下來经磅,調(diào)用read()
方法可以一次讀取文件的全部?jī)?nèi)容泌绣,Python把內(nèi)容讀到內(nèi)存,用一個(gè)str
對(duì)象表示:
>>> f.read()
'Hello, world!'
最后一步是調(diào)用close()
方法關(guān)閉文件预厌。文件使用完畢后必須關(guān)閉阿迈,因?yàn)槲募?duì)象會(huì)占用操作系統(tǒng)的資源,并且操作系統(tǒng)同一時(shí)間能打開的文件數(shù)量也是有限的:
>>> f.close()
由于文件讀寫時(shí)都有可能產(chǎn)生IOError
轧叽,一旦出錯(cuò)苗沧,后面的f.close()
就不會(huì)調(diào)用。所以炭晒,為了保證無論是否出錯(cuò)都能正確地關(guān)閉文件待逞,我們可以使用try ... finally
來實(shí)現(xiàn):
try:
f = open('/path/to/file', 'r')
print f.read()
finally:
if f:
f.close()
但是每次都這么寫實(shí)在太繁瑣,所以腰埂,Python引入了with
語句來自動(dòng)幫我們調(diào)用close()
方法:
with open('/path/to/file', 'r') as f:
print f.read()
這和前面的try ... finally
是一樣的飒焦,但是代碼更佳簡(jiǎn)潔,并且不必調(diào)用f.close()
方法屿笼。
調(diào)用read()
會(huì)一次性讀取文件的全部?jī)?nèi)容牺荠,如果文件有10G,內(nèi)存就爆了驴一,所以休雌,要保險(xiǎn)起見,可以反復(fù)調(diào)用read(size)
方法肝断,每次最多讀取size個(gè)字節(jié)的內(nèi)容杈曲。另外,調(diào)用readline()
可以每次讀取一行內(nèi)容胸懈,調(diào)用readlines()
一次讀取所有內(nèi)容并按行返回list
担扑。因此,要根據(jù)需要決定怎么調(diào)用趣钱。
如果文件很小涌献,read()
一次性讀取最方便;如果不能確定文件大小首有,反復(fù)調(diào)用read(size)
比較保險(xiǎn)燕垃;如果是配置文件枢劝,調(diào)用readlines()
最方便:
for line in f.readlines():
print(line.strip()) # 把末尾的'\n'刪掉
file-like Object
像open()
函數(shù)返回的這種有個(gè)read()
方法的對(duì)象,在Python中統(tǒng)稱為file-like Object卜壕。除了file外您旁,還可以是內(nèi)存的字節(jié)流,網(wǎng)絡(luò)流轴捎,自定義流等等鹤盒。file-like Object不要求從特定類繼承,只要寫個(gè)read()
方法就行轮蜕。
StringIO
就是在內(nèi)存中創(chuàng)建的file-like Object昨悼,常用作臨時(shí)緩沖。
二進(jìn)制文件
前面講的默認(rèn)都是讀取文本文件跃洛,并且是ASCII編碼的文本文件。要讀取二進(jìn)制文件终议,比如圖片汇竭、視頻等等,用'rb'
模式打開文件即可:
>>> f = open('/Users/michael/test.jpg', 'rb')
>>> f.read()
'\xff\xd8\xff\xe1\x00\x18Exif\x00\x00...' # 十六進(jìn)制表示的字節(jié)
字符編碼
要讀取非ASCII編碼的文本文件穴张,就必須以二進(jìn)制模式打開细燎,再解碼。比如GBK編碼的文件:
>>> f = open('/Users/michael/gbk.txt', 'rb')
>>> u = f.read().decode('gbk')
>>> u
u'\u6d4b\u8bd5'
>>> print u
測(cè)試
如果每次都這么手動(dòng)轉(zhuǎn)換編碼嫌麻煩(寫程序怕麻煩是好事皂甘,不怕麻煩就會(huì)寫出又長(zhǎng)又難懂又沒法維護(hù)的代碼)玻驻,Python還提供了一個(gè)codecs
模塊幫我們?cè)谧x文件時(shí)自動(dòng)轉(zhuǎn)換編碼,直接讀出unicode:
import codecs
with codecs.open('/Users/michael/gbk.txt', 'r', 'gbk') as f:
f.read() # u'\u6d4b\u8bd5'
寫文件
寫文件和讀文件是一樣的偿枕,唯一區(qū)別是調(diào)用open()
函數(shù)時(shí)璧瞬,傳入標(biāo)識(shí)符'w'
或者'wb'
表示寫文本文件或?qū)懚M(jìn)制文件:
>>> f = open('/Users/michael/test.txt', 'w')
>>> f.write('Hello, world!')
>>> f.close()
你可以反復(fù)調(diào)用write()
來寫入文件,但是務(wù)必要調(diào)用f.close()
來關(guān)閉文件渐夸。當(dāng)我們寫文件時(shí)嗤锉,操作系統(tǒng)往往不會(huì)立刻把數(shù)據(jù)寫入磁盤,而是放到內(nèi)存緩存起來墓塌,空閑的時(shí)候再慢慢寫入瘟忱。只有調(diào)用close()
方法時(shí),操作系統(tǒng)才保證把沒有寫入的數(shù)據(jù)全部寫入磁盤苫幢。忘記調(diào)用close()
的后果是數(shù)據(jù)可能只寫了一部分到磁盤访诱,剩下的丟失了。所以韩肝,還是用with
語句來得保險(xiǎn):
with open('/Users/michael/test.txt', 'w') as f:
f.write('Hello, world!')
要寫入特定編碼的文本文件触菜,請(qǐng)效仿codecs
的示例,寫入unicode伞梯,由codecs
自動(dòng)轉(zhuǎn)換成指定編碼玫氢。
小結(jié)
函數(shù)
函數(shù)是組織好的帚屉,可重復(fù)使用的,用來實(shí)現(xiàn)單一漾峡,或相關(guān)聯(lián)功能的代碼段攻旦。
函數(shù)能提高應(yīng)用的模塊性,和代碼的重復(fù)利用率生逸。你已經(jīng)知道Python提供了許多內(nèi)建函數(shù)牢屋,比如print()。但你也可以自己創(chuàng)建函數(shù)槽袄,這被叫做用戶自定義函數(shù)烙无。
你可以定義一個(gè)由自己想要功能的函數(shù),以下是簡(jiǎn)單的規(guī)則:
1.函數(shù)代碼塊以 def 關(guān)鍵詞開頭遍尺,后接函數(shù)標(biāo)識(shí)符名稱和圓括號(hào)()截酷。
2.任何傳入?yún)?shù)和自變量必須放在圓括號(hào)中間。圓括號(hào)之間可以用于定義參數(shù)乾戏。
3.函數(shù)的第一行語句可以選擇性地使用文檔字符串—用于存放函數(shù)說明迂苛。
4.函數(shù)內(nèi)容以冒號(hào)起始,并且縮進(jìn)鼓择。
5.return [表達(dá)式] 結(jié)束函數(shù)三幻,選擇性地返回一個(gè)值給調(diào)用方。不帶表達(dá)式的return相當(dāng)于返回 None呐能。
def functionname( parameters ):
"函數(shù)_文檔字符串"
function_suite
return [expression]
函數(shù)調(diào)用
定義一個(gè)函數(shù)只給了函數(shù)一個(gè)名稱念搬,指定了函數(shù)里包含的參數(shù),和代碼塊結(jié)構(gòu)摆出。
這個(gè)函數(shù)的基本結(jié)構(gòu)完成以后朗徊,你可以通過另一個(gè)函數(shù)調(diào)用執(zhí)行,也可以直接從Python提示符執(zhí)行懊蒸。
如下實(shí)例調(diào)用了printme()函數(shù):
#!/usr/bin/python
# -*- coding: UTF-8 -*-
# 定義函數(shù)
def printme( str ):
"打印任何傳入的字符串"
print str;
return;
# 調(diào)用函數(shù)
printme("我要調(diào)用用戶自定義函數(shù)!");
printme("再次調(diào)用同一函數(shù)");
參數(shù)
以下是調(diào)用函數(shù)時(shí)可使用的正式參數(shù)類型:
1.必備參數(shù):
必備參數(shù)須以正確的順序傳入函數(shù)荣倾。調(diào)用時(shí)的數(shù)量必須和聲明時(shí)的一樣。
2.關(guān)鍵字參數(shù):
關(guān)鍵字參數(shù)和函數(shù)調(diào)用關(guān)系緊密骑丸,函數(shù)調(diào)用使用關(guān)鍵字參數(shù)來確定傳入的參數(shù)值舌仍。
使用關(guān)鍵字參數(shù)允許函數(shù)調(diào)用時(shí)參數(shù)的順序與聲明時(shí)不一致,因?yàn)?Python 解釋器能夠用參數(shù)名匹配參數(shù)值通危。
3.默認(rèn)參數(shù):
調(diào)用函數(shù)時(shí)铸豁,缺省參數(shù)的值如果沒有傳入,則被認(rèn)為是默認(rèn)值菊碟。
4.不定長(zhǎng)參數(shù):
你可能需要一個(gè)函數(shù)能處理比當(dāng)初聲明時(shí)更多的參數(shù)节芥。這些參數(shù)叫做不定長(zhǎng)參數(shù),和上述2種參數(shù)不同,聲明時(shí)不會(huì)命名头镊。
匿名函數(shù)
1.python 使用 lambda 來創(chuàng)建匿名函數(shù)蚣驼。
2.lambda只是一個(gè)表達(dá)式,函數(shù)體比def簡(jiǎn)單很多相艇。
3.lambda的主體是一個(gè)表達(dá)式颖杏,而不是一個(gè)代碼塊。僅僅能在lambda表達(dá)式中封裝有限的邏輯進(jìn)去坛芽。
4.lambda函數(shù)擁有自己的命名空間留储,且不能訪問自有參數(shù)列表之外或全局命名空間里的參數(shù)。
5.雖然lambda函數(shù)看起來只能寫一行咙轩,卻不等同于C或C++的內(nèi)聯(lián)函數(shù)获讳,后者的目的是調(diào)用小函數(shù)時(shí)不占用棧內(nèi)存從而增加運(yùn)行效率。
遞歸
在函數(shù)內(nèi)部活喊,可以調(diào)用其他函數(shù)丐膝。如果一個(gè)函數(shù)在內(nèi)部調(diào)用自身本身,這個(gè)函數(shù)就是遞歸函數(shù)胧弛。
舉個(gè)例子尤误,我們來計(jì)算階乘n! = 1 x 2 x 3 x ... x n
,用函數(shù)fact(n)
表示结缚,可以看出:
fact(n) = n! = 1 x 2 x 3 x ... x (n-1) x n = (n-1)! x n = fact(n-1) x n
所以,fact(n)
可以表示為n x fact(n-1)
软棺,只有n=1時(shí)需要特殊處理红竭。
于是,fact(n)
用遞歸的方式寫出來就是:
if n==1:
return 1
return n * fact(n - 1)
上面就是一個(gè)遞歸函數(shù)喘落∫鹣埽可以試試:
>>> fact(1)
1
>>> fact(5)
120
>>> fact(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000L
如果我們計(jì)算fact(5)
,可以根據(jù)函數(shù)定義看到計(jì)算過程如下:
===> fact(5)
===> 5 * fact(4)
===> 5 * (4 * fact(3))
===> 5 * (4 * (3 * fact(2)))
===> 5 * (4 * (3 * (2 * fact(1))))
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3 * 2))
===> 5 * (4 * 6)
===> 5 * 24
===> 120
遞歸函數(shù)的優(yōu)點(diǎn)是定義簡(jiǎn)單瘦棋,邏輯清晰稀火。理論上,所有的遞歸函數(shù)都可以寫成循環(huán)的方式赌朋,但循環(huán)的邏輯不如遞歸清晰凰狞。
使用遞歸函數(shù)需要注意防止棧溢出。在計(jì)算機(jī)中沛慢,函數(shù)調(diào)用是通過棧(stack)這種數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)的赡若,每當(dāng)進(jìn)入一個(gè)函數(shù)調(diào)用,棧就會(huì)加一層棧幀团甲,每當(dāng)函數(shù)返回逾冬,棧就會(huì)減一層棧幀。由于棧的大小不是無限的,所以身腻,遞歸調(diào)用的次數(shù)過多产还,會(huì)導(dǎo)致棧溢出∴痔耍可以試試fact(1000)
:
>>> fact(1000)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in fact
...
File "<stdin>", line 4, in fact
RuntimeError: maximum recursion depth exceeded
解決遞歸調(diào)用棧溢出的方法是通過尾遞歸優(yōu)化脐区,事實(shí)上尾遞歸和循環(huán)的效果是一樣的,所以去件,把循環(huán)看成是一種特殊的尾遞歸函數(shù)也是可以的坡椒。
尾遞歸是指,在函數(shù)返回的時(shí)候尤溜,調(diào)用自身本身倔叼,并且宫莱,return語句不能包含表達(dá)式丈攒。這樣巡验,編譯器或者解釋器就可以把尾遞歸做優(yōu)化,使遞歸本身無論調(diào)用多少次碘耳,都只占用一個(gè)棧幀显设,不會(huì)出現(xiàn)棧溢出的情況辛辨。
上面的fact(n)
函數(shù)由于return n * fact(n - 1)
引入了乘法表達(dá)式,所以就不是尾遞歸了斗搞。要改成尾遞歸方式指攒,需要多一點(diǎn)代碼,主要是要把每一步的乘積傳入到遞歸函數(shù)中:
def fact(n):
return fact_iter(n, 1)
def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)
可以看到僻焚,return fact_iter(num - 1, num * product)
僅返回遞歸函數(shù)本身,num - 1
和num * product
在函數(shù)調(diào)用前就會(huì)被計(jì)算隙弛,不影響函數(shù)調(diào)用咐旧。
fact(5)
對(duì)應(yīng)的fact_iter(5, 1)
的調(diào)用如下:
===> fact_iter(5, 1)
===> fact_iter(4, 5)
===> fact_iter(3, 20)
===> fact_iter(2, 60)
===> fact_iter(1, 120)
===> 120
尾遞歸調(diào)用時(shí),如果做了優(yōu)化室埋,棧不會(huì)增長(zhǎng),因此姚淆,無論多少次調(diào)用也不會(huì)導(dǎo)致棧溢出腌逢。
遺憾的是,大多數(shù)編程語言沒有針對(duì)尾遞歸做優(yōu)化佳鳖,Python解釋器也沒有做優(yōu)化媒惕,所以,即使把上面的fact(n)
函數(shù)改成尾遞歸方式穿挨,也會(huì)導(dǎo)致棧溢出肴盏。
使用遞歸函數(shù)的優(yōu)點(diǎn)是邏輯簡(jiǎn)單清晰,缺點(diǎn)是過深的調(diào)用會(huì)導(dǎo)致棧溢出贞绵。
針對(duì)尾遞歸優(yōu)化的語言可以通過尾遞歸防止棧溢出恍飘。尾遞歸事實(shí)上和循環(huán)是等價(jià)的,沒有循環(huán)語句的編程語言只能通過尾遞歸實(shí)現(xiàn)循環(huán)。
Python標(biāo)準(zhǔn)的解釋器沒有針對(duì)尾遞歸做優(yōu)化胳施,任何遞歸函數(shù)都存在棧溢出的問題肢专。
練習(xí)題
購物車程序
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Created by master on 2018/5/16 14:22.
import os
import pickle
class ShoppingCart(object):
goods_list = [
{"name": "電腦", "price": 1999, "no.": "0"},
{"name": "鼠標(biāo)", "price": 10, "no.": "1"},
{"name": "游艇", "price": 20, "no.": "2"},
{"name": "鍵盤", "price": 90, "no.": "3"},
{"name": "顯示器", "price": 600, "no.": "4"},
{"name": "內(nèi)存條", "price": 500, "no.": "5"},
{"name": "SSD", "price": 300, "no.": "6"},
{"name": "秋褲", "price": 100, "no.": "7"},
]
users = [
{"username": "a", "password": "123", "salary": 0, "bought": []},
{"username": "b", "password": "123", "salary": 0, "bought": []}]
current_user = {}
file_path = "users"
# 開始
def start(self):
self.load_users()
if self.login():
if not self.current_user["salary"] > 0:
self.input_salary()
else:
self.print_goods_list()
self.buy(self.search())
else:
self.start()
# 輸入薪資
def input_salary(self):
salary = int(input("輸入您的工資:"))
self.current_user["salary"] = salary
self.print_goods_list()
goods = self.search()
self.buy(goods)
# 購物
def buy(self, goods):
if goods:
if goods["price"] > self.current_user["salary"]:
print("余額不足博杖!")
else:
self.current_user["salary"] = self.current_user["salary"] - goods["price"]
print("購買成功,剩余余額:%s" % self.high_light(self.current_user["salary"]))
self.current_user["bought"].append(goods)
self.buy(self.search())
self.save_bought_goods()
else:
print("所選商品不存在哩盲,請(qǐng)重新選擇")
self.buy(self.search())
# 保存購買記錄
def save_bought_goods(self):
if os.path.exists(self.file_path):
with open(self.file_path, "rb") as users:
saved_users = pickle.load(users)
for user in saved_users:
if user["username"] == self.current_user["username"]:
user.update(self.current_user)
with open(self.file_path, "wb") as users:
pickle.dump(saved_users, users)
else:
with open(self.file_path, "wb") as users:
pickle.dump(self.users, users)
# 加載用戶列表
def load_users(self):
if os.path.exists(self.file_path):
with open(self.file_path, "rb") as users:
self.users = pickle.load(users)
else:
with open(self.file_path, "wb") as users:
pickle.dump(self.users, users)
# 查詢商品
def search(self):
no = input("輸入商品編號(hào)(no.):廉油,按e退出。")
if not no:
print("輸入有誤班巩,請(qǐng)重新輸入!")
return
if no == "E" or no == "e":
self.save_bought_goods()
exit("退出購物")
else:
for goods in self.goods_list:
if goods["no."] == no:
return goods
# 輸出商品列表
def print_goods_list(self):
for goods in self.goods_list:
print(goods)
# 用戶登陸
def login(self):
username = input("請(qǐng)輸入用戶名:")
password = input("請(qǐng)輸入密碼:")
login_user = None
if not username or not password:
print("用戶名或密碼不能為空")
return
for user in self.users:
if user["username"] == username and user["password"] == password:
print("登錄成功抱慌!%s" % username)
self.current_user = user
self.see_log()
login_user = user
if not login_user:
print("用戶不存在或密碼錯(cuò)誤")
return login_user
# 查看購買記錄
def see_log(self):
print(self.high_light("------------------"))
print(self.high_light("當(dāng)前用戶:%s" % self.current_user["username"]))
print(self.high_light("已購買:%s" % self.current_user["bought"]))
print(self.high_light("余額:%s%s" % (self.high_light(self.current_user["salary"]), "元")))
print(self.high_light("------------------"))
# 高亮顯示
@staticmethod
def high_light(content):
return '\033[0;31m' + str(content) + '\033[0m'
if __name__ == '__main__':
cart = ShoppingCart()
cart.start()