Python入門

大發平臺註冊最高邀請碼是多少99993329發表於2021-02-02

最近心血來潮想了解下Python,反正是興趣使然的學習,對它的好感比Node.js還高一些。這裡記錄一些流水賬,算是個學習筆記。

學習資料:廖雪峰Python教程

再此由衷感謝各位大大的分享。

關於Python

Python是一種想當高階的語言,這意味著他已經自帶了許多功能,編寫一個任務所需要的程式碼量大大減少,相應的,執行速度相比C和java就要慢一些。但其實對使用者來說沒什麼區別。
另外,Python程式碼無法加密,如果釋出了你的Python程式,實際上就是釋出原始碼,相當於被動開源。但是其實沒人會看你的辣雞程式碼的。

安裝Python

Windows上下載安裝Python時記得勾選  Add Python 3.5 to PATH即可。
Python有許多不同的直譯器,我們這裡安裝好之後會自帶官方的直譯器CPython。

Hello Python

在Windows命令列下輸入python進入`Python`互動模式。互動模式下可以直接執行Python語句。
在Windows命令列下輸入python + 檔名.py 可以直接執行python檔案。

也可以使用gitbush來執行,開啟gitbush,輸入`winpty py`進入python互動模式即可執行python語句。
在gitbush中,輸入python + 檔名.py  也可以直接執行python檔案。
也可以在Sublime Text中編輯字尾為 .py 的Python檔案。然後在Windows中,切換到該檔案所在目錄,輸入 `python 檔名.py` 來執行Python檔案。 
實際編寫Python程式碼的時候可以嘗試一邊開始編輯器寫,一邊在命令列的Python互動模式中驗證你的語句。
輸出:`print('my name is', name)`
輸入:`input('input your name')`
python宣告變數直接寫變數名:`name = input('input your name')`
**注意input()返回的資料型別為str,如果需要數字,要呼叫改變資料型別,如int()**

Python基礎

Python採用縮排的語法,一眼看上去有點蛋疼,感覺還是js的分號和大括號穩妥一些。這裡應該始終使用**4個空格**的縮排,在Sublime Text中,右下角可以很方便的調整一個Tab Size,設定為4個縮排就可以直接用Tab了。縮排還有個壞處就是‘複製-貼上’的時候要格外注意。
另外,Python使用#開頭作為註釋,Sublime Text快捷鍵就行了。
**Python和javaScript一樣是大小寫敏感的**

資料型別和變數

Python中常用的整數,浮點數資料型別跟js基本一致。字串型別中,也可以使用`\`來進行轉義,`\n`表示換行,`\t`表示製表符`\`也可以轉義`\`字元本身。
若一個字串中有多個字元都要轉義,可以簡化為`r''`表示,`''`中的字串預設不轉義。
若一個字串內部有很多換行,用`\n`寫在一行不好閱讀,可以用`'''...'''`表示多行內容。這種方式也可以前邊加上`r`
Python中的布林值,為`True`和`False`,並且首字母必須大寫否則報錯。布林值可以用`and`,`or`,`not`來進行與或非運算。
Python中還有空值`None`。
此外Python還有列表,字典等多種資料型別,還允許建立自定義資料型別,後邊會提到。
Python跟js一樣,是動態語言,變數沒有固定的資料型別。Python中宣告變數的時候直接寫變數名即可。使用`=`來為變數賦值。
a = 'abc'  #此時a指向abc
b = a  #此時a,b都指向abc
a = 'xyz'  #此時a指向xyz,b還是指向abc
print(b)   #輸出的是abc
Python一般用全部大寫表示常量。
Python中有兩種除法,一種是`/`,這裡 10/3 = 3.33333333333; `/`除法的計算結果是浮點數,即使能夠整除,結果也是浮點數,例如   9/3 = 3.0;
還有一種除法稱為地板除 `//`,地板除的結果永遠是整數,**取結果的整數部分(去尾法)。10//3 = 3**。
Python的整數和浮點數都沒有大小限制。超過一定範圍就直接表示為`inf`,即無限大。

字串和編碼

Python3版本中字串以Unicode編碼,支援多語言。
對於單個字元的編碼,Python使用`ord()`函式獲取字元的整數表示,`chr()`函式把編碼轉換為對應的字元。
一般我們會在`.py`檔案開頭加上下邊兩行:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
第一行告訴Lunux/OS X系統,這是一個Python可執行程式,Windows會忽略這個註釋;
第二行告訴Python直譯器,按照UTF-8編碼讀取原始碼,否則可能會有亂碼。
申明瞭UTF-8編碼並不意味著你的`.py`檔案就是UTF-8編碼的,必須要確保文字編輯器正在使用UTF-8編碼。

格式化,Python中的格式化跟C語言一直,採用`%`實現。在字串內部,常見的佔位符有:
     - %d    整數
     - %f     浮點數
     - %s     字串
     - %x     十六進位制整數

        當真的需要使用百分號的時候,使用`%%` 來表示。

使用list和tuple

Python內建的一種資料型別是列表,應該就是js裡邊的陣列。
len(list)函式獲得list長度。
list[0]用索引訪問list內的元素。當索引超出範圍時,Python會報錯。list最後一個元素可以用list[len(list)-1] 或者 list[-1]
append()插入末尾;pop()末尾刪除;pop(i)刪除索引為i的元素;

Python還有另一種有序列表叫元組:tuple。tuple和list非常相似,但是tuple一旦初始化就不能修改。tuple用圓括號`a = ('a','b')` ;現在a這個tuple就不能改變了。他也沒有append()等修改的方法。但是能夠正常讀取元素和使用`len()`方法。
不可變的tuple的意義在於讓程式碼更安全,能用tuple的就儘量用tuple。
**注意:因為無法修改,所以tuple定義時就必須寫入所有元素,當tuple只有一個元素的時候,要多寫一個逗號,不然直譯器會把圓括號當做運算子。例如:`t = (1,)`是tuple,而`t = (1)`是數字1。Python在顯示只有一個元素的tuple時,也會多一個逗號,以免你誤解。**
**tuple元素不能變,指的也是元素的指向。而在tuple中的list裡邊的元素,是可以改動的。因為tuple指向的list還是那個list,只是list指向的元素變了。這跟tuple中的元素不變這句話,並不衝突。如下例:**
>>> t = ('a', 'b', ['A', 'B'])  #t 是tuple,t不能改變
>>> t[2][0] = 'X'     #但是t[2]是個list,list裡邊的元素是可以改變的。
>>> t[2][1] = 'Y'
>>> t
('a', 'b', ['X', 'Y'])

條件判斷

Python中條件判斷使用`if a = 0:`,`elif a = 1:`,`else:`。注意這裡的'elif'就是else if的縮寫,這裡注意後邊有個冒號。判斷條件後邊的語句使用縮排。

迴圈

1.`for item in items:`
使用函式`range(n)`可以生成一個從0到n-1的整數序列。再使用`list()`函式還可以轉化為list。
2.while 

break        結束迴圈
continue     跳過當前迴圈

使用dict和set

dict全稱dictionary,即字典。看起來相當於js的物件Obj。
d = {'name':'creabine','age':18}  #dict
d[name] = Creabine  #可以通過key來指定value
'sex' in d   #可以通過  in  來判斷key是否存在,這裡不存在所以返回False
d.get('sex') #通過get()函式取key,key不存在時返回 None。此時Python互動式命令列不顯示結果
d.get('sex',-1) #通過get()函式取key,也可以指定key不存在時要返回的值,這裡指定了-1
可以使用pop(key)方法來刪除一個dict中的key和對應的value。
dict相比list,查詢和插入的速度更快,但是佔記憶體更多。dict的key不能改變。


set和dict類似,也是一組key的集合,但不儲存value。
建立一個set需要提供一個list作為輸入集合:`s = set([1,2,3])`,這裡傳入的引數是一個list,列印s將顯示`{1,2,3}`表示內部有這三個元素,順序並不是有序的。
另外,list中重複的元素在set中將自動被過濾。
通過`add(key)`可以將元素新增到set中。重複新增無效
通過`remove(key)`可以刪除元素
**set可以看做數學意義上的無序和無重複元素的集合。因此兩個set可以做數學意義上的交集(使用`&`),並集(使用`|`)等操作。**

**set和dist的唯一區別僅在於沒有儲存對應的value,但是其原理是一致的。**

**之前講過str是不變物件,list是可變物件。對於不變物件來說,呼叫物件自身的任意方法,也不會改變該物件自身的內容。相反,這些方法會建立新的物件並返回,這樣就保證了不可變物件本身永遠是不可變的。如下例:**
>>> a = 'abc'
>>> b = a.replace('a', 'A')  #這裡實際上是建立了一個新的‘Abc’物件給了b。a本身沒變。
>>> b
'Abc'
>>> a
'abc'

