什么是異常?
異常即是一個事件,該事件會在程序執行過程中發生,影響了程序的正常執行。
一般情況下,在Python無法正常處理程序時就會發生一個異常。
異常是Python對象,表示一個錯誤。
當Python腳本發生異常時我們需要捕獲處理它,否則程序會終止執行。
異常處理
捕捉異常可以使用try / except語句。
try/except語句用來檢測try語句塊中的錯誤,從而讓except語句捕獲異常信息並處理。
如果你不想在異常發生時結束你的程序,只需在try里捕獲它。
以下為簡單的try....except...else的語法:
正常的操作 ...................... except: #可以多次使用except捕捉多個錯誤 發生異常,執行這塊代碼 ...................... else: #可有可無 如果沒有異常執行這塊代碼
例:索引錯誤
l = [1, 2, 3, 4] try: l[5] except IndexError: print('索引錯誤') else: print('成功')
運行結果:
l = [1, 2, 3, 4] try: l[5] except IndexError as i: #可以給錯誤個別名 print(i) #然后打印錯誤,會出現原生錯誤 else: print('成功')
運行結果:
try-finally 語句
try-finally 語句無論是否發生異常都將執行最后的代碼。
try: <語句> finally: <語句> #退出try時總會執行 raise
例:
l = [1, 2, 3, 4] try: l[5] except IndexError: print('索引錯誤') finally: print('happy')
運行結果:
自定義異常
通過創建一個新的異常類,程序可以命名它們自己的異常。異常應該是典型的繼承自Exception類,通過直接或間接的方式。
以下為與RuntimeError相關的實例,實例中創建了一個類,基類為RuntimeError,用於在異常觸發時輸出更多的信息。
在try語句塊中,用戶自定義的異常后執行except塊語句,變量 e 是用於創建Networkerror類的實例。
例:
class 異常錯誤名字(Exception): #自定義異常,繼承Exception類的形式 def __init__(self, msg): #初始化 self.message = msg # def __str__(self): # return self.message try: raise 異常錯誤名字('我的異常') #固定格式 except 異常錯誤名字 as e: print(e)
多任務多線程
線程的並發是利用cpu的上下文的切換,在python3里是假的多線程,它不是真真正正的並行,其實就是串行,只不過利用了cpu上下文的切換而已
線程是程序最小的調度單位
例:串行
def test1(): for i in range(3): print('test1=======>%s' % i) def test2(): for i in range(3): print('test2=======>%s' % i) test1() test2()
運行結果:
例:並發
import threading #導入線程模塊 import time #導入time模塊 def test1(): for i in range(3): time.sleep(1) #為了看出效果,延遲1秒 print('test1=======>%s' % i) def test2(): for i in range(3): time.sleep(1) #為了看出效果,延遲1秒 print('test2=======>%s' % i) t1 = threading.Thread(target=test1) #實例化一個線程,用Thread方法 ,目標是test1(不加括號,是內存地址) t2 = threading.Thread(target=test2) t1.start() #執行這個線程 t2.start() #執行這個線程
運行結果:圖一出現后1秒,再次出現兩行,如圖二,再1秒后,再次出現兩行,如圖三
多線程執行的順序是無序的
def test1(n): time.sleep(1) print('task', n) for i in range(10): t = threading.Thread(target=test1, args=('t-%s' % i,)) #傳參args,必須元組形式,必須要加逗號 t.start()
運行結果:
多線程共享全局變量
因為函數內得不到函數外的數據,所以要global聲明
g_num = 0 #全局變量 def update(): global g_num #global聲明全局變量(才可以修改) for i in range(10): g_num += 1 update() print(g_num)
運行結果:
g_num = 0 #全局變量 def update(): global g_num #global聲明全局變量(才可以修改) for i in range(10): g_num += 1 def reader(): global g_num print(g_num) t1 = threading.Thread(target=update) t2 = threading.Thread(target=reader) t1.start() t2.start()
運行結果:
線程是繼承在進程里的,沒有進程就沒有線程
GIL全局解釋器鎖
GIL的全稱是:Global Interpreter Lock,意思就是全局解釋器鎖,這個GIL並不是python的特性,他是只在Cpython解釋器里引入的一個概念,而在其他的語言編寫的解釋器里就沒有這個GIL例如:Jython,Pypy
為什么會有GIL?:
隨着電腦多核cpu的出現核cpu頻率的提升,為了充分利用多核處理器,進行多線程的編程方式更為普及,隨之而來的困難是線程之間數據的一致性和狀態同步,而python也利用了多核,所以也逃不開這個困難,為了解決這個數據不能同步的問題,設計了gil全局解釋器鎖。
例:
def test1(): global global_num #聲明全局變量 for i in range(1000000): global_num += 1 #加1 print("test1", global_num) def test2(): global global_num for i in range(1000000): global_num += 1 print("test2", global_num) t1 = threading.Thread(target=test1) t2 = threading.Thread(target=test2) t1.start() t2.start() # # t1.join() #等t1線程執行完畢 # # t2.join() print(global_num)
執行結果:會發現每次都是不同的
那么問題來了,為什么每次的執行結果都不一樣呢,讓我們結合上面的大圖,分析一下:
上圖和實例解析:
線程共享數據池,就相當於聲明的全局變量,線程1拿到公共數據后,首先會申請到gil鎖,就是在它進行操作時,不允許其他線程的操作,通過python解釋器,調用系統原生線程,,然后假設在cpu上執行,若執行時間到了,線程1 還沒有執行結束,那么將會被要求釋放gil鎖,相當於暫停,釋放后線程2會拿到公共數據,同樣申請到gil鎖,通過python解釋器調用原生線程,假設在cpu3上執行,並且完成+1的賦值,全局變量將會從0變成1,並且 ,線程2釋放gil鎖,線程1再次開始執行,將從暫停的位置開始,當結束+1賦值時,釋放gil鎖,這里線程1的從0到1賦值的過程將會覆蓋掉線程2的賦值,所以,全局變量還是1,接下來還是同樣的道理,不管線程1還是線程2,每一次賦值都是覆蓋形式的,所以當主線程結束的時候,每次的結果就不一樣了。
那么上面的結果並不是我們想要的,針對上面的情況我們的解決的方案是再加一把鎖
互斥鎖,要么不作,要么做完
GIL全局解釋器鎖 import threading global_num = 0 lock = threading.Lock() #申請一把鎖 def test1(): global global_num lock.acquire() #上鎖 for i in range(1000000): global_num += 1 lock.release() #釋放鎖 print("test1", global_num) def test2(): global global_num lock.acquire() #上鎖 for i in range(1000000): global_num += 1 lock.release() #釋放鎖 print("test2", global_num) t1 = threading.Thread(target=test1) t2 = threading.Thread(target=test2) t1.start() t2.start() t1.join() #為了防止主線程結束的太快,等t1線程執行完畢 t2.join() #為了防止主線程結束的太快,等t2線程執行完畢 print(global_num)
運行結果:
那么上過鎖之后,實際就變成了串行
例:下面代碼開了10個進程,跑了多久的時間
import time def run(n): time.sleep(2) print('this is running======>%s' % n) for i in range(10): t = threading.Thread(target=run, args=(i,)) t.start()
解決方案:
import time def run(n): time.sleep(2) print('this is running======>%s' % n) l = [] #建立一個空列表 start = time.time() #記錄開始的時間 for i in range(10): t = threading.Thread(target=run, args=(i,)) t.start() l.append(t) #把創建的實例加入列表 for j in l: #循環l列表 j.join() #等待循環進程結束 end = time.time() #記錄結束的時間 print('cost', (end-start))
只要在進行耗時的IO操作的時候,能釋放GIL,所以只要在IO密集型的代碼里,用多線程就很合適
進程
一個程序運行起來之后,代碼+用到的資源稱之為進程,它是操作系統分配資源的基本單位,不僅可以通過線程完成多任務,進程也是可以的,cpu密集的時候適合用多進程
多進程的並發
import time import multiprocessing #導入多重處理模塊 def test1(): for i in range(10): time.sleep(1) print('test1====>', i) def test2(): for i in range(10): time.sleep(1) print('test2====>', i) if __name__=='__main__': #main的固定格式,如果沒有將報錯不支持 p1 = multiprocessing.Process(target=test1) #進程1的調用 p2 = multiprocessing.Process(target=test2) #進程2的調用 p1.start() p2.start()
一這兩個進程任務為例,如果cpu核數>=2,那么就是真真正正的 並行
運行結果:以此類推
進程之間是相互獨立的
import multiprocessing import time g_num = 0 def update(): #寫函數 global g_num for i in range(100): g_num += 1 print(g_num) def reader(): #讀函數 print(g_num) if __name__ == '__main__': p1 = multiprocessing.Process(target=update) p2 = multiprocessing.Process(target=reader) p1.start() p2.start()
運行結果:,由此看出讀函數沒有讀到寫函數更改的內容,所以得出,進程之間互相獨立,互不影響
進程池
什么時候用進程池?
當不知道有多少個進程池要跑的時候,用進程池最合適。
import multiprocessing from multiprocessing import Pool #或者用 multiprocessing.Pool(),再給他賦變量 import time import threading g_num = 0 def test1(n): for i in range(n): time.sleep(1) print('test1', i) def test2(n): for i in range(n): time.sleep(1) print('test2', i) if __name__ == '__main__': pool = Pool() #聲明進程池 #括號內沒有聲明進程數,默認無限(根據電腦配置情況) pool.apply_async(test1, (10,)) #使用方法並且加參數 pool.apply_async(test2, (10,)) pool.close() #進程池關閉 pool.join() #進程池等待(注意的是:join必須放在close后邊)
這就是進程池的並發,運行結果:
等等
協程
進程是資源分配的單位,切換需要的資源最大,效率低
線程是操作系統調度的單位,切換需要的資源一般,效率一般
協程是寄生在線程里的,所以協程切換任務資源很小,效率高。IO操作密集的時候首選協程
多進程、多線程根據cpu核數不一樣可能是並行的,但是協成在一個線程中
協程,其實就是串行
例:
import gevent,time def test1(): for i in range(10): time.sleep(1) #遇見延遲就切換 print('test1', i) def test2(): for i in range(10): time.sleep(1) #遇見延遲就切換 print('test2', i) g1 = gevent.spawn(test1) #聲明實例,調用函數的方法就是spawn g2 = gevent.spawn(test2) g1.join() #調用方式join,不是start g2.join()
運行結果:依次出現,由此看出是串行
協程,可以自動切換
由於協程工作時遇見延遲就會切換,所以會在test1,test2來回切換,因為test1延遲1秒,test2,延遲2秒,所以會按照2個test1,一個test2 的規律打印
import gevent,time # from gevent import monkey #打個補丁支持time.sleep() # monkey.patch_all() #讓gevent支持time.sleep() def test1(): for i in range(10): # time.sleep(1) #遇見延遲就切換 gevent.sleep(1) #延遲1秒 print('test1', i) def test2(): for i in range(10): # time.sleep(2) #遇見延遲就切換 gevent.sleep(2) #延遲2秒 print('test2', i) g1 = gevent.spawn(test1) #聲明實例,調用函數的方法就是spawn g2 = gevent.spawn(test2) g1.join() #調用方式join,不是start g2.join()
運行結果: