python 基礎知識
本文所有內容是學習期間做的筆記,僅為個人查閱和復習方便而記錄。所有內容均摘自:http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000
數據類型
- 整數
- 浮點數
- 字符串
- 如果字符串內部既包含
'
又包含"
,可以用轉義字符\
來轉義。- 多行字符串可以通過
'''
字符串內容'''
來表示r''
表示''
內部的字符串默認不轉義
- 布爾值,
true
,false
;布爾值可以用and
、or
和not
運算 - 空值,
None
- 變量, 變量名必須是大小寫英文、數字和
_
的組合,且不能用數字開頭
- 常量,
全部大寫
的變量名表示常量
一種除法是//
,稱為地板除,兩個整數的除法仍然是整數
>>> 10 // 3
3
list和tuple(元組)的區別
list定義,classmates = ['Michael', 'Bob', 'Tracy']
tuple定義,classmates = ('Michael', 'Bob', 'Tracy')
list可變
,tuple不變
。- list用
[]
進行定義,tuple用()
進行定義。都可以通過正整數和負數進行下標的獲取。
tuple所謂的“不變”是說,tuple的每個元素,`指向永遠不變`。即指向'a',就不能改成指向'b',指向一個list,就不能改成指向其他對象,但指向的這個list本身是可變的!
條件判斷
if <條件判斷1>:
<執行1>
elif <條件判斷2>:
<執行2>
elif <條件判斷3>:
<執行3>
else:
<執行4>
注意條件判斷后面的:
,不要少寫了。
循環
- for...in循環
names = ['Michael', 'Bob', 'Tracy']
for name in names:
print(name)
- range()函數,可以生成一個整數序列,再通過list()函數可以轉換為list
- while循環
sum = 0
n = 99
while n > 0:
sum = sum + n
n = n - 2
print(sum)
dict和set
dict:字典,類似於map,可重復,快速查找。
>>> d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
>>> d['Michael']
95
和list比較,dict有以下幾個特點:
- 查找和插入的速度極快,不會隨着key的增加而變慢;
- 需要占用大量的內存,內存浪費多。
而list相反:
- 查找和插入的時間隨着元素的增加而增加;
- 占用空間小,浪費內存很少。
所以,dict是用空間來換取時間的一種方法。
set:集合,不可重復。要創建一個set,需要提供一個list作為輸入集合
>>> s = set([1, 2, 3])
>>> s
{1, 2, 3}
函數
定義函數
定義一個函數要使用def
語句,依次寫出函數名、括號、括號中的參數和冒號:
,然后,在縮進塊中編寫函數體,函數的返回值用return
語句返回。
def my_abs(x):
if x >= 0:
return x
else:
return -x
注意,函數體內部的語句在執行時,一旦執行到return
時,函數就執行完畢,並將結果返回。因此,函數內部通過條件判斷和循環可以實現非常復雜的邏輯。
如果沒有return
語句,函數執行完畢后也會返回結果,只是結果為None
。
return None
可以簡寫為return
。
- 空函數
def nop():
pass
pass
語句什么都不做,
- 參數檢查
數據類型檢查可以用內置函數isinstance()
實現
def my_abs(x):
if not isinstance(x, (int, float)):
raise TypeError('bad operand type')
if x >= 0:
return x
else:
return -x
- ** 返回多個值 **
比如在游戲中經常需要從一個點移動到另一個點,給出坐標、位移和角度,就可以計算出新的新的坐標:
import math
def move(x, y, step, angle=0):
nx = x + step * math.cos(angle)
ny = y - step * math.sin(angle)
return nx, ny
- 其實返回值是一個tuple!但是,在語法上,返回一個tuple可以省略括號,而多個變量可以同時接收一個tuple,按位置賦給對應的值,所以,Python的函數返回多值其實就是返回一個tuple,但寫起來更方便。
- 小結
定義函數時,需要確定函數名和參數個數;
如果有必要,可以先對參數的數據類型做檢查;
函數體內部可以用return隨時返回函數結果;
函數執行完畢也沒有return語句時,自動return None。
函數可以同時返回多個值,但其實就是一個tuple。
函數參數
- 默認參數
def power(x, n=2):
s = 1
while n > 0:
n = n - 1
s = s * x
return s
設置默認參數時,有幾點要注意:
- 一是必選參數在前,默認參數在后,否則Python的解釋器會報錯(思考一下為什么默認參數不能放在必選參數前面);
- 二是如何設置默認參數。
當函數有多個參數時,把變化大的參數放前面,變化小的參數放后面。變化小的參數就可以作為默認參數。
使用默認參數有什么好處?最大的好處是能降低調用函數的難度。
** 定義默認參數要牢記一點:默認參數必須指向不變對象! 如果是list,可用L=None
來聲明 **
- 可變參數
在參數前面加了一個*
號即可。
定義:
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
調用方式1:
>>> calc(1, 2)
5
>>> calc()
0
調用方式2:可通過在list或tuple前面加一個*
號,把list或tuple的元素變成可變參數傳進去
>>> nums = [1, 2, 3]
>>> calc(*nums)
14
- 關鍵字參數
定於:
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)
調用方式1:
>>> person('Michael', 30)
name: Michael age: 30 other: {}
調用方式2:
>>> person('Bob', 35, city='Beijing')
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}
調用方式3:
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
- 命名關鍵字參數
定義方式:
def person(name, age, *args, city, job):
print(name, age, args, city, job)
或
def person(name, age, *, city='Beijing', job):
print(name, age, city, job)
調用方式:
>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer
- 要特別注意,如果沒有可變參數,就必須加一個作為特殊分隔符。如果缺少,Python解釋器將無法識別位置參數和命名關鍵字參數
- 小結
- Python的函數具有非常靈活的參數形態,既可以實現簡單的調用,又可以傳入非常復雜的參數。
- 默認參數一定要用不可變對象,如果是可變對象,程序運行時會有邏輯錯誤!
- 要注意定義可變參數和關鍵字參數的語法:
*args
是可變參數,args接收的是一個tuple;
**kw是
關鍵字參數,kw接收的是一個dict。
- 以及調用函數時如何傳入可變參數和關鍵字參數的語法:
- 可變參數既可以直接傳入:
func(1, 2, 3)
,又可以先組裝list或tuple,再通過*args傳入:func(*(1, 2, 3))
;
- 關鍵字參數既可以直接傳入:
func(a=1, b=2)
,又可以先組裝dict,再通過**kw傳入:func(**{'a': 1, 'b': 2})
。
- 使用
*args
和**kw
是Python的習慣寫法,當然也可以用其他參數名,但最好使用習慣用法。
- 命名的關鍵字參數是為了限制調用者可以傳入的參數名,同時可以提供默認值。
- 定義命名的關鍵字參數在沒有可變參數的情況下不要忘了寫分隔符*,否則定義的將是位置參數。
高級特性
切片
取一個list或tuple的部分元素是非常常見的操作
取list前三個:
>>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']
>>> L[0:3]
['Michael', 'Sarah', 'Tracy']
如果第一個索引是0
,還可以省略,如L[:3]。
支持倒數切片
>>> L = list(range(100))
>>> L
[0, 1, 2, 3, ..., 99]
前10個數,每兩個取一個:
>>> L[:10:2]
[0, 2, 4, 6, 8]
所有數,每5個取一個:
>>> L[::5]
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]
甚至什么都不寫,只寫[:]就可以原樣復制一個list:
>>> L[:]
[0, 1, 2, 3, ..., 99]
** tuple和字符串也可以類似地操作 **
迭代
在Python中,迭代是通過for ... in來完成
只要是可迭代對象,無論有無下標,都可以迭代,比如dict就可以迭代:
>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> for key in d:
... print(key)
...
a
c
b
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實現類似Java那樣的下標循環怎么辦?Python內置的enumerate函數可以把一個list變成索引-元素對,這樣就可以在for循環中同時迭代索引和元素本身:
>>> for i, value in enumerate(['A', 'B', 'C']):
... print(i, value)
...
0 A
1 B
2 C
列表生成式
列表生成式即List Comprehensions,是Python內置的非常簡單卻強大的可以用來創建list的生成式。
舉個例子,要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]可以用list(range(1, 11)):
>>> list(range(1, 11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
如果要生成[1x1, 2x2, 3x3, ..., 10x10]
,可以這樣:
>>> [x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
生成器
創建一個包含100萬個元素的列表,不僅占用很大的存儲空間,如果我們僅僅需要訪問前面幾個元素,那后面絕大多數元素占用的空間都白白浪費了。
一邊循環一邊計算的機制,稱為生成器:generator。
- generator保存的是算法,每次調用next(g),就計算出g的下一個元素的值,直到計算到最后一個元素,沒有更多的元素時,拋出StopIteration的錯誤。
- 創建方法
- 第一種方法很簡單,只要把一個列表生成式的
[]
改成()
,就創建了一個generator:
>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x1022ef630>
- 定義generator的另一種方法。如果一個函數定義中包含
yield
關鍵字,那么這個函數就不再是一個普通函數,而是一個generator:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
- generator和函數的區別
http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014317799226173f45ce40636141b6abc8424e12b5fb27000
最難理解的就是generator和函數的執行流程不一樣。函數是順序執行,遇到return語句或者最后一行函數語句就返回。而變成generator的函數,在每次調用next()的時候執行,遇到yield語句返回,再次執行時從上次返回的yield語句處繼續執行。
最難理解的就是generator和函數的執行流程不一樣。函數是順序執行,遇到return語句或者最后一行函數語句就返回。而變成generator的函數,在每次調用next()的時候執行,遇到yield語句返回,再次執行時從上次返回的yield語句處繼續執行。
迭代器
可以直接作用於for循環的數據類型有以下幾種:
- 一類是集合數據類型,如list、tuple、dict、set、str等;
- 一類是generator,包括生成器和帶yield的generator function。
這些可以直接作用於for循環的對象統稱為可迭代對象:Iterable
。
可以使用isinstance()
判斷一個對象是否是Iterable
對象:
>>> from collections import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True
>>> isinstance([], Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance('abc', Iterator)
False
生成器都是Iterator
對象,但list
、dict
、str
雖然是Iterable
,卻不是Iterator
。
把list
、dict
、str
等Iterable
變成Iterator
可以使用iter()
函數:
>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True
你可能會問,為什么list、dict、str等數據類型不是Iterator?
這是因為Python的Iterator對象表示的是一個數據流,Iterator對象可以被next()函數調用並不斷返回下一個數據,直到沒有數據時拋出StopIteration錯誤。可以把這個數據流看做是一個有序序列,但我們卻不能提前知道序列的長度,只能不斷通過next()函數實現按需計算下一個數據,所以Iterator的計算是惰性的,只有在需要返回下一個數據時它才會計算。
Iterator甚至可以表示一個無限大的數據流,例如全體自然數。而使用list是永遠不可能存儲全體自然數的。
- 小結
- 凡是可作用於for循環的對象都是Iterable類型;
- 凡是可作用於next()函數的對象都是Iterator類型,它們表示一個惰性計算的序列;
- 集合數據類型如list、dict、str等是Iterable但不是Iterator,不過可以通過iter()函數獲得一個Iterator對象。
- Python的for循環本質上就是通過不斷調用next()函數實現的
函數式編程
高階函數
- 變量可以指向函數
>>> f = abs
>>> f(-10)
10
- 函數名也是變量
>>> abs = 10
>>> abs(-10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable
- 傳入函數
** 既然變量可以指向函數,函數的參數能接收變量,那么一個函數就可以接收另一個函數作為參數
,這種函數就稱之為高階函數
。 **
def add(x, y, f):
return f(x) + f(y)
調用
>>> add(-5, 6, abs)
11
返回函數
- 函數作為返回值
高階函數除了可以接受函數作為參數外,還可以把函數作為結果值返回。
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
中又定義了函數sum,並且,內部函數sum
可以引用外部函數lazy_sum
的參數和局部變量,當lazy_sum
返回函數sum時,相關參數和變量都保存在返回的函數中,這種稱為“閉包(Closure)
”的程序結構擁有極大的威力。
** 請再注意一點,當我們調用lazy_sum()時,每次調用都會返回一個新的函數,即使傳入相同的參數:**
>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1==f2
False
- 閉包
注意到返回的函數在其定義內部引用了局部變量args,所以,當一個函數返回了一個函數后,其內部的局部變量還被新函數引用,所以,閉包用起來簡單,實現起來可不容易。
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = count()
>>> f1()
9
>>> f2()
9
>>> f3()
9
- 全部都是9!原因就在於返回的函數引用了變量i,但它並非立刻執行。等到3個函數都返回時,它們所引用的變量i已經變成了3,因此最終結果為9。
返回閉包時牢記的一點就是:返回函數不要引用任何循環變量,或者后續會發生變化的變量。
如果一定要引用循環變量怎么辦?方法是再創建一個函數,用該函數的參數綁定循環變量當前的值,無論該循環變量后續如何更改,已綁定到函數參數的值不變:
def count():
def f(j):
def g():
return j*j
return g
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被執行,因此i的當前值被傳入f()
return fs
再看看結果:
>>> f1, f2, f3 = count()
>>> f1()
1
>>> f2()
4
>>> f3()
9
- 小結
一個函數可以返回一個計算結果,也可以返回一個函數。
返回一個函數時,牢記該函數並未執行,返回函數中不要引用任何可能會變化的變量。
匿名函數
>>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
[1, 4, 9, 16, 25, 36, 49, 64, 81]
匿名函數lambda x: x * x實際上就是:
def f(x):
return x * x
關鍵字lambda
表示匿名函數,冒號前面的x
表示函數參數。
匿名函數有個限制,就是只能有一個表達式,不用寫return
,返回值就是該表達式的結果。
用匿名函數有個好處,因為函數沒有名字,不必擔心函數名沖突。此外,匿名函數也是一個函數對象,也可以把匿名函數賦值給一個變量,再利用變量來調用該函數:
>>> f = lambda x: x * x
>>> f
<function <lambda> at 0x101c6ef28>
>>> f(5)
25
同樣,也可以把匿名函數作為返回值返回,比如:
def build(x, y):
return lambda: x * x + y * y
裝飾器
在函數調用前后自動打印日志,但又不希望修改函數的定義,這種在代碼運行期間動態增加功能的方式,稱之為“裝飾器”(Decorator)。
本質上,decorator就是一個返回函數的高階函數。所以,我們要定義一個能打印日志的decorator,可以定義如下:
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
觀察上面的log,因為它是一個decorator,所以接受一個函數作為參數,並返回一個函數。我們要借助Python的@語法,把decorator置於函數的定義處:
@log
def now():
print('2015-3-25')
調用now()函數,不僅會運行now()函數本身,還會在運行now()函數前打印一行日志:
>>> now()
call now():
2015-3-25
偏函數
functools.partial
就是幫助我們創建一個偏函數的,不需要我們自己定義int2(),可以直接使用下面的代碼創建一個新的函數int2:
>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1010101')
85
所以,簡單總結functools.partial的作用就是,把一個函數的某些參數給固定住(也就是設置默認值),返回一個新的函數,調用這個新函數會更簡單。
- 小結
當函數的參數個數太多,需要簡化時,使用functools.partial可以創建一個新的函數,這個新函數可以固定住原函數的部分參數,從而在調用時更簡單。
模塊
為了避免模塊名沖突,Python又引入了按目錄來組織模塊的方法,稱為包(Package)。
請注意,每一個包目錄下面都會有一個__init__.py
的文件,這個文件是必須存在的,否則,Python就把這個目錄當成普通目錄,而不是一個包。__init__.py
可以是空文件,也可以有Python代碼,因為__init__.py
本身就是一個模塊,而它的模塊名就是目錄名。
使用模塊
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
' a test module '
__author__ = 'Michael Liao'
import sys
def test():
args = sys.argv
if len(args)==1:
print('Hello, world!')
elif len(args)==2:
print('Hello, %s!' % args[1])
else:
print('Too many arguments!')
if __name__=='__main__':
test()
第1行和第2行是標准注釋,第1行注釋可以讓這個hello.py文件直接在Unix/Linux/Mac上運行,第2行注釋表示.py文件本身使用標准UTF-8編碼;
第4行是一個字符串,表示模塊的文檔注釋,任何模塊代碼的第一個字符串都被視為模塊的文檔注釋;
第6行使用__author__變量把作者寫進去,這樣當你公開源代碼后別人就可以瞻仰你的大名;
最后,注意到這兩行代碼:
if __name__=='__main__':
test()
當我們在命令行運行hello模塊文件時,Python解釋器把一個特殊變量__name__
置為__main__
,而如果在其他地方導入該hello模塊時,if判斷將失敗,因此,這種if測試可以讓一個模塊通過命令行運行時執行一些額外的代碼,最常見的就是運行測試。
- 作用域
在一個模塊中,我們可能會定義很多函數和變量,但有的函數和變量我們希望給別人使用,有的函數和變量我們希望僅僅在模塊內部使用。在Python中,是通過_
前綴來實現的。
正常的函數和變量名是公開的(public),可以被直接引用,比如:abc,x123,PI等;
類似__xxx__
這樣的變量是特殊變量,可以被直接引用,但是有特殊用途,比如上面的__author__
,__name__
就是特殊變量,hello模塊定義的文檔注釋也可以用特殊變量__doc__
訪問,我們自己的變量一般不要用這種變量名;
類似_xxx
和__xxx
這樣的函數或變量就是非公開的(private),不應該被直接引用,比如_abc
,__abc
等;
之所以我們說,private函數和變量“不應該”被直接引用,而不是“不能”被直接引用,是因為Python並沒有一種方法可以完全限制訪問private函數或變量,但是,從編程習慣上不應該引用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官方的pypi.python.org 網站注冊,要安裝一個第三方庫,必須先知道該庫的名稱,可以在官網或者pypi上搜索,比如Pillow的名稱叫Pillow,因此,安裝Pillow的命令就是:
pip install Pillow
面向對象編程
數據封裝、繼承和多態是面向對象的三大特點
類和實例
面向對象最重要的概念就是類(Class)和實例(Instance),必須牢記類是抽象的模板,比如Student類,而實例是根據類創建出來的一個個具體的“對象”,每個對象都擁有相同的方法,但各自的數據可能不同。
在創建實例的時候,把一些我們認為必須綁定的屬性強制填寫進去。通過定義一個特殊的__init__方法,在創建實例的時候,就把name,score等屬性綁上去:
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
注意到__init__
方法的第一個參數永遠是self,表示創建的實例本身,因此,在__init__
方法內部,就可以把各種屬性綁定到self,因為self就指向創建的實例本身。
有了__init__
方法,在創建實例的時候,就不能傳入空的參數了,必須傳入與__init__
方法匹配的參數,但self不需要傳,Python解釋器自己會把實例變量傳進去:
>>> bart = Student('Bart Simpson', 59)
>>> bart.name
'Bart Simpson'
>>> bart.score
59
和普通的函數相比,在類中定義的函數只有一點不同,就是第一個參數永遠是實例變量self
,並且,調用時,不用傳遞該參數。除此之外,類的方法和普通函數沒有什么區別,所以,你仍然可以用默認參數、可變參數、關鍵字參數和命名關鍵字參數。
- 小結
類是創建實例的模板,而實例則是一個一個具體的對象,各個實例擁有的數據都互相獨立,互不影響;
方法就是與實例綁定的函數,和普通函數不同,方法可以直接訪問實例的數據;
通過在實例上調用方法,我們就直接操作了對象內部的數據,但無需知道方法內部的實現細節。
和靜態語言不同,Python允許對實例變量綁定任何數據,也就是說,對於兩個實例變量,雖然它們都是同一個類的不同實例,但擁有的變量名稱都可能不同:
>>> bart = Student('Bart Simpson', 59)
>>> lisa = Student('Lisa Simpson', 87)
>>> bart.age = 8
>>> bart.age
8
>>> lisa.age
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'age'
訪問限制
在Class內部,可以有屬性和方法,而外部代碼可以通過直接調用實例變量的方法來操作數據,這樣,就隱藏了內部的復雜邏輯。
如果要讓內部屬性不被外部訪問,可以把屬性的名稱前加上兩個下划線__
,在Python中,實例的變量名如果以__
開頭,就變成了一個私有變量(private),只有內部可以訪問,外部不能訪問,所以,我們把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))
改完后,對於外部代碼來說,沒什么變動,但是已經無法從外部訪問實例變量.__name
和實例變量.__score
了:
>>> bart = Student('Bart Simpson', 98)
>>> bart.__name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'
需要注意的是,在Python中,變量名類似`__xxx__`的,也就是以雙下划線開頭,並且以雙下划線結尾的,是特殊變量,特殊變量是可以直接訪問的,不是private變量,所以,不能用`__name__`、`__score__`這樣的變量名。
雙下划線開頭的實例變量是不是一定不能從外部訪問呢?其實也不是。不能直接訪問__name是因為Python解釋器對外把__name變量改成了_Student__name,所以,仍然可以通過_Student__name來訪問__name變量:
>>> bart._Student__name
'Bart Simpson'
但是強烈建議你不要這么干,因為不同版本的Python解釋器可能會把__name改成不同的變量名。
總的來說就是,Python本身沒有任何機制阻止你干壞事,一切全靠自覺。
最后注意下面的這種錯誤寫法:
>>> bart = Student('Bart Simpson', 98)
>>> bart.get_name()
'Bart Simpson'
>>> bart.__name = 'New Name' # 設置__name變量!
>>> bart.__name
'New Name'
表面上看,外部代碼“成功”地設置了__name
變量,但實際上這個__name
變量和class內部的__name
變量不是一個變量!內部的__name
變量已經被Python解釋器自動改成了_Student__name
,而外部代碼給bart新增了一個__name
變量。不信試試:
>>> bart.get_name() # get_name()內部返回self.__name
'Bart Simpson'
繼承和多態
- 繼承和多態
在OOP程序設計中,當我們定義一個class的時候,可以從某個現有的class繼承,新的class稱為子類(Subclass),而被繼承的class稱為基類、父類或超類(Base class、Super class)。
當子類和父類都存在相同的run()方法時,我們說,子類的run()覆蓋了父類的run(),在代碼運行的時候,總是會調用子類的run()。這樣,我們就獲得了繼承的另一個好處:多態
。
多態的好處就是,當我們需要傳入Dog、Cat、Tortoise……時,我們只需要接收Animal類型就可以了,因為Dog、Cat、Tortoise……都是Animal類型,然后,按照Animal類型進行操作即可。由於Animal類型有run()方法,因此,傳入的任意類型,只要是Animal類或者子類,就會自動調用實際類型的run()方法,這就是多態的意思:
對於一個變量,我們只需要知道它是Animal類型,無需確切地知道它的子類型,就可以放心地調用run()方法,而具體調用的run()方法是作用在Animal、Dog、Cat還是Tortoise對象上,由運行時該對象的確切類型決定,這就是多態真正的威力:調用方只管調用,不管細節,而當我們新增一種Animal的子類時,只要確保run()方法編寫正確,不用管原來的代碼是如何調用的。這就是著名的“開閉”原則:
- 對擴展開放:允許新增Animal子類;
- 對修改封閉:不需要修改依賴Animal類型的run_twice()等函數。
繼承還可以一級一級地繼承下來,就好比從爺爺到爸爸、再到兒子這樣的關系。而任何類,最終都可以追溯到根類object,這些繼承關系看上去就像一顆倒着的樹。
- 靜態語言 vs 動態語言
- 對於靜態語言(例如Java)來說,如果需要傳入Animal類型,則傳入的對象必須是Animal類型或者它的子類,否則,將無法調用run()方法。
- 對於Python這樣的動態語言來說,則不一定需要傳入Animal類型。我們只需要保證傳入的對象有一個run()方法就可以了:
class Timer(object):
def run(self):
print('Start...')
這就是動態語言的“鴨子類型
”,它並不要求嚴格的繼承體系,一個對象只要“看起來像鴨子,走起路來像鴨子”,那它就可以被看做是鴨子。
Python的“file-like object“就是一種鴨子類型。對真正的文件對象,它有一個read()方法,返回其內容。但是,許多對象,只要有read()方法,都被視為“file-like object“。許多函數接收的參數就是“file-like object“,你不一定要傳入真正的文件對象,完全可以傳入任何實現了read()方法的對象。
- 小結
繼承可以把父類的所有功能都直接拿過來,這樣就不必重零做起,子類只需要新增自己特有的方法,也可以把父類不適合的方法覆蓋重寫。
動態語言的鴨子類型特點決定了繼承不像靜態語言那樣是必須的。
獲取對象信息
當我們拿到一個對象的引用時,如何知道這個對象是什么類型、有哪些方法呢?
- 使用type()
>>> type(123)
<class 'int'>
>>> type('str')
<class 'str'>
>>> type(None)
<type(None) 'NoneType'>
>>> type(123)==type(456)
True
>>> type(123)==int
True
>>> type('abc')==type('123')
True
>>> type('abc')==str
True
>>> type('abc')==type(123)
False
-
使用isinstance()
對於class的繼承關系來說,使用type()就很不方便。我們要判斷class的類型,可以使用isinstance()函數。 -
使用dir()
如果要獲得一個對象的所有屬性和方法,可以使用dir()函數,它返回一個包含字符串的list,比如,獲得一個str對象的所有屬性和方法:
>>> dir('ABC')
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
僅僅把屬性和方法列出來是不夠的,配合getattr()
、setattr()
以及hasattr()
,我們可以直接操作一個對象的狀態:
>>> hasattr(obj, 'x') # 有屬性'x'嗎?
True
>>> obj.x
9
>>> hasattr(obj, 'y') # 有屬性'y'嗎?
False
>>> setattr(obj, 'y', 19) # 設置一個屬性'y'
>>> hasattr(obj, 'y') # 有屬性'y'嗎?
True
>>> getattr(obj, 'y') # 獲取屬性'y'
19
>>> obj.y # 獲取屬性'y'
19
實例屬性和類屬性
由於Python是動態語言,根據類創建的實例可以任意綁定屬性
。
當我們定義了一個類屬性后,這個屬性雖然歸類所有,但類的所有實例都可以訪問到。來測試一下:
>>> class Student(object):
... name = 'Student'
...
>>> s = Student() # 創建實例s
>>> print(s.name) # 打印name屬性,因為實例並沒有name屬性,所以會繼續查找class的name屬性
Student
>>> print(Student.name) # 打印類的name屬性
Student
>>> s.name = 'Michael' # 給實例綁定name屬性
>>> print(s.name) # 由於實例屬性優先級比類屬性高,因此,它會屏蔽掉類的name屬性
Michael
>>> print(Student.name) # 但是類屬性並未消失,用Student.name仍然可以訪問
Student
>>> del s.name # 如果刪除實例的name屬性
>>> print(s.name) # 再次調用s.name,由於實例的name屬性沒有找到,類的name屬性就顯示出來了
Student
面向對象高級編程
數據封裝、繼承和多態只是面向對象程序設計中最基礎的3個概念。接下來我們會討論多重繼承、定制類、元類等概念。
使用__slots__
正常情況下,當我們定義了一個class,創建了一個class的實例后,我們可以給該實例綁定任何屬性和方法,這就是動態語言的靈活性。
先定義class:
class Student(object):
pass
- 給實例綁定一個屬性和方法:
>>> s = Student()
>>> s.name = 'Michael' # 動態給實例綁定一個屬性
>>> print(s.name)
Michael
>>> def set_age(self, age): # 定義一個函數作為實例方法
... self.age = age
...
>>> from types import MethodType
>>> s.set_age = MethodType(set_age, s) # 給實例綁定一個方法
>>> s.set_age(25) # 調用實例方法
>>> s.age # 測試結果
25
** 給一個實例綁定的方法,對另一個實例是不起作用的 **
>>> s2 = Student() # 創建新的實例
>>> s2.set_age(25) # 嘗試調用方法
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'set_age'
- ** 為了給所有實例都綁定方法,可以給class綁定方法:**
>>> def set_score(self, score):
... self.score = score
...
>>> Student.set_score = set_score
>>> s.set_score(100)
>>> s.score
100
>>> s2.set_score(99)
>>> s2.score
99
- 通常情況下,上面的set_score方法可以直接定義在class中,但動態綁定允許我們在程序運行的過程中動態給class加上功能,這在靜態語言中很難實現。
- 使用__slots__
如果我們想要限制實例的屬性怎么辦?
為了達到限制的目的,Python允許在定義class的時候,定義一個特殊的__slots__
變量,來限制該class實例能添加的屬性:
class Student(object):
__slots__ = ('name', 'age') # 用tuple定義允許綁定的屬性名稱
>>> s = Student() # 創建新的實例
>>> s.name = 'Michael' # 綁定屬性'name'
>>> s.age = 25 # 綁定屬性'age'
>>> s.score = 99 # 綁定屬性'score'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'
- 注意:
使用__slots__要注意,__slots__
定義的屬性僅對當前類實例起作用,** 對繼承的子類是不起作用 ** 的:
>>> class GraduateStudent(Student):
... pass
...
>>> g = GraduateStudent()
>>> g.score = 9999
除非在子類中也定義__slots__
,這樣,子類實例允許定義的屬性就是** 自身 ** 的__slots__
加上** 父類 ** 的__slots__
。
使用@property
在綁定屬性時,如果我們直接把屬性暴露出去,雖然寫起來很簡單,但是,沒辦法檢查參數,導致可以把成績隨便改:
s = Student()
s.score = 9999
為了限制score的范圍,通常情況下可以通過一個set_score()方法來設置成績,再通過一個get_score()來獲取成績。但這略顯復雜,沒有直接用屬性這么直接簡單。
裝飾器(decorator)可以給函數動態加上功能嗎?對於類的方法,裝飾器一樣起作用。Python內置的@property裝飾器就是負責把一個方法變成屬性調用的:
class Student(object):
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value
只需要加上@property
就可以了,此時,@property
本身又創建了另一個裝飾器@屬性名.setter
,負責把一個setter方法變成屬性賦值。
>>> s = Student()
>>> s.score = 60 # OK,實際轉化為s.set_score(60)
>>> s.score # OK,實際轉化為s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
...
ValueError: score must between 0 ~ 100!
** 還可以定義只讀屬性,只定義getter方法,不定義setter方法就是一個只讀屬性 **
多重繼承
繼承是面向對象編程的一個重要的方式,因為通過繼承,子類就可以擴展父類的功能。
通過多重繼承,一個子類就可以同時獲得多個父類的所有功能。
-
MixIn
MixIn的目的就是給一個類增加多個功能,這樣,在設計類的時候,我們優先考慮通過多重繼承來組合多個MixIn的功能,而不是設計多層次的復雜的繼承關系。 -
小結
** 由於Python允許使用多重繼承
,因此,MixIn就是一種常見的設計。 **
** 只允許單一繼承的語言(如Java)不能使用MixIn的設計。 **
定制類
- str()
返回用戶
看到的字符串
使得print(實例名),以更友好的方式輸出。
- repr()
返回程序開發者
看到的字符串
__repr__()
是為調試服務的
解決辦法是再定義一個__repr__()。但是通常__str__()和__repr__()代碼都是一樣的,所以,有個偷懶的寫法:
class Student(object):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Student object (name=%s)' % self.name
__repr__ = __str__
- iter()
如果一個類想被用於for ... in循環,類似list或tuple那樣,就必須實現一個__iter__()
方法,該方法返回一個迭代對象,然后,Python的for循環就會不斷調用該迭代對象的__next__()
方法拿到循環的下一個值,直到遇到StopIteration錯誤時退出循環。
我們以斐波那契數列為例,寫一個Fib類,可以作用於for循環:
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1 # 初始化兩個計數器a,b
def __iter__(self):
return self # 實例本身就是迭代對象,故返回自己
def __next__(self):
self.a, self.b = self.b, self.a + self.b # 計算下一個值
if self.a > 100000: # 退出循環的條件
raise StopIteration();
return self.a # 返回下一個值
現在,試試把Fib實例作用於for循環:
>>> for n in Fib():
... print(n)
...
1
1
2
3
5
...
46368
75025
- getitem()
像list那樣按照下標取出元素,需要實現__getitem__()
方法:
class Fib(object):
def __getitem__(self, n):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
調用:
>>> f = Fib()
>>> f[0]
1
>>> f[1]
1
>>> f[2]
2
>>> f[3]
3
>>> f[10]
89
>>> f[100]
573147844013817084101
如果想是想list神奇的切片方法需要對__getitem__()
方法進行改造
__getitem__()
傳入的參數可能是一個int
,也可能是一個切片對象slice
class Fib(object):
def __getitem__(self, n):
if isinstance(n, int): # n是索引
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
if isinstance(n, slice): # n是切片
start = n.start
stop = n.stop
if start is None:
start = 0
a, b = 1, 1
L = []
for x in range(stop):
if x >= start:
L.append(a)
a, b = b, a + b
return L
現在試試Fib的切片:
>>> f = Fib()
>>> f[0:5]
[1, 1, 2, 3, 5]
>>> f[:10]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
但是沒有對step參數作處理:
>>> f[:10:2]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
也沒有對負數作處理,所以,要正確實現一個__getitem__()
** 還是有很多工作要做的。**
此外,如果把對象看成dict,__getitem__()
的參數也可能是一個可以作key的object,例如str。
與之對應的是__setitem__()
方法,把對象視作list或dict來對集合賦值。最后,還有一個__delitem__()方
法,用於刪除某個元素。
總之,通過上面的方法,我們自己定義的類表現得和Python自帶的list、tuple、dict沒什么區別,這完全歸功於動態語言的“鴨子類型”,不需要強制繼承某個接口。
- getattr()
Python還有另一個機制,那就是寫一個__getattr__()
方法,動態返回一個屬性
class Student(object):
def __init__(self):
self.name = 'Michael'
def __getattr__(self, attr):
if attr=='score':
return 99
- 注意,只有在沒有找到屬性的情況下,才調用__getattr__,已有的屬性,比如name,不會在__getattr__中查找。
- call()
一個對象實例可以有自己的屬性和方法,當我們調用實例方法時,我們用instance.method()來調用。能不能直接在實例本身上調用呢?在Python中,答案是肯定的。
任何類,只需要定義一個__call__()方法,就可以直接對實例進行調用。請看示例:
class Student(object):
def __init__(self, name):
self.name = name
def __call__(self):
print('My name is %s.' % self.name)
調用方式如下:
>>> s = Student('Michael')
>>> s() # self參數不要傳入
My name is Michael.
__call__()
還可以定義參數。對實例進行直接調用就好比對一個函數進行調用一樣,所以你完全可以把對象看成函數,把函數看成對象,因為這兩者之間本來就沒啥根本的區別。
如果你把對象看成函數,那么函數本身其實也可以在運行期動態創建出來,因為類的實例都是運行期創建出來的,這么一來,我們就模糊了對象和函數的界限。
那么,怎么判斷一個變量是對象還是函數呢?其實,更多的時候,我們需要判斷一個對象是否能被調用,能被調用的對象就是一個Callable對象,比如函數和我們上面定義的帶有__call__()的類實例:
>>> callable(Student())
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable(None)
False
>>> callable('str')
False
通過callable()函數,我們就可以判斷一個對象是否是“可調用”對象。
使用枚舉類
當我們需要定義常量時,一個辦法是用大寫變量通過整數來定義,例如月份:
JAN = 1
FEB = 2
MAR = 3
...
NOV = 11
DEC = 12
好處是簡單,缺點是類型是int,並且仍然是變量。
更好的方法是為這樣的枚舉類型定義一個class類型,然后,每個常量都是class的一個唯一實例。Python提供了Enum類來實現這個功能:
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派生出自定義類:
from enum import Enum, unique
@unique
class Weekday(Enum):
Sun = 0 # Sun的value被設定為0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6
@unique
裝飾器可以幫助我們檢查保證沒有重復值。
- 小結
Enum可以把一組相關常量定義在一個class中,且class不可變,而且成員可以直接比較。
使用元類
- type()
動態語言和靜態語言最大的不同,就是函數和類的定義,不是編譯時定義的,而是運行時動態創建的。
比方說我們要定義一個Hello的class,就寫一個hello.py模塊:
class Hello(object):
def hello(self, name='world'):
print('Hello, %s.' % name)
當Python解釋器載入hello模塊時,就會依次執行該模塊的所有語句,執行結果就是動態創建出一個Hello的class對象,測試如下:
>>> from hello import Hello
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class 'hello.Hello'>
type()函數可以查看一個類型或變量的類型,Hello是一個class,它的類型就是type,而h是一個實例,它的類型就是class Hello。
我們說class的定義是運行時動態創建的,而創建class的方法就是使用type()函數。
type()函數既可以返回一個對象的類型,又可以創建出新的類型,比如,我們可以通過type()函數創建出Hello類,而無需通過class Hello(object)...的定義:
>>> def fn(self, name='world'): # 先定義函數
... print('Hello, %s.' % name)
...
>>> Hello = type('Hello', (object,), dict(hello=fn)) # 創建Hello class
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class '__main__.Hello'>
要創建一個class對象,type()函數依次傳入3個參數:
class的名稱;
繼承的父類集合,注意Python支持多重繼承,如果只有一個父類,別忘了tuple的單元素寫法;
class的方法名稱與函數綁定,這里我們把函數fn綁定到方法名hello上。
通過type()函數創建的類和直接寫class是完全一樣的,因為Python解釋器遇到class定義時,僅僅是掃描一下class定義的語法,然后調用type()函數創建出class。
正常情況下,我們都用class Xxx...來定義類,但是,type()函數也允許我們動態創建出類來,也就是說,動態語言本身支持運行期動態創建類
,** 這和靜態語言有非常大的不同 ** ,要在靜態語言運行期創建類,必須構造源代碼字符串再調用編譯器,或者借助一些工具生成字節碼實現,本質上都是動態編譯,會非常復雜。
- metaclass
除了使用type()動態創建類以外,要控制類的創建行為,還可以使用metaclass。
metaclass,直譯為元類,簡單的解釋就是:
當我們定義了類以后,就可以根據這個類創建出實例,所以:先定義類,然后創建實例。
但是如果我們想創建出類呢?那就必須根據metaclass創建出類,所以:先定義metaclass,然后創建類。
連接起來就是:先定義metaclass,就可以創建類,最后創建實例。
所以,metaclass允許你創建類或者修改類。換句話說,你可以把類看成是metaclass創建出來的“實例”。
metaclass是Python面向對象里最難理解,也是最難使用的魔術代碼。正常情況下,你不會碰到需要使用metaclass的情況,所以,以下內容看不懂也沒關系,因為基本上你不會用到。
錯誤、調試和測試
錯誤處理
try...except...finally...
try
try:
print('try...')
r = 10 / 0
print('result:', r)
except ZeroDivisionError as e:
print('except:', e)
finally:
print('finally...')
print('END')
finally
如果有,則一定會被執行(可以沒有finally語句)
不需要在每個可能出錯的地方去捕獲錯誤,只要在合適的層次去捕獲錯誤就可以了。這樣一來,就大大減少了寫try...except...finally的麻煩。
- 調用堆棧
如果錯誤沒有被捕獲,它就會一直往上拋,最后被Python解釋器捕獲,打印一個錯誤信息,然后程序退出。
- 記錄錯誤
如果不捕獲錯誤,自然可以讓Python解釋器來打印出錯誤堆棧,但程序也被結束了。既然我們能捕獲錯誤,就可以把錯誤堆棧打印出來,然后分析錯誤原因,同時,讓程序繼續執行下去。
Python內置的logging模塊可以非常容易地記錄錯誤信息:
# err_logging.py
import logging
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar('0')
except Exception as e:
logging.exception(e)
main()
print('END')
通過配置,logging還可以把錯誤記錄到日志文件里,方便事后排查。
- 拋出錯誤 (
raise
語句)
如果要拋出錯誤,首先根據需要,可以定義一個錯誤的class,選擇好繼承關系,然后,用raise語句拋出一個錯誤的實例:
# err_raise.py
class FooError(ValueError):
pass
def foo(s):
n = int(s)
if n==0:
raise FooError('invalid value: %s' % s)
return 10 / n
foo('0')
執行,可以最后跟蹤到我們自己定義的錯誤:
$ python3 err_raise.py
Traceback (most recent call last):
File "err_throw.py", line 11, in <module>
foo('0')
File "err_throw.py", line 8, in foo
raise FooError('invalid value: %s' % s)
__main__.FooError: invalid value: 0
- 小結
Python內置的try...except...finally
用來處理錯誤十分方便。出錯時,會分析錯誤信息並定位錯誤發生的代碼位置才是最關鍵的。
程序也可以主動拋出錯誤,讓調用者來處理相應的錯誤。但是,應該在文檔中寫清楚可能會拋出哪些錯誤,以及錯誤產生的原因。
調試
- 斷言assert
- logging
logging允許你指定記錄信息的級別,有debug
,info
,warning
,error
等幾個級別,當我們指定level=INFO時,logging.debug就不起作用了。同理,指定level=WARNING后,debug和info就不起作用了。這樣一來,你可以放心地輸出不同級別的信息,也不用刪除,最后統一控制輸出哪個級別的信息。
logging的另一個好處是通過簡單的配置,一條語句可以同時輸出到不同的地方,比如console和文件。
-
pdb
第4種方式是啟動Python的調試器pdb,讓程序以單步方式運行 -
pdb.set_trace()
這個方法也是用pdb,但是不需要單步執行,我們只需要import pdb,然后,在可能出錯的地方放一個pdb.set_trace(),就可以設置一個斷點:
# err.py
import pdb
s = '0'
n = int(s)
pdb.set_trace() # 運行到這里會自動暫停
print(10 / n)
單元測試
文檔測試
自動執行寫在注釋中的這些代碼。Python內置的“文檔測試”(doctest)模塊可以直接提取注釋中的代碼並執行測試。
讓我們用doctest來測試上次編寫的Dict類:
# mydict2.py
class Dict(dict):
'''
Simple dict but also support access as x.y style.
>>> d1 = Dict()
>>> d1['x'] = 100
>>> d1.x
100
>>> d1.y = 200
>>> d1['y']
200
>>> d2 = Dict(a=1, b=2, c='3')
>>> d2.c
'3'
>>> d2['empty']
Traceback (most recent call last):
...
KeyError: 'empty'
>>> d2.empty
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'empty'
'''
def __init__(self, **kw):
super(Dict, self).__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
if __name__=='__main__':
import doctest
doctest.testmod()
運行python3 mydict2.py:
$ python3 mydict2.py
IO 編程
文件讀寫
- 讀文件
try:
f = open('/path/to/file', 'r')
print(f.read())
finally:
if f:
f.close()
但是每次都這么寫實在太繁瑣,所以,Python引入了with
語句來自動幫我們調用close()
方法:
with open('/path/to/file', 'r') as f:
print(f.read())
調用read()會一次性讀取文件的全部內容,如果文件有10G,內存就爆了,所以,要保險起見,可以反復調用read(size)方法,每次最多讀取size個字節的內容。另外,調用readline()可以每次讀取一行內容,調用readlines()一次讀取所有內容並按行返回list。因此,要根據需要決定怎么調用。
如果文件很小,read()一次性讀取最方便;如果不能確定文件大小,反復調用read(size)比較保險;如果是配置文件,調用readlines()最方便:
for line in f.readlines():
print(line.strip()) # 把末尾的'\n'刪掉
- file-like Object
像open()函數返回的這種有個read()方法的對象,在Python中統稱為file-like Object。除了file外,還可以是內存的字節流,網絡流,自定義流等等。file-like Object不要求從特定類繼承,只要寫個read()方法就行。
- 二進制文件
前面講的默認都是讀取文本文件,並且是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()
你可以反復調用write()來寫入文件,但是務必要調用f.close()來關閉文件。當我們寫文件時,操作系統往往不會立刻把數據寫入磁盤,而是放到內存緩存起來,空閑的時候再慢慢寫入。只有調用close()方法時,操作系統才保證把沒有寫入的數據全部寫入磁盤。忘記調用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())
hello world!
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 StringIO
>>> f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')
>>> f.read()
b'\xe4\xb8\xad\xe6\x96\x87'
操作文件和目錄
Python內置的os模塊也可以直接調用操作系統提供的接口函數。
- 環境變量
>>> os.environ
environ({'VERSIONER_PYTHON_PREFER_32_BIT': 'no', 'TERM_PROGRAM_VERSION': '326', 'LOGNAME': 'michael', 'USER': 'michael', 'PATH': '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/X11/bin:/usr/local/mysql/bin', ...})
>>> os.environ.get('PATH')
'/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/X11/bin:/usr/local/mysql/bin'
>>> os.environ.get('x', 'default')
'default'
- 操作文件和目錄
# 查看當前目錄的絕對路徑:
>>> os.path.abspath('.')
'/Users/michael'
# 在某個目錄下創建一個新目錄,首先把新目錄的完整路徑表示出來:
>>> os.path.join('/Users/michael', 'testdir')
'/Users/michael/testdir'
# 然后創建一個目錄:
>>> os.mkdir('/Users/michael/testdir')
# 刪掉一個目錄:
>>> os.rmdir('/Users/michael/testdir')
- 小結
Python的os模塊封裝了操作系統的目錄和文件操作,要注意這些函數有的在os模塊中,有的在os.path模塊中。
序列化
我們把變量從內存中變成可存儲或傳輸的過程稱之為序列化,在Python中叫pickling
,在其他語言中也被稱之為serialization,marshalling,flattening等等,都是一個意思。
序列化之后,就可以把序列化后的內容寫入磁盤,或者通過網絡傳輸到別的機器上。
反過來,把變量內容從序列化的對象重新讀到內存里稱之為反序列化,即unpickling
。
Python提供了pickle
模塊來實現序列化。
pickle.dumps()方法把任意對象序列化成一個bytes,然后,就可以把這個bytes寫入文件。或者用另一個方法pickle.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中直接反序列化出對象。我們打開另一個Python命令行來反序列化剛才保存的對象:
>>> f = open('dump.txt', 'rb')
>>> d = pickle.load(f)
>>> f.close()
>>> d
{'age': 20, 'score': 88, 'name': 'Bob'}
- Pickle的問題和所有其他編程語言特有的序列化問題一樣,就是它只能用於Python,並且可能不同版本的Python彼此都不兼容,因此,只能用Pickle保存那些不重要的數據,不能成功地反序列化也沒關系。
- 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。
- 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__))
進程和線程
略
正則表達式
- 基礎
在正則表達式中,如果直接給出字符,就是精確匹配。用\d可以匹配一個數字,\w可以匹配一個字母或數字,所以:
'00\d'
可以匹配'007'
,但無法匹配'00A'
;
'\d\d\d'
可以匹配'010'
;
'\w\w\d'
可以匹配'py3'
;
.
可以匹配任意字符,所以:
'py.'
可以匹配'pyc'
、'pyo'
、'py!'
等等。
要匹配變長的字符,在正則表達式中,用*
表示任意個字符(包括0個),用+
表示至少一個字符,用?
表示0個或1個字符,用{n}
表示n個字符,用{n,m}
表示n-m個字符
- 進階
要做更精確地匹配,可以用[]表示范圍,比如:
[0-9a-zA-Z\_]
可以匹配一個數字、字母或者下划線;
[0-9a-zA-Z\_]+
可以匹配至少由一個數字、字母或者下划線組成的字符串,比如'a100','0_Z','Py3000'等等;
[a-zA-Z\_][0-9a-zA-Z\_]*
可以匹配由字母或下划線開頭,后接任意個由一個數字、字母或者下划線組成的字符串,也就是Python合法的變量;
[a-zA-Z\_][0-9a-zA-Z\_]{0, 19}
更精確地限制了變量的長度是1-20個字符(前面1個字符+后面最多19個字符)。
A|B
可以匹配A或B,所以(P|p)ython可以匹配'Python'或者'python'。
^
表示行的開頭,^\d
表示必須以數字開頭。
$
表示行的結束,\d$
表示必須以數字結束。
你可能注意到了,py也可以匹配'python',但是加上^py$
就變成了整行匹配,就只能匹配'py'了。
- re模塊
有了准備知識,我們就可以在Python中使用正則表達式了。Python提供re模塊,包含所有正則表達式的功能。由於Python的字符串本身也用\
轉義,所以要特別注意:
match()
方法判斷是否匹配,如果匹配成功,返回一個Match對象
,否則返回None
。常見的判斷方法就是:
test = '用戶輸入的字符串'
if re.match(r'正則表達式', test):
print('ok')
else:
print('failed')
- 切分字符串
用正則表達式切分字符串比用固定的字符更靈活,請看正常的切分代碼:
>>> 'a b c'.split(' ')
['a', 'b', '', '', 'c']
嗯,無法識別連續的空格,用正則表達式試試:
>>> re.split(r'\s+', 'a b c')
['a', 'b', 'c']
無論多少個空格都可以正常分割。加入,試試:
>>> re.split(r'[\s\,]+', 'a,b, c d')
['a', 'b', 'c', 'd']
再加入;試試:
>>> re.split(r'[\s\,\;]+', 'a,b;; c d')
['a', 'b', 'c', 'd']
如果用戶輸入了一組標簽,下次記得用正則表達式來把不規范的輸入轉化成正確的數組。
- 分組
除了簡單地判斷是否匹配之外,正則表達式還有提取子串的強大功能。用()
表示的就是要提取的分組(Group)。比如:
^(\d{3})-(\d{3,8})$
分別定義了兩個組,可以直接從匹配的字符串中提取出區號和本地號碼:
>>> m = re.match(r'^(\d{3})-(\d{3,8})$', '010-12345')
>>> m
<_sre.SRE_Match object; span=(0, 9), match='010-12345'>
>>> m.group(0)
'010-12345'
>>> m.group(1)
'010'
>>> m.group(2)
'12345'
** 注意到group(0)永遠是原始字符串,group(1)、group(2)……表示第1、2、……個子串。 **
- 貪婪匹配
最后需要特別指出的是,正則匹配默認是貪婪匹配
,也就是匹配盡可能多的字符。舉例如下,匹配出數字后面的0:
>>> re.match(r'^(\d+)(0*)$', '102300').groups()
('102300', '')
由於\d+
采用貪婪匹配,直接把后面的0
全部匹配了,結果0*
只能匹配空字符串了。
必須讓\d+
采用非貪婪匹配(也就是盡可能少匹配),才能把后面的0匹配出來,加個?就可以讓\d+采用非貪婪匹配:
>>> re.match(r'^(\d+?)(0*)$', '102300').groups()
('1023', '00')
- 編譯
當我們在Python中使用正則表達式時,re模塊內部會干兩件事情:
編譯正則表達式,如果正則表達式的字符串本身不合法,會報錯;
用編譯后的正則表達式去匹配字符串。
如果一個正則表達式要重復使用幾千次,出於效率的考慮,我們可以預編譯該正則表達式,接下來重復使用時就不需要編譯這個步驟了,直接匹配:
>>> import re
# 編譯:
>>> re_telephone = re.compile(r'^(\d{3})-(\d{3,8})$')
# 使用:
>>> re_telephone.match('010-12345').groups()
('010', '12345')
>>> re_telephone.match('010-8086').groups()
('010', '8086')
編譯后生成Regular Expression對象,由於該對象自己包含了正則表達式,所以調用對應的方法時不用給出正則字符串。
常用內建模塊
常用第三方模塊
PIL
PIL:Python Imaging Library,已經是Python平台事實上的圖像處理標准庫了。PIL功能非常強大,但API卻非常簡單易用。
安裝Pillow
在命令行下直接通過pip安裝:
$ pip install pillow
如果遇到Permission denied安裝失敗,請加上sudo重試。
操作圖像
來看看最常見的圖像縮放操作,只需三四行代碼:
from PIL import Image
# 打開一個jpg圖像文件,注意是當前路徑:
im = Image.open('test.jpg')
# 獲得圖像尺寸:
w, h = im.size
print('Original image size: %sx%s' % (w, h))
# 縮放到50%:
im.thumbnail((w//2, h//2))
print('Resize image to: %sx%s' % (w//2, h//2))
# 把縮放后的圖像用jpeg格式保存:
im.save('thumbnail.jpg', 'jpeg')
其他功能如切片、旋轉、濾鏡、輸出文字、調色板等一應俱全。
比如,模糊效果也只需幾行代碼:
from PIL import Image, ImageFilter
# 打開一個jpg圖像文件,注意是當前路徑:
im = Image.open('test.jpg')
# 應用模糊濾鏡:
im2 = im.filter(ImageFilter.BLUR)
im2.save('blur.jpg', 'jpeg')
效果如下:
PIL-blur
PIL的ImageDraw提供了一系列繪圖方法,讓我們可以直接繪圖。比如要生成字母驗證碼圖片:
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import random
# 隨機字母:
def rndChar():
return chr(random.randint(65, 90))
# 隨機顏色1:
def rndColor():
return (random.randint(64, 255), random.randint(64, 255), random.randint(64, 255))
# 隨機顏色2:
def rndColor2():
return (random.randint(32, 127), random.randint(32, 127), random.randint(32, 127))
# 240 x 60:
width = 60 * 4
height = 60
image = Image.new('RGB', (width, height), (255, 255, 255))
# 創建Font對象:
font = ImageFont.truetype('Arial.ttf', 36)
# 創建Draw對象:
draw = ImageDraw.Draw(image)
# 填充每個像素:
for x in range(width):
for y in range(height):
draw.point((x, y), fill=rndColor())
# 輸出文字:
for t in range(4):
draw.text((60 * t + 10, 10), rndChar(), font=font, fill=rndColor2())
# 模糊:
image = image.filter(ImageFilter.BLUR)
image.save('code.jpg', 'jpeg')
如果運行的時候報錯:
IOError: cannot open resource
這是因為PIL無法定位到字體文件的位置,可以根據操作系統提供絕對路徑,比如:
'/Library/Fonts/Arial.ttf'
要詳細了解PIL的強大功能,請請參考Pillow官方文檔:
https://pillow.readthedocs.org/
小結
PIL提供了操作圖像的強大功能,可以通過簡單的代碼完成復雜的圖像處理。
virtualenv
每個應用可能需要各自擁有一套“獨立”的Python運行環境。virtualenv就是用來為一個應用創建一套“隔離”的Python運行環境。
- 首先,我們用pip安裝virtualenv:
$ pip3 install virtualenv
- 第一步,創建目錄
Mac:~ michael$ mkdir myproject
Mac:~ michael$ cd myproject/
Mac:myproject michael$
- 第二步,創建一個獨立的Python運行環境,命名為
venv
,(virtualenv --no-site-packages venv
):
Mac:myproject michael$ virtualenv --no-site-packages venv
Using base prefix '/usr/local/.../Python.framework/Versions/3.4'
New python executable in venv/bin/python3.4
Also creating executable in venv/bin/python
Installing setuptools, pip, wheel...done.
命令virtualenv
就可以創建一個獨立的Python運行環境,我們還加上了參數--no-site-packages
,這樣,已經安裝到系統Python環境中的所有第三方包都不會復制過來,這樣,我們就得到了一個不帶任何第三方包的“干凈”的Python運行環境。
- 進入虛擬環境(
source venv/bin/activate
)
Mac:myproject michael$ source venv/bin/activate
(venv)Mac:myproject michael$
注意到命令提示符變了,有個(venv)前綴,表示當前環境是一個名為venv的Python環境。
** virtualenv為應用提供了隔離的Python運行環境,解決了不同應用間多版本的沖突問題。 **
圖形界面
網絡編程
電子郵件
訪問數據庫
使用SQLite
# 導入SQLite驅動:
>>> import sqlite3
# 連接到SQLite數據庫
# 數據庫文件是test.db
# 如果文件不存在,會自動在當前目錄創建:
>>> conn = sqlite3.connect('test.db')
# 創建一個Cursor:
>>> cursor = conn.cursor()
# 執行一條SQL語句,創建user表:
>>> cursor.execute('create table user (id varchar(20) primary key, name varchar(20))')
<sqlite3.Cursor object at 0x10f8aa260>
# 繼續執行一條SQL語句,插入一條記錄:
>>> cursor.execute('insert into user (id, name) values (\'1\', \'Michael\')')
<sqlite3.Cursor object at 0x10f8aa260>
# 通過rowcount獲得插入的行數:
>>> cursor.rowcount
1
# 關閉Cursor:
>>> cursor.close()
# 提交事務:
>>> conn.commit()
# 關閉Connection:
>>> conn.close()
使用MySQL
MySQL的配置文件默認存放在/etc/my.cnf或者/etc/mysql/my.cnf,設置默認編碼:
[client]
default-character-set = utf8
[mysqld]
default-storage-engine = INNODB
character-set-server = utf8
collation-server = utf8_general_ci
- 安裝MySQL驅動
由於MySQL服務器以獨立的進程運行,並通過網絡對外服務,所以,需要支持Python的MySQL驅動來連接到MySQL服務器。MySQL官方提供了mysql-connector-python驅動,但是安裝的時候需要給pip命令加上參數--allow-external:
$ pip install mysql-connector-python --allow-external mysql-connector-python
MySQL服務器的test數據庫
# 導入MySQL驅動:
>>> import mysql.connector
# 注意把password設為你的root口令:
>>> conn = mysql.connector.connect(user='root', password='password', database='test')
>>> cursor = conn.cursor()
# 創建user表:
>>> cursor.execute('create table user (id varchar(20) primary key, name varchar(20))')
# 插入一行記錄,注意MySQL的占位符是%s:
>>> cursor.execute('insert into user (id, name) values (%s, %s)', ['1', 'Michael'])
>>> cursor.rowcount
1
# 提交事務:
>>> conn.commit()
>>> cursor.close()
# 運行查詢:
>>> cursor = conn.cursor()
>>> cursor.execute('select * from user where id = %s', ('1',))
>>> values = cursor.fetchall()
>>> values
[('1', 'Michael')]
# 關閉Cursor和Connection:
>>> cursor.close()
True
>>> conn.close()
web開發
web框架
- Flask,比較流行的Web框架
- Django:全能型Web框架;
- web.py:一個小巧的Web框架;
- Bottle:和Flask類似的Web框架;
- Tornado:Facebook的開源異步Web框架。
使用模板
Flask通過render_template()
函數來實現模板的渲染。和Web框架類似,Python的模板也有很多種。Flask默認支持的模板是jinja2
。
$ pip install jinja2
在Jinja2模板中,我們用{{ name }}
表示一個需要替換的變量。很多時候,還需要循環、條件判斷等指令語句,在Jinja2中,用{% ... %}
表示指令。
除了Jinja2,常見的模板還有:
- Mako:用<% ... %>和${xxx}的一個模板;
- Cheetah:也是用<% ... %>和${xxx}的一個模板;
- Django:Django是一站式框架,內置一個用{% ... %}和{{ xxx }}的模板。
異步IO
協程
所以子程序調用是通過棧實現的,一個線程就是執行一個子程序。
子程序調用總是一個入口,一次返回,調用順序是明確的。而協程的調用和子程序不同。
協程看上去也是子程序,但執行過程中,在子程序內部可中斷,然后轉而執行別的子程序,在適當的時候再返回來接着執行。
** 和多線程比,協程有何優勢? **
- 最大的優勢就是協程極高的執行效率。因為子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯。
- 第二大優勢就是不需要多線程的鎖機制,因為只有一個線程,也不存在同時寫變量沖突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多。
- 因為協程是一個線程執行,那怎么利用多核CPU呢?最簡單的方法是多進程+協程,既充分利用多核,又充分發揮協程的高效率,可獲得極高的性能。
Python對協程的支持是通過generator實現的。
在generator中,我們不但可以通過for循環來迭代,還可以不斷調用next()函數獲取由yield語句返回的下一個值。
但是Python的yield不但可以返回一個值,它還可以接收調用者發出的參數。
asyncio
asyncio
是Python 3.4版本引入的標准庫,直接內置了對異步IO的支持。
asyncio
的編程模型就是一個消息循環。我們從asyncio
模塊中直接獲取一個EventLoop
的引用,然后把需要執行的協程扔到EventLoop
中執行,就實現了異步IO。
- 小結
asyncio提供了完善的異步IO支持;
異步操作需要在coroutine
中通過yield from
完成;
多個coroutine
可以封裝成一組Task然后並發執行。
async/await
用asyncio提供的@asyncio.coroutine可以把一個generator標記為coroutine類型,然后在coroutine內部用yield from調用另一個coroutine實現異步操作。
為了簡化並更好地標識異步IO,從Python 3.5開始引入了新的語法async和await,可以讓coroutine的代碼更簡潔易讀。
請注意,async和await是針對coroutine的新語法,要使用新的語法,只需要做兩步簡單的替換:
把@asyncio.coroutine
替換為async
;
把yield from
替換為await
。
讓我們對比一下上一節的代碼:
@asyncio.coroutine
def hello():
print("Hello world!")
r = yield from asyncio.sleep(1)
print("Hello again!")
用新語法重新編寫如下:
async def hello():
print("Hello world!")
r = await asyncio.sleep(1)
print("Hello again!")
剩下的代碼保持不變。
小結
Python從3.5版本開始為asyncio提供了async和await的新語法;
注意新語法只能用在Python 3.5以及后續版本,如果使用3.4版本,則仍需使用上一節的方案。
aiohttp
asyncio可以實現單線程並發IO操作。如果僅用在客戶端,發揮的威力不大。如果把asyncio用在服務器端,例如Web服務器,由於HTTP連接就是IO操作,因此可以用單線程+coroutine實現多用戶的高並發支持。
asyncio實現了TCP、UDP、SSL等協議,aiohttp則是基於asyncio實現的HTTP框架。
我們先安裝aiohttp:
pip install aiohttp
然后編寫一個HTTP服務器,分別處理以下URL:
/
- 首頁返回b'<h1>Index</h1>'
;
/hello/{name}
- 根據URL參數返回文本hello, %s!
。
代碼如下:
import asyncio
from aiohttp import web
async def index(request):
await asyncio.sleep(0.5)
return web.Response(body=b'<h1>Index</h1>')
async def hello(request):
await asyncio.sleep(0.5)
text = '<h1>hello, %s!</h1>' % request.match_info['name']
return web.Response(body=text.encode('utf-8'))
async def init(loop):
app = web.Application(loop=loop)
app.router.add_route('GET', '/', index)
app.router.add_route('GET', '/hello/{name}', hello)
srv = await loop.create_server(app.make_handler(), '127.0.0.1', 8000)
print('Server started at http://127.0.0.1:8000...')
return srv
loop = asyncio.get_event_loop()
loop.run_until_complete(init(loop))
loop.run_forever()
注意aiohttp的初始化函數init()也是一個coroutine,loop.create_server()則利用asyncio創建TCP服務。