函式

Python有許多內建函式。呼叫函式的時候,如果傳入的引數數量或者型別有誤,則會報錯。
abs()求絕對值。max()求一組數中的最大值。

定義函式

Python中使用`def`語句來定義函式,用`return`來返回值。例如:
# test.py檔案
def my_abs(x)
    if x >= 0:
        return x
    else:
        return -x
該函式可以直接在互動模式下定義,也可以寫在檔案中。然後在當前目錄下啟動python直譯器,使用`from test import my_abs`來匯入`my_abs()`函式,注意這裡的test是檔名,不帶`.py`副檔名。

如果想定義一個什麼也不做的空函式,可以使用`pass`語句:
def donothing():
    pass
既然`pass`語句什麼都不做,那他有什麼用呢?他的作用是佔位,比如還沒想好怎麼寫的函式,使用pass,讓程式碼能執行起來。缺少了pass,程式碼執行就會報錯。
呼叫函式時,引數個數不對的時候,python直譯器會自動報錯,丟擲`TypeError`。但是引數型別不對的時候,需要自己在函式中寫上型別檢查。例如剛才的my_abs()函式。
deg my_abs(x):
    if not isinstance(x,(int,float)):
        raise TypeError('bad operand type')
    if x >= 0:
        return x
    else:
        return -x
這樣新增了引數檢查之後,若傳入錯誤的引數型別,函式就可以丟擲一個錯誤。

函式還可以返回多個值。此時,返回值實際上是一個tuple。在語法上,返回一個tuple可以省略括號,而多個變數可以同時接收一個tuple,按位置賦值給對應的變數。
x,y = return2val()
print(x,y)

函式的引數

Python的函式定義非常簡單,但靈活度非常大,除了正常定義的必選引數外,還可以使用預設引數,可變引數和關鍵字引數。使函式定義出來的介面不但能處理複雜的引數,還可以簡化呼叫者的程式碼。例如:
#有一個求x的n次方的函式:
def power(x,n):
    s = 1
    while n>o:
        n = n - 1
        s = s * n
    return s
若對於該函式,我們常用它求2次方,則可以給他一個預設引數:
def power(x,n=2):
    s = 1
    while n>o:
        n = n - 1
        s = s * n
    return s
# 有預設引數x=2,此時呼叫 power(5)則相當於power(5,2)。對於n不為2的其他情況,就必須明確的傳入引數,如power(5,4)
預設引數可以簡化函式的呼叫。這裡要注意幾點:**1.設定預設引數時,必選引數在前,預設引數在後。否則直譯器難以判斷會報錯。2.當函式有多個引數時,把變化大的引數放前面,變化小的引數放後邊。變化小的引數就可以作為預設引數。**
這裡有個坑:Python函式在定義的時候,預設引數的值就已經被計算出來了。如果在函式執行的過程中,改變了預設引數的值,就會出現問題,例如:
def add_end(L=[]):
    L.append('END')
    return L
# 多次呼叫改變了預設引數的值:
>>> add_end()
['END']
>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']
也就是說,預設引數必須指定為不可變物件。所以當遇到上邊這種情況時,可以使用none來實現:
def add_end(L=None):
    if L is None:
        L = []
    L.append('END')
    return L
# 這樣不論呼叫多少次,都沒問題了。
**可變引數**
在引數前加一個`*`號,可以把引數的數量定義為可變的。例如:`def a(*n)`。
另外,如果傳入的引數是一個list或者tuple,Python允許你在list或者tuple前加一個`*`號,把list或tuple的元素變成可變引數填進去。
**可變引數由於數量未知,所以函式中通常會使用`for a in n`這樣的語句來遍歷傳入的引數。**

**關鍵字引數**
可變引數允許傳入0或任意個引數,在函式呼叫時這些引數自動組裝為一個tuple。而關鍵字引數允許傳入0或任意個引數,這些引數在函式中自動組裝為一個dict。關鍵字引數前邊加上`**`來表示。例如:
def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)
# 可以傳入任意多個關鍵字引數:
>>> person('Michael', 30)   # 0個關鍵字引數
name: Michael age: 30 other: {}
>>> person('Bob', 35, city='Beijing')  # 1個關鍵字引數
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender='M', job='Engineer')  # 2個關鍵字引數
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}
關鍵字引數能擴充套件函式的功能。關鍵字引數也可以把預先組裝的dict作為關鍵字傳進去。這裡傳進去的引數,是預先組裝的dict的拷貝,改變他,之前的dict不會變。

**命名關鍵字引數**
對於關鍵字引數,若想要限制關鍵字引數的名字,就可以使用命名關鍵字引數。例如:
 # 命名關鍵字引數,用 * 分隔
def person(name,age,*,city,job):
    print(name,age,city,job)
 # 命名關鍵字引數,若已經有一個可變引數,後邊跟著的命名關鍵字引數就不需要 * 了
def person(name,age,*args,city,job):
    print(name,age,city,job)
#  呼叫方式:
>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer
和關鍵字引數`**kw`不同,命名關鍵字引數用特殊的分隔符`*`,`*`後邊的引數被視為命名關鍵字引數。命名關鍵字引數必須傳入引數名。命名關鍵字引數可以有預設值,此時可以不傳入有預設值的引數。


**引數組合**
在Python中定義函式,可以用必選引數,預設引數,可變引數,關鍵字引數和命名關鍵字引數,這5中引數都可以組合使用。**但是注意,引數定義的順序必須是:必選引數,預設引數,可變引數,命名關鍵字引數和關鍵字引數。**

遞迴函式

**在函式內部,可以呼叫其他函式,如果在一個函式內部呼叫函式本身,這個函式就是遞迴函式。**
例如要計算階乘:`n! = 1 * 2 * 3 * ... * n`,用函式fact(n)表示,可以看出:`fact(n) = n! = 1 * 2 * 3 * ... * (n-1) * n = (n-1)! *n = fact(n-1) * n`,於是,求階乘的函式可以寫成:
 #求階乘的遞迴函式
def fact(n):
    if n = 1:
        return 1
    return n * fact(n-1)
遞迴函式的有點事定義簡單,邏輯清晰。**理論上,所有遞迴函式都可以寫成迴圈的方式,但迴圈的邏輯不如遞迴清晰。**
使用遞迴函式時要注意防止棧溢位,也就是不能遞迴太多次。解決呼叫棧溢位的方法是通過尾遞迴優化。尾遞迴就是在函式返回的時候,呼叫它本身,並且return語句不能包含表示式。這樣不論遞迴多少次,都只佔用一個棧幀,不會出現棧溢位。
將上邊的遞迴改為尾遞迴:
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)僅返回遞迴函式本身,num - 1和num * product在函式呼叫前就會被計算,不影響函式呼叫。
**遺憾的是,大多數程式語言沒有針對尾遞迴做優化,Python直譯器也沒有做優化,所以,即使把上面的fact(n)函式改成尾遞迴方式,也會導致棧溢位。**

..........所以說了這麼多你在逗我嗎

高階特性

人生苦短,我用Python。
利用Python的高階特性,可以用最少的程式碼來完成開發,提高效率。

切片

去list或tuple中的幾個元素,使用切片(Slice)。
L = list(range(100))
L = [0,1,2,3,...,99]
# 切片示例:
L[0:3] == L[:3] == [0,1,2]  #`L[0:3]`表示從索引0開始取,取到索引3位置,但不包括索引3。並且如果第一個索引是0,還可以省略,即`L[:3]`。
L[-3:0] == L[-3:] == [97,98,99]  #同時,索引支援負數取倒數的元素,倒數第一個元素的索引是 -1 。若要取最後3個元素,則`L[-3:]`
L[10:20] == [10.11.12.13.14.15.16.17.18.19]
L[:10:2] == [0,2,4,6,8]  #前10個數,每兩個取一個
L[::5] == [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]  # 所有數,每5個取一個。
L[:] == [1,2,3,...,99]  # 什麼都不寫,複製一個list。
tuple也是一種list,唯一的區別是tuple中的元素不可變,因此tuple也可以用切片操作,操作的結果仍是tuple。
字串`'xxx'`也可以看成是一種list,每個元素就是一個字元,因此,字串也可以用切片操作,操作的結果仍是字串。

在很多程式語言中,針對字串提供了各種擷取操作,Python沒有針對字串的擷取,統一使用切片操作。

迭代

