python 基礎知識


python 基礎知識

本文所有內容是學習期間做的筆記,僅為個人查閱和復習方便而記錄。所有內容均摘自:http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000

數據類型

  • 整數
  • 浮點數
  • 字符串
  • 如果字符串內部既包含'又包含",可以用轉義字符\來轉義。
  • 多行字符串可以通過'''字符串內容'''來表示
  • r''表示''內部的字符串默認不轉義
  • 布爾值, true, false;布爾值可以用andornot運算
  • 空值,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對象,但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的情況,所以,以下內容看不懂也沒關系,因為基本上你不會用到。

錯誤、調試和測試

錯誤處理

  • 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)……表示第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服務。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM