一、字符編碼
1、字符編碼發展史
-
階段一:現代計算機起源于美國,最早誕生的也是基于英文考慮的ASCII碼;
ASCII:一個bytes代表一個字符(英文字符/鍵盤上的所有其他字符),1bytes = 8bit,8個bit位可以產生2**8 = 256種變化,即可以表示256個字符。
-
階段二:為了滿足中文,中國人定制了GBK編碼
GBK:2bytes表示一個字符;
其他國家也紛紛定制自己的編碼,如日本把日文編到shift_JIS里,韓國把韓文編到Euc-kr里 -
階段三:各國都有各自的編碼標準,就會不可避免地出現沖突,結果就是在多語言混合的文本中,顯示出來就會亂碼。
- 于是產生了unicode,統一用2bytes代表一個字符,2bytes = 16bit ,16個bit位可以產生2**16 = 65536種變化,可以表示65536個字符,然后將全世界各國的文字符號全都包含在內,因而兼容萬國文字語言。
- 但對于英文文本來說,這種編碼方式就會多出一倍的存儲空間,為了解決這個問題,于是又產生了UTF-8,它是對unicode的壓縮優化,英文字符只占用1bytes,中文字符占用3bytes。
注意:需要強調一點;
- unicode:簡單粗暴,所有字符都占2bytes,優點是字符 ---> 二進制表示 轉換速度快,缺點是占用空間大
- utf-8:精準,對不同的字符用不同的長度表示,優點是節省空間,缺點是:字符 ---> 二進制表示 轉換速度慢,因為每次都需要計算出字符需要多長的bytes才能夠準確表示
- 內存中使用的編碼是unicode,用空間換時間(程序都需要加載到內存中才能運行,因為內存應該是盡可能保證快)
- 硬盤中或者網絡傳輸用utf-8,網絡 I/O 或磁盤 I/O 遠大于utf-8的轉換延遲,而且 I/O 應該是飛可能地節省帶寬,保證數據傳輸的穩定性。
注:所有程序,最終都要加載到內存,程序保存到硬盤,不同的國家用不同的編碼格式,但是到內存中我們為了兼容萬國(計算機可以運行任何國家的程序原因在于此),統一且固定使用unicode,這就是為何內存固定用unicode的原因,你可能會說兼容萬國我可以用utf-8啊,可以,完全可以正常工作,之所以不用肯定是unicode比utf-8更高效啊(uicode固定用2個字節編碼,utf-8則需要計算),但是unicode更浪費空間,沒錯,這就是用空間換時間的一種做法,而存放到硬盤,或者網絡傳輸,都需要把unicode轉成utf-8,因為數據的傳輸,追求的是穩定,高效,數據量越小數據傳輸就越靠譜,于是都轉成utf-8格式的,而不是unicode。
2、字符編碼的使用
無論是何種編輯器,要防止文件出現亂碼(請一定注意,存放一段代碼的文件也僅僅只是一個普通文件而已,此處指的是文件沒有執行前,我們打開文件時出現的亂碼)
核心法則就是,文件以什么編碼保存的,就以什么編碼方式打開
2.1 程序的執行
python test.py
(執行程序,一定是先將文件內容加載到內存中)
步驟一:啟動python解釋器
步驟二:python解釋器此時就是一個文本編輯器,負責打開文件test.py,即從硬盤中讀取test.py的內容到內存中。
此時,python解釋器會讀取test.py的第一行內容,
# -*-coding:utf-8 -*-
,來決定以什么編碼格式來讀入內存,這一行就是來設定python解釋器這個軟件的編碼使用的編碼格式,這個編碼可以用sys.getdefaultencoding()
查看,如果不在python文件指定頭信息#-*-coding:utf-8-*-
,那就使用默認的python2中默認使用ascii,python3中默認使用utf-8。
錯誤:
正確:
步驟三:讀取已經加載到內存的代碼(unicode編碼的二進制),然后執行,執行過程中可能會開辟新的內存空間,比如 name = Alex
- 內存的編碼使用unicode,不代表內存中全都是unicode編碼的二進制;
- 在程序執行之前,內存中確實都是unicode編碼的二進制,比如從文件中讀取了一行
name = Alex
,其中“name”、“=”、引號,地位都一樣,都是普通字符而已,都是 unicode編碼的二進制形式存放在內存中的;- 但是程序在執行過程中,會申請內存(與程序代碼所存在的內存是兩個空間),可以存放任意編碼格式的數據,比如
name = Alex
會被python解釋器識別為字符串,會申請內存空間來存放“Hello”,然后讓“name”指向該內存地址,此時新申請的該內存地址保存的也是unicode編碼的“Alex”,如果代碼換成name = "Alex".encode('utf-8')
,那么新申請的內存空間里存放的就是utf-8編碼的字符串“Alex”了。
針對python3 如下圖:
瀏覽網頁的時候,服務器會把動態生成的unicode內容轉換為utf-8再傳輸到瀏覽器:
如果服務器端encode的編碼格式是utf-8,客戶端內存中收到的也是utf-8編碼的二進制。
2.2、Python2與Python3的區別
在Python2中有兩種字符串類型:str 和 unicode
str類型:當Python解釋器執行到產生字符串的代碼時(例如:s = "林"
),會申請新的內存地址,然后將“林” encode成文件開頭指定的編碼格式,這已經是encode之后的結果,所以 s 只能decode,所以很重要的一點是:在Python2中,str 就是編碼后的結果bytes,str = bytes,所以在Python2中,unicode字符編碼結果就 str/bytes 。
#coding:utf-8
s='林' #在執行時,'林'會被以conding:utf-8的形式保存到新的內存空間中
print repr(s) #'\xe6\x9e\x97' 三個Bytes,證明確實是utf-8
print type(s) #<type 'str'>
s.decode('utf-8')
# s.encode('utf-8') #報錯,s為編碼后的結果bytes,所以只能decode
unicode類型:當Python解釋器執行到產生字符串的代碼時(例如:s = u"林"
),會申請新的內存地址,然后將”林“以unicode的格式存放到新的內存空間,所以 s 只能encode,不能decode 。
s=u'林'
print repr(s) #u'\u6797'
print type(s) #<type 'unicode'>
# s.decode('utf-8') #報錯,s為unicode,所以只能encode
s.encode('utf-8')
打印到終端:對于print
需要特別說明的是:當程序執行時,比如name = '蔡'
,print(name)
#這一步是將name
指向的那塊新的內存空間(非代碼所在的內存空間)中的內容,打印到終端,而終端仍然是運行于內存中的,所以這打印可以理解為從一塊內存打印到另一塊內存,unicode --> unicode 。
對于unicode格式的數據來說,無論怎么打印,都不會亂碼。
Python3中的字符串與Python2中的 u"字符串” 一樣,都是unicode,所以無論如何打印都不會亂碼。
在pycharm中,
在windows終端
但是在python2中存在另外一種非unicode的字符串,此時,print x,會按照終端的編碼執行x.decode('終端編碼'),變成unicode后,再打印,此時終端編碼若與文件開頭指定的編碼不一致,亂碼就產生了
在pycharm中(終端編碼為utf-8,文件編碼為utf-8,不會亂碼)
在windows終端(終端編碼為gbk,文件編碼為utf-8,亂碼產生)
驗證:
分別驗證在pycharm(默認是utf-8)中和cmd(默認是gbk)下述的打印結果:
#coding:utf-8
s=u'林' #當程序執行時,'林'會被以unicode形式保存新的內存空間中
#s指向的是unicode,因而可以編碼成任意格式,都不會報encode錯誤
s1=s.encode('utf-8')
s2=s.encode('gbk')
print s1 #打印正常否?#在pycharm打印正,cmd中打印會亂碼
print s2 #打印正常否,正好與上面相反
print repr(s) #u'\u6797'
print repr(s1) #'\xe6\x9e\x97' 編碼一個漢字utf-8用3Bytes
print repr(s2) #'\xc1\xd6' 編碼一個漢字gbk用2Bytes
print type(s) #<type 'unicode'>
print type(s1) #<type 'str'>
print type(s2) #<type 'str'>
2.3、在Python3中也有兩種字符串類型str 和 bytes
str 是 unicode
#coding:utf-8
s='林' #當程序執行時,無需加u,'林'也會被以unicode形式保存新的內存空間中,
#s可以直接encode成任意編碼格式
s1 = s.encode('utf-8')
s2 = s.encode('gbk')
print(s1) #b'\xe6\x9e\x97'(utf-8三個字節)
print(s2) #b'\xc1\xd6' (gbk兩個字節)
print(type(s)) #<class 'str'>
bytes 就是 bytes
#coding:utf-8
s='林' #當程序執行時,無需加u,'林'也會被以unicode形式保存新的內存空間中,
#s可以直接encode成任意編碼格式
s1=s.encode('utf-8')
s2=s.encode('gbk')
print(s) #林
print(s1) #b'\xe6\x9e\x97' 在python3中,是什么就打印什么
print(s2) #b'\xc1\xd6' 同上
print(type(s)) #<class 'str'>
print(type(s1)) #<class 'bytes'>
print(type(s2)) #<class 'bytes'>
二、文件操作
1、文件處理流程
- 1.打開文件,得到文件句柄并賦值給一個變量
- 2.通過文件句柄對文件進行操作
- 3.關閉文件
2、基本操作
2.1 打開文件并讀取
打開文件時,需要指定文件路徑和以何等方式打開文件,打開后,即可獲取該文件句柄,日后通過此文件句柄對該文件操作。
f = open('data','r',encoding='utf-8') #這里的f指向的是一個內存空間地址
file = f.read() #通過read方法將內存空間中的內容讀取出來并賦值給file
print(f) #輸出一個內存空間地址,及文件名、打開的模式、編碼格式。
f.close() #關閉文件
print(file) #輸出文件內容
輸出:
<_io.TextIOWrapper name='data' mode='r' encoding='utf-8'>
打開文件的模式有:
- r ,只讀模式【默認】
- w,只寫模式【不可讀;不存在則創建;存在則清空內容;】
- x, 只寫模式【不可讀;不存在則創建,存在則報錯】
- a, 追加模式【可讀; 不存在則創建;存在則只追加內容;】
"+" 表示可以同時讀寫某個文件:
- r+, 讀寫【可讀,可寫】
- w+,寫讀【可讀,可寫】
- x+ ,寫讀【可讀,可寫】
- a+, 寫讀【可讀,可寫】
"b"表示以二進制的方式操作
- rb 或 r+b
- wb 或 w+b
- xb 或 w+b
- ab 或 a+b
以二進制的方式讀寫一個圖片:
with open('sb.jpg','rb') as f,\
open('sb_alex.jpg','wb') as f1:
data=f.read()
f1.write(data)
注:以 b 方式打開時,讀取到的內容是bytes類型,寫入時也需要提供bytes類型。
2.2 with語句
為了避免打開文件后忘記關閉,可以通過管理上下文,即:
with open('log','r') as f:
print(f.read())
- 以此方式打開,當with代碼塊執行完畢時,內部會自動關閉并釋放文件資源。
- 在Python2.7后,with又支持同時對多個文件的上下文進行管理,即:
with open('log1') as obj1, open('log2') as obj2:
pass
2.3 常用操作
read
讀取文件
with open('a.txt','r',encoding='utf-8') as f:
print(f.read(4)) #數字指的是讀的是字符個數
write
往文件中寫入
with open('access.log','a',encoding='utf-8') as f:
f.write('\n我是一個好人')
seek/tell
seek是將光標跳轉到指定位置
tell是顯示當前光標所在位置
with open('a.txt','r',encoding='utf-8') as f:
f.seek(3) #seek內指定的數字代表字節,默認情況,是以文件起始位置作為開始,往后移動3個bytes
print(f.tell()) # 打印當前光標所在的位置
以當前光標所在的位置為開始,移動光標
with open('b.txt','rb') as f:
f.seek(2,1) # 1 代表以當前光標所在的位置為開始,往后移動2個 bytes
從文件末尾開始,移動光標
with open('b.txt','rb') as f:
f.seek(-3,2) # 2表示從文件末尾為開始,往前移動3個bytes
truncate
截斷,從指定位置截斷,truncate中的數字是字節數
with open('a.txt','r+',encoding='utf-8') as f:
f.truncate(3) #truncate中的數字是字節數,3個字節可以保留一個漢字,如果只截斷1個字節,文件將出現亂碼
模擬Linux命令tail -f access.log
打開文件,并將光標移動到文件末尾,循環讀取文件,這樣每當文件中新增一條日志并保存后,就會被程序讀取到并打印出來。
# tail -f access.log
import time
with open('access.log','r',encoding='utf-8') as f:
f.seek(0,2) # 將光標移動末尾第一位
while True:
line=f.readline().strip()
if line:
print('新增一行日志',line)
time.sleep(0.5)
close
關閉文件
f = open('test','w')
f.write('11111\n22222\n')
f.close()
closed
判斷文件是否關閉,關閉返回
f = open('test','w')
f.write('11111\n22222\n')
f.close()
print(f.closed)
readline
一行一行的讀取文件
with open('access.log','r') as f:
print(f.readline())
print(f.readline())
print(f.readline())
輸出:
11111
22222
33333
# 有空行是因print默認會打換行符,在print中加上end=''即可取消空行
readlines
一次讀取全部文件
with open('access.log','r') as f:
print(f.readlines())
輸出:
['11111\n', '22222\n', '33333\n', '44444\n', '55555\n', '66666\n', '77777\n', '88888']
# 可以看出readlines將文件內容變成一個列表了
with open('access.log','r') as f:
for i in f.readlines():
print(i,end='')
輸出:
11111
22222
33333
44444
55555
66666
77777
88888
注:以上兩種方法都會將文件一次加載到內存中,如果文件特別大,比如打開一個20G的文件,而機器內存只有16G,通過這兩種方法無疑會將機器內存撐爆。
想要一行一行讀取文件內容,而每次讀取一行內容將覆蓋之前一行,這樣就不會將一個文件中的所有內容全部加載到內存了。想要實現這點,可以采用下面這種方法:
直接循環文件句柄
with open('access.log','r') as f:
for i in f:
print(i,end='')
輸出:
11111
22222
33333
44444
55555
66666
77777
88888
模擬文件改名
# 模擬文件改名
f1 = open('c','r')
f2 = open('.c.swap','w')
for i in f1:
# print(i)
if i.startswith('5'): # 修改以5開頭的行
i = '11111\n'
f2.write(i)
f1.close()
f2.close()
import os
os.remove('c') # 先刪除,再改名
os.rename('.c.swap','c')
readable
判斷文件是否以只讀模式打開
with open('access.log','a',encoding='utf-8') as f:
print(f.readable())
輸出:
False
# 因為是以a模式打開,所以返回False
writeable
是否以可寫的模式打開文件,與readable類似
writelines
將列表寫入文件,不過請注意,列表中的數據類型只能是字符串
with open('access.log','w',encoding='utf-8') as f:
f.writelines(['111\n','222\n','333\n'])
flush
操作系統在將數據寫入硬盤時會等內存中積累到一定量時,才會數據一次性寫入硬盤,這樣可以節省磁盤 I/O ,flush方法是將內存中的數據強制刷到硬盤
with open('access.log','w',encoding='utf-8') as f:
f.write('111\n222')
f.flush()
三、函數
-
Python中函數的定義:函數是邏輯結構化和過程化的一種編程方法。
函數的特性:- 減少重復代碼
- 使程序變的可擴展
- 使程序變得易維護
以我自己的理解,函數(function)--> 即功能,由一段代碼實現的一個特定的功能,然后在程序中可以反復調用此功能,減少代碼冗余。這是我簡單的理解,不詳之處還請大家多多指正。
-
在python中,函數分為兩類:
- 內置函數,點擊查看
- 自定義函數,請繼續向下看
1. 函數的定義
# 語法
# def 函數名(參數1,參數2,...):
# """文檔注釋"""
# 函數體
# return 值
函數的定義主要有如下要點:
- def:表示函數的關鍵字
函數名:函數的名稱,后續代碼可根據函數名調用此函數
函數體:函數中進行一系統的邏輯計算,如:發送郵件、計算出[11,22,38,55,2]中的最大數等。。。
參數:為函數體提供數據
返回值:當函數執行完畢后,可以給調用者返回數據。
1.1 定義函數的二種形式
1.1.1 無參函數
如果函數的功能僅僅只是執行一些操作而已,就定義成無參函數,無參函數通常都沒有返回值
def print_star():
print('#'*6)
print_star() # 調用函數
1.1.2 有參函數
函數的功能的執行依賴于外部傳入的參數,有參函數通常都有返回值。
def my_max(x,y):
res=x if x >y else y # 三元運算
print(res)
return res
my_max(5,8) # 5和8是傳給my_max函數的參數
2 函數調用
按照有參和無參,可以將函數調用分為兩種,如下:
def foo():
print('from foo')
def bar(name):
print('bar===>',name)
foo() # 定義時無參,調用時也無需傳入參數
bar('egon') # 定義時有參,調用時也必須傳入參數
按照函數的調用形式和出現的位置,分三種:
-
foo()
# 調用函數的語句形式 - 調用函數的表達式形式
def my_max(x,y):
res=x if x >y else y
return res
res=my_max(1,2)*10000000 #調用函數的表達式形式
print(res)
- 把函數調用當中另外一個函數的參數
def my_max(x,y):
res=x if x >y else y
return res
res=my_max(my_max(10,20),30) #把函數調用當中另外一個函數的參數
print(res)
輸出:
30
3. 函數的返回值
以下三種情況返回值都為None
- 沒有return
- return 什么都不寫
- return None
def foo():
print('from foo')
return None
res=foo()
print(res) # res接收返回值None
多個返回值,將返回成一個元組
def foo():
print('from foo')
return 1,[2,3],(4,5),{}
res=foo()
print(res) #打印結果:(1,[2,3],(4,5),{})
a,b,c,d=foo() # 可以直接用函數的返回值賦值給變量
print(d)
輸出結果:
from foo
(1, [2, 3], (4, 5), {})
from foo
{}
4. 函數的參數
4.1 從大的角度去看,函數的參數分兩種:
- 形參(可以理解為變量名)
- 實參(賦予變量的值)
#定義階段
def foo(x,y): #x=1,y=2
print(x)
print(y)
#調用階段
foo(1,2)
輸出:
1
2
4.2 詳細區分函數的參數
詳細的區分函數的參數分為五種:
- 位置參數
- 關鍵字參數
- 默認參數
- 可變長參數( *args,**kwargs)
- 命名關鍵字參數
4.2.1 位置參數
實參必須與形參一一對應,實參的個數不能多也不能少。
def foo(x,y,z):#位置形參:必須被傳值的參數
print(x,y,z)
foo(1,2,3) #位置實參數:與形參一一對應
輸出:
1 2 3
4.2.2 關鍵字參數
Key = value
def foo(x,y,z):
print(x,y,z)
foo(z=3,x=1,y=2)
輸出:
1 2 3
注:位置參數與關鍵字參數可以一起使用,但是需要注意以下問題:
1.關鍵字實參必須在位置實參后面
2.不能重復對一個形參傳值
# foo(1,z=3,y=2) #正確
# foo(x=1,2,z=3) #錯誤
# foo(1,x=1,y=2,z=3) #錯誤
4.2.3 默認參數
在定義函數時,如果使用了默認參數;那在調用此函數時,如果對默認參數指定了新值,則取新值;如果沒有對默認參數指定值,則使用默認參數定義的值。
def register(name,age,sex='male'): #形參:默認參數sex='male'
print(name,age,sex)
register('alex',age=40) # 調用時沒有給sex賦值
輸出結果:
alex 40 male
給默認參數賦新值:
def register(name,age,sex='male'): #形參:默認參數
print(name,age,sex)
register('鋼蛋',20,'female')
register('鐵蛋',sex='female',age=19)
# 輸出:
鋼蛋 20 female
鐵蛋 19 female
默認參數需要注意的問題:
- 默認參數必須跟在非默認參數后面
```
# def register(sex='male',name,age): #在定義階段就會報錯
# print(name,age,sex)
```
- 默認參數在定義階段就已經賦值了,而且只在定義階段賦值一次
```
a=100000000
def foo(x,y=a):
print(x,y)
a=0
foo(1)
# 輸出結果:
1 100000000
```
注:由結果可以看出,后來給a賦值0后并沒有生效
- 默認參數的值通常定義成不可變類型的數據
4.2.4 可變長參數
第一種:args*
*會把溢出的按位置定義的實參都接收,以元組的形式賦值給args
def foo(x,y,*args): #*會把溢出的按位置定義的實參都接收,以元組的形式賦值給args
print(x,y)
print(args)
foo(1,2,3,4,5)
輸出:
1 2
(3, 4, 5)
利用可變長參數計算任意長度的和
def add(*args):
res=0
for i in args:
res+=i
return res
print(add(1,2,3,4))
print(add(1,2))
輸出:
10
3
*第二種:*kwargs **
**會把溢出的按關鍵字定義的實參都接收,以字典的形式賦值給kwargs
def foo(x, y, **kwargs): # **會把溢出的按關鍵字定義的實參都接收,以字典的形式賦值給kwargs
print(x, y)
print(kwargs)
foo(1,2,a=1,name='egon',age=18)
輸出:
1 2
{'age': 18, 'a': 1, 'name': 'egon'}
4.2.5 命名關鍵字參數(了解)
def foo(name,age,*,sex='male',height):
print(name,age)
print(sex)
print(height)
foo('egon',17,height='185')
輸出:
egon 17
male
185
*后定義的參數為命名關鍵字參數,這類參數,必須被傳值,而且必須以關鍵字實參的形式去傳值
4.2.6 各種形式的參數混合使用
def foo(name,age=10,*args,sex='male',height,**kwargs):
print(name)
print(age)
print(args)
print(sex)
print(height)
print(kwargs)
foo('alex',1,2,3,4,5,sex='female',height='150',a=1,b=2,c=3)
輸出結果:
alex
1 # 雖然age是默認參數,但是這里以位置參數的方式,將1傳給age為值
(2, 3, 4, 5)
female
150
{'b': 2, 'c': 3, 'a': 1}
4.2.7 其他姿勢
可變長參數的其他玩法:
*args :
# def foo(*args):
# print(args)
# foo(1,2,3,4) # 1,2,3,4 <=====>*(1,2,3,4)
#*['A','B','C','D'], 等同于'A','B','C','D'
# foo(*['A','B','C','D']) ===> foo('A','B','C','D')
# foo(['A','B','C','D']) #
# def foo(x,y,z):
# print(x,y,z)
#
# # foo(*[1,2,3]) #相當于foo(1,2,3)
# foo(*[1,2]) #相當于foo(1,2)
**kwargs :
def foo(**kwargs):
print(kwargs)
foo(**{'y': 2, 'x': 1,'a':1}) #相當于foo(a=1,y=2,x=1)
輸出:
{'y': 2, 'x': 1, 'a': 1}
*args 與 *kwargs*混合使用:
def wrapper(*args,**kwargs):
print(args)
print(kwargs)
wrapper(1,2,3,a=1,b=2)
輸出:
(1, 2, 3)
{'b': 2, 'a': 1}
嵌套:
def foo(x,y,z):
print('from foo',x,y,z)
def wrapper(*args,**kwargs):
print(args) #args=(1,2,3)
print(kwargs) #kwargs={'a':1,'b':2}
foo(*args,**kwargs) #foo(*(1,2,3),**{'a':1,'b':2}) #foo(1,2,3,b=2,a=1)
# wrapper(1,2,3,a=1,b=2)
wrapper(1,z=2,y=3)
# 輸出:
(1,)
{'y': 3, 'z': 2}
from foo 1 3 2
注:函數定義階段到底干了什么事情:只檢測函數體的語法,并不會執行