若給定一個list或tuple,可以通過for迴圈來遍歷,這種遍歷我們稱為迭代(Iteration)。在Python中通過`for...in`來完成。在其他語言,如c,java,js中則是通過for迴圈下標來實現的。
Python的迭代不僅可以迭代list和tuple。還可以迭代dict。因為dict的儲存不是按照list的方式順序排列,所以,迭代出的結果順序可能不一樣。預設情況下dict迭代的是key。若要迭代value,可以用`for value in d.values()`若要同時迭代key和value,可以用`for k,v in d.items()`。
字串也是可迭代物件。也可以這樣迭代。
**那麼如何判斷一個物件是可迭代物件呢?方法是通過collections模組的Iterable型別判斷:**
from collections import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整數是否可迭代
False
**最後一個問題,若相對list實現類似js那樣的下標迴圈怎麼辦?使用Python的內建函式`enumerate`可以把list變成索引-元素對,這樣就可以再for迴圈中迭代索引和元素本身:**
>>> for i, value in enumerate(['A', 'B', 'C']):
...     print(i, value)
...
0 A
1 B
2 C
上邊的for迴圈裡,同時引用兩個變數,這在Python中是很常見的,如下:
>>> for x, y in [(1, 1), (2, 4), (3, 9)]:
...     print(x, y)
...
1 1
2 4
3 9

列表生成式

