python 基礎知識

python 基礎知識

本文全部內容是學習期間作的筆記,僅爲我的查閱和複習方便而記錄。全部內容均摘自:http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000python

數據類型

  • 整數
  • 浮點數
  • 字符串mysql

    • 若是字符串內部既包含'又包含",能夠用轉義字符\來轉義。
    • 多行字符串能夠經過'''字符串內容'''來表示
    • r''表示''內部的字符串默認不轉義
  • 布爾值, true, false;布爾值能夠用andornot運算
  • 空值,None
  • 變量, 變量名必須是大小寫英文、數字和_的組合,且不能用數字開頭
  • 常量, 所有大寫的變量名錶示常量git

一種除法是//,稱爲地板除,兩個整數的除法仍然是整數web

>>> 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>

注意條件判斷後面的:,不要少寫了。sql

循環

  • 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相反:json

  • 查找和插入的時間隨着元素的增長而增長;
  • 佔用空間小,浪費內存不多。

因此,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對象,但listdictstr雖然是Iterable,卻不是Iterator

listdictstrIterable變成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的狀況,因此,如下內容看不懂也不要緊,由於基本上你不會用到。

  • 更多內容參考:http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014319106919344c4ef8b1e04c48778bb45796e0335839000

錯誤、調試和測試

錯誤處理

  • 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用來處理錯誤十分方便。出錯時,會分析錯誤信息並定位錯誤發生的代碼位置纔是最關鍵的。

程序也能夠主動拋出錯誤,讓調用者來處理相應的錯誤。可是,應該在文檔中寫清楚可能會拋出哪些錯誤,以及錯誤產生的緣由。

調試

  • print
  • 斷言assert
  • logging

logging容許你指定記錄信息的級別,有debuginfowarningerror等幾個級別,當咱們指定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)……表示第一、二、……個子串。 **

  • 貪婪匹配

最後須要特別指出的是,正則匹配默認是貪婪匹配,也就是匹配儘量多的字符。舉例以下,匹配出數字後面的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服務。

相關文章
相關標籤/搜索