列表生成式即List Comprehensions,是Python內建的非常簡單卻強大的可以用來建立list的生成式。例如:
list(range(1,11)) == [1,2,3,...,10]
[x * x for x in range(1,11)] == [1*1,2*2,3*3,...,10*10]
[x * x for x in range(1,11) if x%2 == 0] == [2*2,4*4,...,10*10]
# 兩層迴圈,左邊的for迴圈在外邊,右邊的for迴圈巢狀在裡邊
>>> [m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']
# 同時使用多個變數
>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> for k, v in d.items():
...     print(k, '=', v)
...
y = B
x = A
z = C
# 用兩個變數生成list
>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> [k + '=' + v for k, v in d.items()]
['y=B', 'x=A', 'z=C']
# 把list中所有字串變成小寫
>>> L = ['Hello', 'World', 'IBM', 'Apple']
>>> [s.lower() for s in L]
['hello', 'world', 'ibm', 'apple']

生成器

通過列表生成式建立列表會受到記憶體的限制,容量有限。若要建立含很多元素的列表,不僅要佔用很大的儲存空間,如果我們只訪問前幾個,後邊的很多元素就浪費了。所以,如果列表元素可以按照某種演算法推算出來,我們就不必穿件完整的list,而在迴圈的過程中計算出後續元素。在Python中,這種機制稱為生成器:generator

建立一個generator,有很多方法。第一種方法很簡單,只要把列表生成式的 [] 改成 () ,就建立了一個generator。
>>> L = [x * x for x in range(10)]
>>> L   #建立了一個list
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g   #建立了一個generator
<generator object <genexpr> at 0x1022ef630>
我們可以直接列印出list的每一個元素。當我們想列印generator的元素,如果要一個一個列印出來,可以使用`next(g)`獲得generator的下一個返回值。
剛才說過generator儲存的是演算法,每次呼叫`next(g)`就計算出 g 的下一個元素的值,值到最後一個元素。沒有更多元素的時候,丟擲`StopIteration`的錯誤。
不想多次呼叫next的時候,可以使用for迴圈:
>>> g = (x * x for x in range(10))
>>> for n in g:    #用for迴圈列印g
...     print(n)
... 
0
1
4
...
建立了一個generator之後,基本上永遠都不會用next(),而是通過for來迭代他,並且不需要關心StopIteration的錯誤。
generator非常強大,如果推算的演算法比較複雜,用類似列表生成式的for迴圈無法實現的時候,還可以用函式來實現。比如斐波拉契數列(Fibonacci),除第一個和第二個數之外,任意一個數都由前兩個數相加得到。斐波拉契數列用列表生成式寫不出來,但是用函式把他列印出來卻很容易。
def fib(max):
    n, a, b = 0, 0, 1   #等價於  n=0  a=0  b=1
    while n < max:
        print(b)
        a, b = b, a + b  #等價於 t=(b,a+b) a=t[0]  b=t[1] 
        n = n + 1
    return 'done'
上邊的`a,b = b,a+b`的好處是,不用顯式的寫出臨時變數tuple1這個tuple也可以實現賦值。

顯然,函式fib實際上是定義了斐波拉契數列的推算規則,非常類似generator。要把fib函式變成generator,只需要把`print(b)`改成`yield b`
這就是定義generator的另一種方法,**如果一個函式定義中包含`yield`關鍵字,那麼這個函式就不再是一個普通函式,而是一個generator。**


**這裡要注意,generator和函式的執行順序也是不一樣的。函式時順序執行,遇到return或者最後一句返回。而generator的函式,在每次呼叫next()的時候執行,遇到yield語句返回。再次執行時從上次返回的yield語句處繼續執行。**例如:
#定義generator
def odd():
    print('step 1')
    yield 1
    print('step 2')
    yield(3)
    print('step 3')
    yield(5)
#執行
>>> o = odd()
>>> next(o)
step 1
1
>>> next(o)
step 2
3
>>> next(o)
step 3
5
>>> next(o)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
其實在把函式改成generator後,也基本不用next()來獲取下一個返回值而使用for迴圈。
>>> for n in fib(6):
...     print(n)
...
1
1
...
但是用for迴圈呼叫generator時,發現拿不到generator的return語句的返回值。如果想要拿到返回值,必須捕獲StopIteration錯誤,返回值包含在StopIteration的value中:
>>> g = fib(6)
>>> while True:
...     try:
...         x = next(g)
...         print('g:', x)
...     except StopIteration as e:
...         print('Generator return value:', e.value)
...         break
...
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
Generator return value: done
練習:生成楊輝三角:
# -*- coding: utf-8 -*-

def triangles():
    while True:
        yield L
        L.append(0);
        L = [L[i-1] + L[i] for i in range(len(L))]
n = 0
for t in triangles():
    print(t)
    n = n + 1
    if n == 10:
        break

迭代器

**凡是可作用於for迴圈的物件都是Iterable型別;**
**凡是可作用於next()函式的物件都是Iterator型別,它們表示一個惰性計算的序列;**

可以使用isinstance()判斷一個物件是否是Iterable物件或者Iterator物件:
>>> from collections import Iterable
>>> isinstance([], Iterable)  #判斷是否為可迭代物件
True

>>> isinstance((x for x in range(10)), Iterator)  #判斷是否為迭代器物件
True
你可能會問,為什麼list、dict、str等資料型別不是Iterator?這是因為Python的Iterator物件表示的是一個資料流,Iterator物件可以被next()函式呼叫並不斷返回下一個資料,直到沒有資料時丟擲StopIteration錯誤。可以把這個資料流看做是一個有序序列,但我們卻不能提前知道序列的長度,只能不斷通過next()函式實現按需計算下一個資料,所以Iterator的計算是惰性的,只有在需要返回下一個資料時它才會計算。
Iterator甚至可以表示一個無限大的資料流,例如全體自然數。而使用list是永遠不可能儲存全體自然數的。


小結:
**集合資料型別如list、dict、str等是Iterable但不是Iterator,不過可以通過iter()函式獲得一個Iterator物件。**
**Python的for迴圈本質上就是通過不斷呼叫next()函式實現的,例如:**
for x in [1, 2, 3, 4, 5]:
    pass

#實際上完全等價於
# 首先獲得Iterator物件:
it = iter([1, 2, 3, 4, 5])
# 迴圈:
while True:
    try:
        # 獲得下一個值:
        x = next(it)
    except StopIteration:
        # 遇到StopIteration就退出迴圈
        break

函數語言程式設計

函數語言程式設計就是一種抽象程度很高的程式設計正規化,純粹的函數語言程式設計語言編寫的函式沒有變數,因此,任意一個函式,只要輸入是確定的,輸出就是確定的,這種純函式我們稱之為沒有副作用。而允許使用變數的程式設計語言,由於函式內部的變數狀態不確定,同樣的輸入,可能得到不同的輸出,因此,這種函式是有副作用的。

函數語言程式設計的一個特點就是,允許把函式本身作為引數傳入另一個函式,還允許返回一個函式!

Python對函數語言程式設計提供部分支援。由於Python允許使用變數,因此,Python不是純函數語言程式設計語言。

高階函式

函式可以把返回值賦給變數,也可以把它本身賦給變數,這樣變數就指向了該函式。
函式名其實就是指向函式的變數。如果把一個函式的函式名指向其他東西,就無法再用這個函式名呼叫函式了:
>>> abs = 10
>>> abs(-10)
變數可以指向函式,函式的引數又可以接收變數,那麼**一個函式就可以接受另一個函式作為引數。這種函式就稱為高階函式。**
#一個簡單的高階函式
def add(x,y,f)
    return f(x) + f(y)

add(-5,6,abs)   #這樣呼叫的結果就是  abs(-5) + abs(6) = 11

map/reduce

Python內建了`map()`和`reduce()`函式。
**`map()`函式**
map()函式接收兩個引數,一個是函式,一個是Iterable(可迭代物件),map將傳入的函式一次作用到序列的每個元素。並把結果作為新的Iterator返回。例如:
def f(x):
    return x*x
r = map(f,[1,2,3])  # map傳入第一個引數是函式物件本身
list(r)  # 由於其結果r是一個Iterator,Iterator是惰性序列,因此通過list函式計算出整個序列並返回一個list。
[1,4,9]

# 一行程式碼將list的所有數字轉化為字串
list(map(str,[1,2,3,4,5]))
['1','2','3','4','5']
**`reduce()`函式**
reduce()也接收兩個引數,一個是函式f(該函式也必須接收兩個引數),一個是Iterable(可迭代物件)。reduce()函式會把f的結果和序列的下一個元素做累計計算,例如:
reduce(f,[x1,x2,x3,x4]) = f(f(f(x1,x2),x3),x4)
例如對一個序列求和:當然這裡只是舉個例子,求和可以直接用Python的內建函式sum()
>>> from functools import reduce
>>> def add(x, y):
...     return x + y
...
>>> reduce(add, [1, 3, 5, 7, 9])
25
練習:
# -*- coding: utf-8 -*-
# 整理名字,首字母大寫其餘小寫
def normalize(name):
    newName = name[0].upper() + name[1:].lower()
    return newName
# 測試:
L1 = ['adam', 'LISA', 'barT']
L2 = list(map(normalize, L1))
print(L2)
# 求一個list中所有項的乘積
from functools import reduce
def prod(L):
    return reduce(lambda x,y:x*y,L)
print('3 * 5 * 7 * 9 =', prod([3, 5, 7, 9]))

filter

filter()函式用於過濾序列。filter()也接收一個函式和一個序列。和map()不同的是,filter把傳入的函式一次作用於每一個元素,根據返回的值是True還是False決定保留還是丟棄該元素。
filter()函式返回的是一個Iterator,也就是一個惰性序列,所以要用list()函式獲取所有結果並返回list。

sorted

sorted()排序也是一個高階函式,可以接收一個key函式來實現自定義排序規則,例如按照絕對值大小排序:`sorted([36,-5,12,-46],key=abs)`。key指定的函式來實現自定義的排序,例如安絕對值大小排序。
sorted排序時,對數字預設升序,對字串會因為大小使得大寫字母在前,要忽略大小寫排序時,可以用lower,如果想反向排序,還能再加上reverser=True,如下:
`sorted(['bob','about','Zoo','Create'],key=str.lower)`。

返回函式

高階函式除了可以接收函式作為引數外,還可以把函式作為結果值返回。例如:
def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum
#當我們呼叫lazy_sum()時,返回的並不是求和結果,而是求和函式:
>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90>
#呼叫函式f時,才真正計算求和的結果:
>>> f()
25
#注意,當我們呼叫lazy_sum()時,每次呼叫都會返回一個新的函式,即使傳入相同的引數:
>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1==f2   #f1()和f2()的呼叫結果互不影響。
False
在上例中,我們在函式lazy_sum中又定義了函式sum,並且內部函式sum可以引用外部函式lazy_sum的引數和區域性變數,當lazy_sum返回函式sum時,相關引數和變數都儲存在返回的函式中,這種稱為**閉包**的程式結構具有極大的威力。

**閉包**
閉包可以理解為一個外函式返回了一個在它內部定義的內函式。這個被返回的內函式可以用外函式的引數和區域性變數。
**另外,返回的內函式並沒有立即執行,而是直到呼叫了內函式才執行。因此,返回閉包時牢記一點:返回函式不要飲用任何迴圈變數,或後續會發生變化的變數。**

匿名函式

當我們傳入函式的時候,有些時候並不需要顯式的定義函式,直接傳入匿名函式更加方便。Python有限的支援匿名函式。例如:`list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))`。可以看出,這裡的匿名函式`lambda x: x * x`實際上就是:
def f(x):
    return x*x
關鍵字`lambda`表示匿名函式,冒號前邊的x表示引數,冒號後編寫表示式。匿名函式只能有一個表示式且不用寫return,返回值就是該表示式的結果。
匿名函式也是一個函式物件,可以把它賦值給變數,再利用變數來呼叫該函式。也可以把匿名函式作為返回值。

裝飾器

函式也是一個物件,函式物件還可以被賦值給變數呼叫。函式物件有一個`_name_`屬性,可以拿到函式的名字。

參考部落格Python裝飾器由淺入深

裝飾器(Decorator)裝飾器通常用於在不改變原有函式程式碼和功能的情況下,為其新增額外的功能。比如在原函式執行前先執行點什麼,在執行後執行點什麼。

寫程式碼要遵循**開放封閉原則**,雖然在這個原則主要是針對物件導向開發,但是也適用於函數語言程式設計,簡單來說,它規定已經實現的功能程式碼內部不允許被修改,但外部可以被擴充套件,即:封閉:已實現的功能程式碼塊;開放:對擴充套件開放。

偏函式

Python的functools模組提供了很多有用的功能,其中一個就是偏函式(Partial function)。
用例:
int('123456')  #int()函式可以將字串轉為整數
123456
int('123456',base=2)  #int()函式還可以傳入一個引數,將二進位制字串轉為整數
#若要大量轉換二進位制字串,每次都傳入base=2很麻煩,所以可以寫個函式給個預設引數
def int2(x,base=2)
    return int(x,base)
#functools.partial就是幫助我們建立偏函式,不用自己定義int(),可以直接如下
>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
所以,簡單總結functools.partial的作用就是,把一個函式的某些引數給固定住(也就是設定預設值),返回一個新的函式,呼叫這個新函式會更簡單。
最後,建立偏函式時,實際上可以接收函式物件、*args和**kw這3個引數。

模組

隨著程式碼越來越多,為了方便維護,把他們分組放在不同的檔案中。在Python中,一個.py檔案就成為一個模組(Module)。
再向上一層,又引入了按目錄來組織模組的方式,成為包(Package)。
舉個例子,一個abc.py的檔案就是一個名字叫abc的模組,一個xyz.py的檔案就是一個名字叫xyz的模組。現在,假設我們的abc和xyz這兩個模組名字與其他模組衝突了,於是我們可以通過包來組織模組,避免衝突。方法是選擇一個頂層包名,比如mycompany,按照如下目錄存放:
這裡寫圖片描述

引入了包以後,只要頂層的包名不與別人衝突,那所有模組都不會與別人衝突。現在,abc.py模組的名字就變成了mycompany.abc,類似的,xyz.py的模組名變成了mycompany.xyz。
請注意,每一個包目錄下面都會有一個__init__.py的檔案,這個檔案是必須存在的,否則,Python就把這個目錄當成普通目錄,而不是一個包。__init__.py可以是空檔案,也可以有Python程式碼,因為__init__.py本身就是一個模組,而它的模組名就是mycompany。

使用模組

Python本身內建了許多模組,安裝Python之後即可使用。
使用模組之前要匯入該模組,使用:`import module_name` 即可匯入模組。匯入之後,就有了module_name這個變數指向該模組,利用該變數即可訪問該模組的所有功能。
當使用命令列執行一個模組的時候,Python直譯器會把一個特殊變數`_name_`置為`_main_`。而如果在其他地方匯入該模組時不會。所以可以使用`if _name_ == '_main_'`這個判斷,來讓一個模組在通過命令列執行時執行一些額外程式碼,最常見的就執行測試。
**作用域**
在一個模組中,我們可能會定義很多函式和變數,但有的函式和變數我們希望給別人使用,有的函式和變數我們希望僅僅在模組內部使用。在Python中,是通過_字首來實現的。
正常的函式和變數名是公開的(public),可以被直接引用。類似`_xxx_`這樣的變數是特殊變數,可以被直接引用,但是有特殊的用途。例如`_author_`,`_name_`,`_doc_`等。
類似`_xxx`和`__xxx`這樣的函式或變數就是非公開的(private),不應該被直接引用。
如何使用private函式和變數呢:
def _private_1(name):
    return 'Hello, %s' % name

def _private_2(name):
    return 'Hi, %s' % name

def greeting(name):
    if len(name) > 3:
        return _private_1(name)
    else:
        return _private_2(name)
**我們在模組裡公開greeting()函式,而把內部邏輯用private函式隱藏起來了,這樣,呼叫greeting()函式不用關心內部的private函式細節,這也是一種非常有用的程式碼封裝和抽象的方法,即:外部不需要引用的函式全部定義成private,只有外部需要引用的函式才定義為public。**

安裝第三方模組

Python中安裝第三方模組是使用包管理工具pip完成的。Mac和Linux不用安裝pip。Windows在安裝Python的時候有勾選項即可。在命令列視窗嘗試執行pip可知是否有安裝成功。
安裝第三方模組:`pip install Pillow`即可安裝Pillow模組。
**模組搜尋路徑**
當我們試圖載入一個模組時,Python會在指定路徑下搜尋對應的.py檔案,找不到就會報錯。預設情況下Python直譯器會搜尋當前目錄、所有已安裝的內建模組和第三方模組,搜尋路徑存放在sys模組的path變數中。
如果要新增自己的搜尋目錄,有兩種方式:
1.修改sys.path
2.設定環境變數PYTHONPATH,該環境變數的內容會被自動新增到模組搜尋路徑中。

物件導向程式設計

物件導向程式設計————Object Oriented Programming,簡稱OOP,是一種程式設計思想。OOP把物件作為程式的基本單元,一個物件包含了資料和運算元據的函式。

程式導向的程式設計把計算機程式視為一系列的命令集合,即一組函式的順序執行。為了簡化程式設計,程式導向把函式繼續切分為子函式,即把大塊函式通過切割成小塊函式來降低系統的複雜度。

而**物件導向的程式設計把計算機程式視為一組物件的集合**,而每個物件都可以接收其他物件發過來的訊息,並處理這些訊息,計算機程式的執行就是一系列訊息在各個物件之間傳遞。


**舉例說明程式導向和麵向物件在程式流程上的不同:**
假設處理學生的成績表,程式導向的程式可以用dict表示,然後使用一個函式列印學生的程式。但是物件導向的設計思想,首先考慮的是學生應該被視為一個物件,物件擁有名字和成績的屬性,還要有能夠列印自身的方法。
#  建立Student類
class Student(object):

    def __init__(self, name, score):
        self.name = name
        self.score = score

    def print_score(self):
        print('%s: %s' % (self.name, self.score))
# 根據類建立例項
bart = Student('Bart Simpson', 59)
lisa = Student('Lisa Simpson', 87)
bart.print_score()
lisa.print_score()
物件導向的設計思想是從自然界中來的,因為在自然界中,類(Class)和例項(Instance)的概念是很自然的。Class是一種抽象概念,比如我們定義的Class——Student,是指學生這個概念,而例項(Instance)則是一個個具體的Student,比如,Bart Simpson和Lisa Simpson是兩個具體的Student。
所以,物件導向的設計思想是抽象出Class,根據Class建立Instance。

類和例項

物件導向最重要的概念就是類(Class)和例項(Instance),必須牢記類是抽象的模板,比如Student類,而例項是根據類建立出來的一個個具體的“物件”,每個物件都擁有相同的方法,但各自的資料可能不同。

在Python中,通過class關鍵字定義類。定義時可以把一些我們認為必須繫結的屬性強制填寫進去。這裡的_init_的第一個引數永遠是self,表示建立的例項本身。因此在_inti_方法內部,可以把各種屬性繫結到self,即例項本身。
class Student(object)  # class後邊緊接著是類名,即Student,通常大寫開頭,緊接著是(object),表示該類是從哪個類繼承下來的,繼承的概念我們後面再講,通常,如果沒有合適的繼承類,就使用object類,這是所有類最終都會繼承的類。

    def _init_(self,name,score)  #每個例項都要有name和score屬性
        self.name = name
        self.score = score
有了_init_方法,建立例項時就不能穿入空引數了,必須傳入與_init_匹配的引數。self不用傳,Python直譯器會自己傳。
定義好Student類之後,就可以根據他來建立Student例項:
>>> bart = Student('Tom',59)
>>> bart  #可以看到,變數bart指向的就是一個Student的例項,後面的0x10a67a590是記憶體地址
<__main__.Student object at 0x10a67a590>
>>> Student  #而Student本身則是一個類。
<class '__main__.Student'>
類中定義的函式與普通函式的唯一區別在於,他的第一個引數永遠是例項變數self,並且呼叫時不用傳入該引數。

**資料封裝**
物件導向變成的重要特點就是資料封裝。上邊的Student類中,每個例項都有name和score資料,可以通過函式來訪問這些資料,比如列印成績。但是既然例項本身有資料,就可以直接在類的內部定義訪問資料的函式,這樣就把資料給封裝起來了。這些封裝資料的函式是和類本身關聯起來的,我們稱為類方法:
class Student(object):

    def __init__(self, name, score):
        self.name = name
        self.score = score

    def print_score(self):  #第一個引數同樣必須是self
        print('%s: %s' % (self.name, self.score))

訪問限制

在Class內部,可以有屬性和方法。而外部程式碼可以直接呼叫例項變數的方法來運算元據。這樣就隱藏了內部的複雜邏輯。
同時,外部程式碼還可以自由的修改一個例項的屬性。如果要讓內部屬性不被外部訪問,可以在屬性名稱前加上**兩個下劃線`__`。**在Pyhon中,例項的變數若以__開頭,就變成了私有變數(private),只有內部可以訪問,外部不能訪問。例如:
class Student(object):

    def __init__(self, name, score):
        self.__name = name   # 這樣__name就是私有變數了,只能在例項內部訪問。
        self.__score = score

    def print_score(self):
        print('%s: %s' % (self.__name, self.__score))

#例項外部程式碼:
例項變數.__name  #由於是私有變數,已經無法再外部這樣訪問了。
這樣就確保了外部程式碼不能隨意修改物件內部的狀態,保護物件。如果想讓外部程式碼獲取和更改內部變數,可以再給類增加相應getter和setter方法即可。在setter方法中還可以對傳入引數進行檢查,防止傳入錯誤的引數。
**注意:在Python中,變數名類似`__xxx__`的,也就是以雙下劃線開頭且結尾的,是特殊變數,特殊變數可以直接訪問,不是private變數。只有類似`__xxx`的,以雙下劃線開頭的,才是private,而private實際上也只是Python把`__xxx`改成了`_ClassName__xxx`而已,但是不要耍小聰明用這種方式訪問private,因為Python直譯器可能會改成其他寫法。最後,有可能會遇到類似`_xxx`的單下劃線開頭,意思是雖然能夠從外部訪問,但是請不要隨意訪問我,把我視為private。**

繼承和多型

在OOP程式設計中,定義一個class的時候,可以從某個現有的class繼承,新的class稱為子類(Subclass),而被繼承的class成為基類,父類或超類(Base class/Super class)。子類會繼承父類的全部功能
class Animal(object):   #建立一個animal類
    def run(self):
        print('Animal is running...')
class Dog(Animal): #建立一個dog類,繼承自animal,直接擁有run方法
    pass
class Cat(Animal):  #建立一個cat類,繼承自animal,直接擁有run方法
    def run(self):  # 如果在子類建立一個同名方法,會覆蓋父類的方法。
        print('Cat is running...') 
當我們定義一個class的時候,我們實際上就定義了一種資料型別。我們定義的資料型別和Python自帶的資料型別,比如str、list、dict一樣,也可以使用`isinstance(a,Animal)`這樣的方式來判斷。而子類因為繼承子父類,所以一個dog,即是Dog型別,又是Animal型別。這就是多型。
多型的好處在於,我們可以編寫一些函式,他們接收父類Animal資料型別作為變數,而Animal的子類Cat,Dog類的例項都可以在這些函式中執行,並且會根據Cat和Dog自身的情況來執行。如果以後又增加了Animal的子類Bird類,也不需要回頭去修改這些函式,這就是“開閉”原則:
**對擴充套件開放:允許新增Animal子類;**
**對修改封閉:不需要修改依賴Animal型別的函式。**

繼承還可以一級一級地繼承下來,就好比從爺爺到爸爸、再到兒子這樣的關係。而任何類,最終都可以追溯到根類object,類似DOM樹的繼承樹。


**靜態語言 vs 動態語言**
對於靜態語言(例如Java)來說,如果需要傳入Animal型別,則傳入的物件必須是Animal型別或者它的子類,否則,將無法呼叫run()方法。
對於Python這樣的動態語言來說,則不一定需要傳入Animal型別。我們只需要保證傳入的物件有一個run()方法就可以了。
這就是動態語言的“鴨子型別”,它並不要求嚴格的繼承體系,一個物件只要“看起來像鴨子,走起路來像鴨子”,那它就可以被看做是鴨子。

Python的“file-like object“就是一種鴨子型別。對真正的檔案物件,它有一個read()方法,返回其內容。但是,許多物件,只要有read()方法,都被視為“file-like object“。許多函式接收的引數就是“file-like object“,你不一定要傳入真正的檔案物件,完全可以傳入任何實現了read()方法的物件。

獲取物件資訊

當我們拿到一個物件的引用時,如何知道這個物件是什麼型別,有哪些方法?
使用`type()`函式判斷物件型別。他返回對應的Class型別,int,str等。
使用`isinstance()`函式判斷是否是某種型別。對於物件和繼承很好用。isinstance還可以判斷變數是否是多種型別中的一種。傳入個tuple即可。
使用`dir()`函式可以獲得一個包含字串的list,其中包含該物件的所有屬性和方法。

例項屬性和類屬性

由於Python是動態語言,根據類建立的例項可以任意繫結屬性。obj.name = Tom 即可。但是我們也可以給類本身繫結屬性。可以直接在class中定義屬性,這種屬性是類屬性,歸類所有:
class Student(object)
    name = 'Student'
當我們定義了一個類屬性的時候,這個屬性歸類所有,但是該類的所有例項都可以訪問該屬性。如果例項中定義了或賦予了同名屬性,會優先訪問例項屬性,如果沒有就訪問類屬性。所以實際程式設計的時候,要避免例項屬性和類屬性同名。

物件導向高階程式設計

資料封裝,繼承和多型只是物件導向程式設計中最基礎的3個概念。在Python中,物件導向還有很多高階特性,允許我們寫出非常強大的功能。例如多重繼承,定製類,元類等概念。

使用slots

定義一個class,建立一個例項之後,還可以給該例項繫結任何屬性和方法,這就是動態語言的靈活性。當然,給一個例項繫結的方法,對另一個例項是不起作用的。
想給所有例項都繫結方法,那就直接給class繫結方法。這樣所有的例項均可呼叫該方法。動態繫結還允許我們在程式的執行過程中動態給class加上功能,這在靜態語言中很難實現。

如果我們想要限制例項的屬性,只允許對例項新增固定的屬性,就可以使用`__slots__`。
Python允許在定義class的時候,定義一個特殊的`__slots__`變數,來限制該class例項能新增的屬性:
class Student(object):
    __slots__ = ('name','age')  #用tuple定義允許繫結的屬性名稱。對例項繫結其他屬性會報錯。
**注意:`__slots__`定義的屬性僅對當前類的例項起作用,對繼承的子類的例項是不起作用的。除非在子類中也定義slots,這樣,子類例項允許定義的屬性就是自身slots機上父類的slots。**

使用@property

之前說過,在繫結屬性時為了不隨意修改屬性,會把屬性變為private,然後通過getter和setter去讀寫,在setter中還可以對屬性範圍進行檢查。但這樣略顯複雜,Python有更簡單的方式:
Python內建的`@property`裝飾器負責**把一個方法變成屬性呼叫**。
class Student(object):

    @property             # 用@property裝飾器裝飾birth屬性
    def birth(self):      # birth的getter
        return self._birth

    @birth.setter      # 把birth的setter函式變為一個屬性
    def birth(self, value):   # birth的setter
        self._birth = value

    @property          #只裝飾age,不設定setter屬性,就把age變成了一個只讀屬性
    def age(self):
        return 2015 - self._birth
# 呼叫的時候
>>> s = Student()  #例項s
>>> s.birth = 1991 # OK,實際轉化為s.set_birth(1991)  
>>> s.birth # OK,實際轉化為s.get_birth()
**能看出,`@property`裝飾器主要在於簡化了呼叫,實際上還是要寫一個getter和setter函式的。**

多重繼承

之前已經說過了繼承,通過繼承,子類可以獲得父類的功能。多重繼承其實就是繼承自多個父類,如下:
class Dog(Animal)   # 繼承
    pass
class Cat(Animal,Runnable)  # 多重繼承
    pass
# 這種多重繼承的設計又稱為MixIn。為了更好地看出繼承關係,可以
class Cat(Animal,RunnableMinIn)  # 用MixIn來表明繼承關係
    pass
MixIn的目的就是給一個類增加多個功能,這樣,在設計類的時候,我們優先考慮通過多重繼承來組合多個MixIn的功能,而不是設計多層次的複雜的繼承關係。
由於Python允許使用多重繼承,因此,MixIn就是一種常見的設計。只允許單一繼承的語言(如Java)不能使用MixIn的設計。

定製類

之前說過,類似`__xxx__`這種變數或函式名,就表示他們在Python中是有特殊用途的。例如
  • __slots__():限制例項可設定的屬性範圍。
  • __len__():讓class作用於len()函式,求長度。
  • __str__():改變類列印出來的內容。
  • __repr__():返回開發者看到的字串,有時可以: __repr__ = __str__
  • __iter__():返回一個迭代函式。使得calss可以被用於for迴圈。
  • __next__():例項在for迴圈的時候就是不斷呼叫這個方法來拿到下一個值。
  • __getitem__():表現的像list那樣按照下標取出元素,還可以傳入切片物件slice。
  • __getattr__():呼叫方法或屬性。沒找到屬性的時候會在該函式中查詢。
  • __call__():直接呼叫例項。

使用列舉類

定義常量的時候,可以使用列舉類。
from enum import Enum

Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
#這樣我們就獲得了Month型別的列舉類,可以直接使用Month.Jan來引用一個常量,或者列舉它的所有成員:
for name, member in Month.__members__.items():
    print(name, '=>', member, ',', member.value)
#value屬性則是自動賦給成員的int常量,預設從1開始計數。
如果需要更精確地控制列舉型別,可以從Enum派生出自定義類。
@unique裝飾器可以幫助我們檢查保證沒有重複值。

**Enum可以把一組相關常量定義在一個class中,且class不可變,而且成員可以直接比較**

使用元類

**type()**
動態語言和靜態語言最大的不同,就是函式和類的定義,不是編譯時定義的,而是執行時動態建立的。type()函式可以檢視一個型別或變數的型別,Hello是一個class,它的型別就是type,而h是一個例項,它的型別就是class Hello。


**metaclass**
metaclass,直譯為元類,簡單的解釋就是:當我們定義了類以後,就可以根據這個類建立出例項,所以:先定義類,然後建立例項。但是如果我們想建立出類呢?那就必須根據metaclass建立出類,所以:先定義metaclass,然後建立類。連線起來就是:先定義metaclass,就可以建立類,最後建立例項。

**metaclass是Python中非常具有魔術性的物件,它可以改變類建立時的行為。這種強大的功能使用起來務必小心。**

錯誤、除錯和測試

Python內建了一套異常處理機制,來幫助我們進行錯誤處理。

編寫測試也很重要。有了良好的測試,就可以在程式修改後反覆執行,確保程式輸出符合我們編寫的測試。

錯誤處理

高階語言通常都內建了一套try...except...finally...的錯誤處理機制,Python也不例外。
**try**
try:    #當我們認為某些程式碼可能會出錯時,就可以用try來執行這段程式碼
    print('try...')
    r = 10 / 0
    print('result:', r)
except ZeroDivisionError as e: #如果執行出錯,則後續程式碼不會繼續執行,而是直接跳轉至錯誤處理程式碼,即except語句塊
    print('except:', e)
finally:  #執行完except後,如果有finally語句塊,則執行finally語句塊,至此,執行完畢。
    print('finally...')
print('END')
如果有錯誤發生: try ⇒  except ⇒ finally
如果沒有錯誤發生: try ⇒ finally
finally也可以不存在,但如果存在就一定會被執行。
可以有多個except捕獲不同的錯誤,此外還可以在except後邊加一個else,當沒有錯誤發生時自動執行else語句:
try:
    print('try...')
    r = 10 / int('2')
    print('result:', r)
except ValueError as e:
    print('ValueError:', e)
except ZeroDivisionError as e:
    print('ZeroDivisionError:', e)
else:
    print('no error!')
finally:
    print('finally...')
print('END')
使用try...except捕獲錯誤還有一個巨大的好處,就是可以跨越多層呼叫,比如函式main()呼叫foo(),foo()呼叫bar(),結果bar()出錯了,這時,只要main()捕獲到了,就可以處理。也就是說,不需要在每個可能出錯的地方去捕獲錯誤,只要在合適的層次去捕獲錯誤就可以了。這樣一來,就大大減少了寫try...except...finally的麻煩。


**呼叫堆疊**
如果錯誤沒有被捕獲,會一直往上拋,最後被Python直譯器捕獲,列印一個錯誤資訊然後退出程式。根據錯誤資訊,從上往下可以看到整個錯誤的呼叫函式鏈。跟蹤該鏈,就可以找到錯誤源頭。

**丟擲錯誤**
其實錯誤也是class,捕獲一個錯誤就是捕獲一個例項。錯誤是有意建立並且丟擲的。Python內建函式會丟擲很多型別的錯誤,我們自己編寫的函式也可以丟擲錯誤。

除錯

程式出錯的時候需要除錯,可能要看看變數的值之類的,比如print變數,但是除錯完還要刪掉,韓麻煩。
**斷言(assert)**
凡是用print來檢視的地方都可以用assert來替代。例如:
def foo(s):
    n = int(s)
    assert n != 0, 'n is zero!'
    return 10 / n

def main():
    foo('0')
assert的意思是斷言n != 0應該為True,即n不能為0,否則程式肯定會出錯。
若斷言失敗就會丟擲錯誤,當然程式中處處都有assert也不好,所以Python直譯器啟動的時候可以用 -0 引數來關閉assert,關閉後assert就直接當pass了。


**logging**
還有一種方式是把print替換為logging。相比assert,logging不會丟擲錯誤,而且可以輸出到檔案。logging允許你指定記錄資訊的級別,有debug,info,warning,error等幾個級別。logging還可以通過配置,一條語句同時輸出到不同的地方。


**pdb**
第四種方式是Python的偵錯程式pdb,讓程式單步執行,隨時檢視狀態。
使用`python -m pdb xxx.py` 啟動。

**IDE**
最爽的還是使用IDE吧,目前比較好的Python IDE有PyCharm。
另外Eclipse加上pydev外掛也可以除錯Python程式。

單元測試

單元測試是用來對一個模組、一個函式或者一個類來進行正確性檢驗的測試工作。比如要測試一個abs()函式,可能要編寫正數,負數,0,None等值,然後期待相應的結果。把這些測試用例放在一個測試模組裡就是一個完整的單元測試。

單元測試的好處在於,如果我們修改了abs()函式,只要再執行一次單元測試,就能知道我們的修改有沒有對原函式造成影響了。

可以在單元測試中編寫兩個特殊的setUp()和tearDown()方法。這兩個方法會分別在每呼叫一個測試方法的前後分別被執行。setUp()和tearDown()方法有什麼用呢?設想你的測試需要啟動一個資料庫,這時,就可以在setUp()方法中連線資料庫,在tearDown()方法中關閉資料庫,這樣,不必在每個測試方法中重複相同的程式碼。

文件測試

Python的文件測試模組可以直接提取出註釋中的程式碼並執行測試。

IO程式設計

IO在計算機中指Input/Output,也就是輸入和輸出。
IO程式設計中,Stream(流)是一個很重要的概念,可以把流想象成一個水管,資料就是水管裡的水,但是隻能單向流動。Input Stream就是資料從外面(磁碟、網路)流進記憶體,Output Stream就是資料從記憶體流到外面去。

非同步IO的複雜度遠遠高於同步IO。

檔案讀寫

讀寫檔案時常見的IO操作,Python內建了讀寫檔案的函式,用法是和C相容的。
在磁碟上讀寫檔案的功能都是有作業系統提供的,現在作業系統不允許普通程式直接操作磁碟,所以讀寫文見就是請求作業系統開啟檔案物件,然後通過作業系統提供的介面來讀寫檔案。

**讀檔案**、
使用Python內建的`open()`函式,傳入檔案位置和識別符號。
例如:`f = open('/User/hehe/test.txt','r')` 。其中識別符號r表示讀。這樣就成功的開啟了一個檔案,如果檔案不存在會報錯。接下來使用read()方法來讀取檔案的全部內容,Python把內容讀取到記憶體,用一個str物件。`f.read()`。最後呼叫`close()`方法關閉檔案。
**檔案使用完後必須關閉,因為檔案物件會佔用作業系統的資源,並且作業系統同一時間能開啟的檔案數量也是有限的。**


由於讀寫檔案都可能產生IOError,一旦出錯後邊的f.close()就不會呼叫無法關閉檔案。所以為了保證無論如何都能正確關閉檔案,可以用  try....finally來實現。但是Python還給出了更簡單的方式,使用with語句來幫我們呼叫close:
with open('/path/to/file', 'r') as f:
    print(f.read())
呼叫read()會一次性讀取檔案的全部內容,如果檔案太大記憶體就爆了。所以保險起見,可以反覆呼叫read(size)方法,每次讀取size個位元組的內容。另外呼叫readline()可以每次讀取一行內容,呼叫readlines()一次讀取所有內容並行返回list。可以根據需求決定如何呼叫。
如果檔案很小,read()一次性讀取最方便,如果確定檔案大小,反覆呼叫read(size)比較保險,如果是配置檔案,呼叫readlines()最方便。
for line in f.readlines():
    print(line.strip()) # 把末尾的'\n'刪掉
**file-like Object**
像open()函式返回的這種有read()方法的物件,在Pyton中統稱為file-like Object。除了file,還可以是記憶體的位元組流,網路流等等。
StringIO就是在記憶體中建立的file-like Object,常用作臨時緩衝。


**二進位制檔案**
前邊說的預設都是文字,並且是UTF-8編碼的文字。要讀取二進位制檔案,如圖片視訊等,就要用'rb'模式開啟檔案。
>>> f = open('/Users/michael/test.jpg', 'rb')
>>> f.read()
b'\xff\xd8\xff\xe1\x00\x18Exif\x00\x00...' # 十六進位制表示的位元組
**字元編碼**
要讀取非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')
**寫檔案**
寫檔案和讀檔案是一樣的,唯一的區別是呼叫open()函式時,傳入識別符號'w'或者'wb'表示寫文字檔案或寫二進位制檔案。
>>> f = open('/Users/michael/test.txt', 'w')
>>> f.write('Hello, world!')
>>> f.close()
#或者使用with
with open('/Users/michael/test.txt', 'w') as f:
    f.write('Hello, world!')
要寫入特定編碼的文字檔案,請給open()函式傳入encoding引數,將字串自動轉換成指定編碼。

StringIO和BytesIO

**StringIO**
很多時候,資料讀寫不一定是檔案,也可以在記憶體中讀寫。StringIO顧名思義就是在記憶體中讀寫str。要把str寫入StringIO,我們需要先建立一個StringIO,然後,像檔案一樣寫入即可:
>>> from io import StringIO
>>> f = StringIO()
>>> f.write('hello')
5
>>> f.write(' ')
1
>>> f.write('world!')
6
>>> print(f.getvalue())   #getvalue()方法用於獲得寫入後的str。
hello world!
要讀取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'

操作檔案和目錄

Python內建的os模組可以直接呼叫作業系統提供的介面函式。
在Python互動式命令列試試os模組的基本功能:
import os
os.name  #作業系統型別
'posix'  # 說明系統是Linux,Unix或Max OS X
'nt'     # 說明系統是Windows
os.uname()  # 獲取系統詳細資訊,Windows不可用。
 **環境變數**:在作業系統中定義的環境變數,全部儲存在os.environ這個變數中,可以直接檢視。獲取某個環境變數的值可以呼叫  os.environ.get('key')。
 **操作檔案和目錄**:操作檔案和目錄的函式一部分放在os模組中,一部分放在os.path模組中,這一點要注意下。檢視,建立和刪除目錄可以這樣呼叫:
# 檢視當前目錄的絕對路徑
os.path.abspath('.')
# 在某個目錄下建立一個新目錄,首先把新目錄的完整路徑表示出來:
os.path.join('D:\Python','testdir')
# 然後用上一句返回的路徑,建立一個目錄
os.mkdir('D:\Python\Creabine')
# 刪除一個目錄
os.rmdir('D:\Python\Creabine')
把兩個路徑合成一個時,不要直接拼接字串,而要通過`os.path.join()`函式,這樣可以正確處理不用作業系統的路徑分隔符。在Linux/Unix/Mac下,`os.path.join()`返回這樣的字串:`part-1/part-2`,而Windows會返回這樣的字串:`part-1\part-2`。同樣的道理,拆分路徑也不要拆字串,使用`os.path.split()`函式,這樣可以把路徑拆分為兩部分,後一部分總是最後級別的檔案或目錄。如:
os.path.split('D:\Python\Creabine')
('D:\\Python','Creabine')
使用`os.path.splitext()`可以直接讓你得到副檔名:
os.path.splitext('D:\Python\test.py')
('D:\Python\test','.py')
這些合併拆分路徑的函式並不要求目錄和檔案要真實存在,他們只對字串進行操作。
檔案操作使用下邊的函式:
# 對檔案重新命名,在當前目錄下操作
os.rename('test.txt','hehe.py')
# 刪掉檔案
os.remove('hehe.py')
**複製檔案的函式在os中不存在,因為複製檔案並非由作業系統提供系統呼叫。理論上可以通過檔案讀寫完成複製,但要寫很多程式碼。幸運的是`shutil`模組提供了`copyfile()`函式,以及其他很多實用函式,他們看以看做是os模組的補充。**

最後看看如何用Python的特性來過濾檔案:
# 遍歷當前路徑下的檔案和資料夾:
os.listdir('.')
# 遍歷當前目錄下的所有資料夾
[x for x in os.listdir('.') if os.path.isdir(x)]
# 遍歷當前目錄下的所有 .py  檔案
[x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1]=='.py']

序列化

在程式執行的過程中,所有的變數都是存在記憶體中。當程式執行結束,變數所佔用的記憶體就被作業系統全部回收,如果沒有把內容儲存到磁碟上,下次重新執行,程式執行過程中改變的變數就會被初始化回去。
我們把變數從記憶體中變成可儲存或傳輸的過程稱之為序列化,在Python中叫pickling。序列化之後,就可以把序列化後的內容寫入磁碟,或者通過網路傳輸到別的機器上。反過來,把變數內容從序列化的物件重新讀到記憶體裡稱之為反序列化,即unpickling。
**Python提供了pickle模組來實現序列化。**
嘗試講一個物件序列化並寫入檔案:
import pickle
d = dict(name='Bob',age=20,score=88)
pickle.dumps(d)
`pickle.dumps()`方法把任意物件序列化成一個bytes,然後就可以把這個bytes寫入檔案。或者用另一個方法`puckle.dump()`直接把物件序列化後寫入一個file-like Object:
f = open('dump.txt','wb')
pickle.dump(d,f)
f.close()
此時寫入的dump.txt檔案裡,是一堆亂七八糟的內容,這是Python儲存的物件內部資訊。
當我們要把物件從磁碟讀到記憶體時,可以先把內容讀到一個bytes,然後用`pickle.loads()`方法反序列化出物件,也可以直接用`pickle.load()`方法從一個file-like Object中直接反序列化出物件。
f = open('dump.txt','rb')
d = pickle.load(f)
f.close()
d
['age':20,'score':88,'name':'Bob']
Pickle的問題在於,他只能用於Python,並且不同版本的Python彼此之間都不相容,因此只能用來儲存那些不重要的資料,不能成功的反序列化也沒關係。

**JSON**:若想在不用的語言間傳遞物件,就要把物件序列化為標準格式,如XML,蛋更好的方式是序列化為JSON,他可以被所有語言讀取,也可以方便的存到磁碟或通過網路傳輸。
JSON和Python內建的資料型別的對應如下:
JSON型別 Python型別
{} dict
[] list
‘string’ str
123.45 int或float
true/false True/False
null None

Python內建的JSON模組提供了完善的Python物件到JSON格式的轉換:

import json
d = dict(name='Bob'.age=20,score=88)
json.dumps(d)   # 強Python物件變為一個JSON
'{"age":20,"score":88,"name":"Bob"}'
dumps()方法返回一個str,內容就是標準的JSON。類似的,dump()方法可以直接把JSON寫入一個file-like Object。
要把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的字串之間轉換。

**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))
執行上邊的程式碼會報錯,因為Student物件並不是一個可序列化為JSON的物件。此時我們要給dumps()方法多傳入一個引數即可:`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>

程式和執行緒

過去的單核CPU執行多工,是通過輪流執行各個任務交替進行的,由於CPU運算速度快,給人的感覺就是多個任務同時執行。真正的並行執行多工只能在多核CPU上實現。但是一般任務數量遠多於CPU核心的數量,所以系統也會自動把任務輪流排程到每個核心上執行。

對作業系統來說,一個任務就是一個程式(Process),比如開啟一個檔案就是一個程式,兩個檔案就是兩個程式。有的程式的內部,會同時幹多件事,就要同時執行多個子任務,這些子任務稱為執行緒(Thread)。所以,一個程式至少有一個執行緒。

之前寫的Python程式都是執行單任務的程式,也就只有一個執行緒,實際上的多工有以下3種模式:
- 多程式模式
- 多執行緒模式
- 多程式+多執行緒模式(複雜,很少用)
Python既支援多程式,又支援多執行緒。

執行緒是最小的執行單元,而程式由至少一個執行緒組成。如何排程程式和執行緒,完全由作業系統決定,程式自己不能決定什麼時候執行,執行多長時間。

多程式和多執行緒的程式涉及到同步、資料共享的問題,編寫起來更復雜。

多程式

multiprocessing

如果你打算編寫多程式的服務程式,Unix/Linux無疑是正確的選擇。由於Windows沒有fork呼叫,難道在Windows上無法用Python編寫多程式的程式?

由於Python是跨平臺的,自然也應該提供一個跨平臺的多程式支援。multiprocessing模組就是跨平臺版本的多程式模組。

多執行緒

ThreadLocal

程式 vs. 執行緒

分散式程式

正規表示式

常用內建模組

datetime

collections

base64

struct

hashlib

itertools

XML

HTMLParser

urllib

常用第三方模組

PIL

virtualenv

圖形介面

網路程式設計

TCP/IP簡介

TCP程式設計

UDP程式設計

電子郵件

SMTP傳送郵件

POP3收取郵件

訪問資料庫

使用SQLite

使用MySQL

使用SQLAlchemy

Web開發

最早的軟體都是執行在大型機上的,軟體使用者通過“啞終端”登陸到大型機上去執行軟體。後來隨著PC機的興起,軟體開始主要執行在桌面上,而資料庫這樣的軟體執行在伺服器端,這種Client/Server模式簡稱CS架構。

隨著網際網路的興起,人們發現,CS架構不適合Web,最大的原因是Web應用程式的修改和升級非常迅速,而CS架構需要每個客戶端逐個升級桌面App,因此,Browser/Server模式開始流行,簡稱BS架構。

Python有上百種Web開發框架,有很多成熟的模板技術,選擇Python開發Web應用,不但開發效率高,而且執行速度快。

HTTP協議簡介

HTTP是在網路上傳輸HTML的協議,用於瀏覽器和伺服器的通訊。

Web採用的HTTP協議採用了非常簡單的請求-響應模式,從而大大簡化了開發。當我們編寫一個頁面時,我們只需要在HTTP請求中把HTML傳送出去,不需要考慮如何附帶圖片、視訊等,瀏覽器如果需要請求圖片和視訊,它會傳送另一個HTTP請求,因此,一個HTTP請求只處理一個資源。

HTTP響應如果包含body,也是通過\r\n\r\n來分隔的。請再次注意,Body的資料型別由Content-Type頭來確定,如果是網頁,Body就是文字,如果是圖片,Body就是圖片的二進位制資料。

當存在Content-Encoding時,Body資料是被壓縮的,最常見的壓縮方式是gzip,所以,看到Content-Encoding: gzip時,需要將Body資料先解壓縮,才能得到真正的資料。壓縮的目的在於減少Body的大小,加快網路傳輸。

HTML簡介

介紹了HTML,CSS,JS,這裡就不看了。

WSGI介面

瞭解了HTTP協議和HTML文件,我們其實就明白了一個Web應用的本質就是:

  1. 瀏覽器傳送一個HTTP請求;
  2. 伺服器收到請求,生成一個HTML文件;
  3. 伺服器把HTML文件作為HTTP響應的Body傳送給瀏覽器;
  4. 瀏覽器收到HTTP響應,從HTTP Body取出HTML文件並顯示。

所以,最簡單的Web應用就是先把HTML用檔案儲存好,用一個現成的HTTP伺服器軟體,接收使用者請求,從檔案中讀取HTML,返回。Apache、Nginx、Lighttpd等這些常見的靜態伺服器就是幹這件事情的。

要動態生成HTML的話,Python使用WSGI(Web Server Gateway Interface)介面來處理TCP連結,HTTP請求等底層邏輯。

使用Web框架

使用模板

非同步IO

協程

asyncio

async/await

aiohttp

實戰

相關文章