1. 程式人生 > >Python-約束和異常處理

Python-約束和異常處理

  今天我們來說一說類的規範以及程式出現錯誤後我們要怎麼進行處理

一.類的約束

  首先,你要清楚,約束是對類的約束,比如,現在你是一個專案經理,然後呢,你給手下的人分活,張三你處理一下普通使用者登入,李四你處理一下會員登入,王五已處理一下管理員登入,那這個時候呢他們就開始分別去寫他們的功能了,但是呢,你要知道程式設計師不一定會有那麼好的默契,很有可能三個人會寫三個完全不同的方法,就比如這樣:

class Normal:          #張三,普通人登入
    def login(self):
        pass
class Member:         #李四,會員登入
    def denglu(self):
        pass
class Admin:            #王五,管理員登入
    def login(self):
        pass

  然後呢,他們把這樣的程式碼交給你了,你看了一眼,張三和王五還算OK,這個李四寫的是什麼鬼?denglu.....難受不,但是好歹能用,還能湊合,但是這時你這邊要使用了問題就來了

#專案經理寫的總入口
def login(obj):
    print('準備驗證碼.....')
    obj.login()
    print('進入主頁......')

  對於張三和王五的程式碼沒有問題,但是李四的你是不是調用不了,那如何避免這樣的問題呢?我們要約束程式的結構,也就是說在分配任務之前就應該把功能定義好,然後分別交給地下的程式設計師來完成相應的功能.

  在python中有兩種辦法來解決這樣的問題

    1.提取父類,然後在父類中定義好方法,在這個方法中什麼都不用幹,就拋一個異常就可以了,這樣所有的子類都必須重寫這個方法,否則訪問的時候就會報錯

    2.使用元類來描述父類,在元類中給出一個抽象方法,這樣子類就不得不給出抽象方法的具體實現,也可以起到約束的效果

  首先,我們先看第一種解決方案:提取一個父類,在父類中給出一個方法,並且在方法中不給出任何程式碼,直接拋異常.

class Base:
    def login(self):
        raise Exception('你沒有實現login方法()')
class Normal(Base):
    def login(self):
        pass
class Member(Base):
    def denglu(self):
        pass
class Admin(Base):
    def login(self):
        pass
#專案經理寫的總入口
def login(obj):
    print('準備驗證碼....')
    obj.login()
    print('進入主頁.....')

n = Normal()
m = Member()
a = Admin()
login(n)
login(m)   #報錯
login(a)

  在執行到login(m)的時候程式會報錯,原因是此時訪問的login()是父類中的方法,但是父類中的方法會丟擲一個異常,所以報錯.這樣程式設計師就不得不寫login方法了,從而對子類進行了相應的約束

  在本示例中要注意,我們丟擲的是Exception異常,而Exception是所有異常的根,我們無法通過這個異常來判斷出程式是因為什麼報的錯,所以最好是換一個比較專業的錯誤資訊,最好是換成NotImplementError,其含義是'沒有實現的錯誤',這樣程式設計師或者專案經理可以一目瞭然的知道是什麼錯了,就好比你犯錯了,我就告訴你犯錯了,你也不知道哪裡錯了,這時我告訴你,你xxx錯了,你改也好改不是?

  第二套方案:寫抽象類和抽象方法,這種方案相對來說比上一個麻煩一些,需要給大家先引入一個抽象的概念,什麼是抽象呢?想一下動物的吃,你怎麼描述?一個動物到底應該怎麼吃?是不是描述不清楚,這裡動物的吃就是一個抽象的概念,只是一個動作的概念,沒有具體實現,這種就是抽象的動作,換句話說,我們如果寫一個方法,不知道方法的內部應該到底寫什麼,那這個方法其實就應該是一個抽象的方法,如果一個類中包含抽象方法,那麼這個類一定是一個抽象類,,抽象類是不能有例項的,比如你看看一些抽象派的畫作,在現實中是不存在的,也就無法建立例項物件與之相對應,所以抽象類無法建立物件,建立物件的時候會報錯.

  在Python中編寫一個抽象類比較麻煩,需要引入abc模組中的ABCMeta和abstractmethod這兩個內容,來我們看一個例子

form abc import ABCMeta,abstractmethod
#類中包含了抽象方法,那此時這個類就是抽象類.注意:抽象類可以有普通方法
class IGame(metaclass = ABCMeta):
    #一個遊戲到底怎麼玩兒?你能形容?流程能一樣嗎?
    @abstractmethod
    def play(self):
        pass
    def turn_off(self):
        print('破B遊戲不玩了,脫坑了')
class DNF Game(IGame):
    #子類必須實現父類中的抽象方法,否則子類也是抽象類
    def play(self):
        print('dnf的玩兒法')
#g = IGame()   #抽象類不能建立物件
dg = DNFGame()
dg.play()

  通過程式碼我們能發現,這裡的IGame對DNFGame進行了約束,換句話說,父類對子類進行了約束.

  接下來繼續解決我們一開始的問題

from abc import ABCMeta,abstractmethod

class Base(metaclass = ABCMeta):
    @abstractmethod
    def login(self):
        pass
class Normal(Base):
    def login(self):
        pass
class Member(Base):
    def denglu(self):    #這個就沒用了
        pass
    def login(self):       #子類對父類進行實現
        pass
class Admin(Base):
    def login(self):
        pass

#專案經理寫的總入口
def login(obj):
    print('準備驗證碼....')
    obj.login()
    print('進入主頁....')
n = Normal()
m = Member()
a = Admin()
login(n)
login(m)
login(a)

  總結:約束,其實就是父類對子類進行約束,子類必須要寫xxx方法,在Python中約束的方式和方法有兩種:

    1.使用抽象類和抽象方法,由於該方案來源是java和c#,所以使用頻率還是很少的

    2.使用人為丟擲異常的方案,並且儘量丟擲的是NotlmplementError,這樣比較專業,而且錯誤比較明確(推薦)

二.異常處理

  首先我們先說一下,什麼是異常?異常是程式在執行過程中產生的錯誤,就好比你在回家路上突然天塌了,那這個就屬於一個異常,總之就是不正常,那如果程式出現了異常,怎麼處理呢?在之前的學習中我們已經寫過類似的程式碼了

  我們先製造一個錯誤,來看看異常長什麼樣

def chu(a,b):
    return a/b
ret = chu(10,0)
print(ret)
結果:
Traceback (most recent call last):
    File "E:/s17pycharm/每日作業/練習2.py", line 762, in <module>
        ret = chu(10,0)
    File "E:/s17pycharm/每日作業/練習2.py", line 761, in chu
        return a/b
ZeroDivisionError: division by zero

  什麼錯誤呢,除法中除數不能是0,那如果真的出了這個錯,你把這一堆資訊拋給客戶麼?肯定不能,那如何處理呢?

def chu(a,b):
    return a/b
try:
    ret = chu(10,0)
    print(ret)
except Exception as e:
    print('除數不能是0')
結果:
除數不能是0

  那try...except是什麼意思呢?嘗試著執行xxx程式碼,出現了錯誤就執行except後面的程式碼,在這個過程中,當代碼出現錯誤的時候,系統會產生一個異常物件,然後這個異常會向外拋,被except攔截,並把接收到的異常物件賦值給e,那這裡的e就是異常物件,那這裡的Exception的子類物件,我們看到的ZeroDivisionError 其實就是Exception的子類,那這樣寫好像有點問題,Exception表示所有的錯誤,太籠統了,所有的錯誤都會被認為是Exception,當程式中出現多種錯誤的時候,就不好分類了,最好是出什麼異常就用什麼來處理,這樣就更加合理了,所以在try...except語句中,還可以寫更多的except.

try:
    print('各種操作...')
except ZeroDivisionError as e:
    print('除數不能是0')
except FileNotFoundError as e:
    print('檔案不存在')
except Exception as e:
    print('其他錯誤')

  此時程式執行過程中,如果出現了ZeroDivisionError就會被第一個except捕獲,如果出現了FileNotFoundError就會被第二個except捕獲,如果都不是這兩個異常,那就會被最後的Exception捕獲,總之最後的Exception就是我們異常處理的最後一個守門員,這時我們最常用的一套寫法,接下來給出一個完整的異常處理寫法(語法):

try:
    '''操作'''
except Exception as e:
    '''異常的父類,可以捕獲所有的異常'''
else:
    '''保護不丟擲異常的程式碼,當try中無異常的時候執行'''
finally:
    '''最後總是要執行我'''

  解讀: 程式先執行操作, 然後如果出錯了會走except中的程式碼, 如果不出錯, 執行else中的程式碼. 不論處不出錯,最後都要執行finally中的語句. 一般我們用try...except就夠用了,頂多加上finally,finally一般用來作為收尾工作. 

  上面是處理異常, 我們在執行程式碼的過程中如果出現了一些條件上的不對等, 根本不符合我的程式碼邏輯, 比如, 引數,我要求你傳遞一個數字, 你非得傳遞一個字串, 那對不起,我沒辦法幫你處理. 那如何通知你呢? 兩個方案,

  方案一, 直接返回即可,我不管你還不行麼?

  方案二,丟擲一個異常,告訴你,我不好惹,乖乖的聽話. 

  第一方案是我們之前寫程式碼經常用到的方案, 但這種方案並不夠好, 無法起到警示作用, 所以以後的程式碼中如果出現了類似的問題,直接拋一個錯誤出去. 那怎麼拋呢? 我們要用到raise關鍵字

def add(a, b):
    '''
    給我傳遞兩個整數. 我幫你計算兩個數的和
    :param :param a:
    :param :param b:
    :return :return:
    '''
    if not type(a) == int and not type(b) == int:
        # 當程式執行到這句話的時候. 整個函式的呼叫會被中斷. 並向外丟擲一個異常.
        raise Exception("不是整數, 朕不能幫你搞定這麼複雜的運算.")
    return a + b

# 如果呼叫方不處理異常. 那產生的錯誤將會繼續向外拋. 最後就拋給了使用者
# add("你好", "我叫賽利亞")

# 如果呼叫方處理了異常. 那麼錯誤就不會丟給使用者. 程式也能正常進行
try:
    add("胡辣湯", "滋滋冒油的大腰子")
except Exception as e:
    print("報錯了. 自己處理去吧")

  當程式執行到raise, 程式會被中斷,並例項化後面的異常物件, 拋給呼叫方, 如果呼叫方不處理, 則會把錯誤繼續向上丟擲, 最終拋給使用者,如果呼叫方處理了異常,那程式可以正常的進行執行.

  說了這麼多,異常也知道如何丟擲和處理了, 但是我們現在用的都是人家python給的異常. 如果某一天,你寫的程式碼中出現了一個無法用現有的異常來解決問題,那怎麼辦呢? 彆著急,python可以自定義異常.

  自定義異常: 非常簡單,只要你的類繼承了Exception類,那你的類就是一個異常類,就這麼簡單. 比如,你要寫一個男澡堂子程式,那這時要是來個女的, 你怎麼辦? 是不是要丟擲一個性別異常啊? 好. 我們來完成這個案例:

# 繼承Exception. 那這個類就是一個異常類
class GenderError(Exception):
    pass
class Person:
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

def nan_zao_tang_xi_zao(person):
    if person.gender != "男":
        raise GenderError("性別不對. 這裡是男澡堂子")

p1 = Person("alex", "男")
p2 = Person("eggon", "蛋")

# nan_zao_tang_xi_zao(p1)
# nan_zao_tang_xi_zao(p2) # 報錯. 會丟擲一個異常: GenderError

# 處理異常
try:
    nan_zao_tang_xi_zao(p1)
    nan_zao_tang_xi_zao(p2)
except GenderError as e:
    print(e) # 性別不對, 這裡是男澡堂子
except Exception as e:
    print("反正報錯了")

  ok搞定, 但是, 如果是真的報錯了,我們在除錯的時候, 最好是能看到錯誤源自於哪裡,怎麼辦呢? 需要引入另一個模組traceback,這個模組可以獲取到我們每個方法的呼叫資訊,又被成為堆疊資訊,這個資訊對我們排錯是很有幫助的. 

import traceback
# 繼承Exception. 那這個類就是⼀個異常類
class GenderError(Exception):
    pass

class Person:
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

def nan_zao_tang_xi_zao(person):
    if person.gender != "男":
        raise GenderError("性別不對. 這裡是男澡堂子")

p1 = Person("alex", "男")
p2 = Person("eggon", "蛋")

# nan_zao_tang_xi_zao(p1)
# nan_zao_tang_xi_zao(p2) # 報錯. 會丟擲一個異常: GenderError

# 處理異常
try:
    nan_zao_tang_xi_zao(p1)
    nan_zao_tang_xi_zao(p2)
except GenderError as e:
    val = traceback.format_exc() # 獲取到堆疊資訊
    print(e) # 性別不對, 這裡是男澡堂子
    print(val)
except Exception as e:
    print("反正報錯了")

結果: 
性別不對. 這裡是男澡堂子
Traceback (most recent call last):
    File "/Users/sylar/PycharmProjects/oldboy/面向物件/day05.py", line 155, in
<module>
        nan_zao_tang_xi_zao(p2)
    File "/Users/sylar/PycharmProjects/oldboy/面向物件/day05.py", line 144, in
nan_zao_tang_xi_zao
    raise GenderError("性別不對. 這裡是男澡堂子")
GenderError: 性別不對. 這裡是男澡堂子

  搞定了,這樣我們就能收放自如了, 當測試程式碼的時候把堆疊資訊打印出來, 但是當到了線上的生產環境的時候把這個堆疊去掉即可. 

四.MD5加密

  想一個事情,你在銀行取錢或者辦卡的時候,我們都要輸入密碼,那這個密碼如果就按照我們輸入的那樣去儲存, 是不是很不安全啊,如果某一個程式設計師進入到了銀行的資料庫, 而銀行的資料庫又存的都是明文(不加密的密碼)密碼, 這時整個銀行的賬戶裡的資訊都是非常非常不安全的,那怎麼辦才安全呢? 給密碼加密,並且是不可逆的加密演算法, 這樣即使獲取到了銀行的賬戶和密碼資訊,對於黑客而言都無法進行破解,那我們的賬號就相對安全了很多,那怎麼加密呢? 最常用的就是用MD5演算法. 

  MD5是一種不可逆的加密演算法,它是可靠的,並且安全的,在python中我們不需要手寫這一套演算法,只需要引入一個叫hashlib的模組就能搞定MD5的加密工作

import hashlib

obj = hashlib.md5()
obj.update("alex".encode("utf-8")) # 加密的必須是位元組
miwen = obj.hexdigest()
print(miwen) # 534b44a19bf18d20b71ecc4eb77c572f

  那這樣的密文安全麼? 其實是不安全的,當我們用這樣的密文去找一個所謂的MD5解密工具,是有可能解密成功的. 

  這就尷尬了,MD5不是不可逆麼? 注意,這裡有一個叫撞庫的問題,就是由於MD5的原始演算法已經存在很久了,那就有一些人用一些簡單的排列組合來計算MD5,然後當出現相同的MD5密文的時候就很容易反推出原來的資料是什麼,所以並不是MD5可逆, 而是有些別有用心的人把MD5的常用資料已經算完並保留起來了. 

  那如何應對呢? 加鹽就行了,在使用MD5的時候,給函式的引數傳遞一個byte即可. 

import hashlib

obj = hashlib.md5(b"fjlksajflkjasfsalwer123dfskjf") # 加鹽
obj.update("alex".encode("utf-8")) # 加密的必須是位元組
miwen = obj.hexdigest()
print(miwen) # 99fca4b872fa901aac30c3e952ca786d

  此時你再去任何網站去試,累死他也解不開密. 

  那MD5如何應用呢? 

import hashlib

def my_md5(s):
    obj = hashlib.md5(b"fjlksajflkjasfsalwer123dfskjf")
    obj.update(s.encode("utf-8")) # 加密的必須是位元組
    miwen = obj.hexdigest()
    return miwen

# alex: 99fca4b872fa901aac30c3e952ca786d
username = input("請輸入使用者名稱:")
password = input("請輸入密碼:")
# 資料儲存的時候.
# username: my_md5(password)
# 假設現在的使用者名稱和密碼分別是
# wusir: 99fca4b872fa901aac30c3e952ca786d ==> wusir: alex
# 使用者登入
if username == "wusir" and my_md5(password) == "99fca4b872fa901aac30c3e952ca786d":
    print("成功")
else:
    print("失敗")

  所以以後存密碼就不要存明文了,要存密文,安全,並且這裡加的鹽不能改來改去的,否則, 整套密碼就都亂了. 

五.日誌

  首先, 你要知道在編寫任何一款軟體的時候, 都會出現各種各樣的問題或者bug,這些問題或者bug一般都會在測試的時候給處理掉,但是多多少少的都會出現一些意想不到的異常或者錯誤,那這個時候, 我們是不知道哪裡出了問題的,因為很多BUG都不是必現的bug,如果是必現的,測試的時候肯定能測出來,最頭疼的就是這種不必現的bug,我這跑沒問題,客戶那一用就出問題,那怎麼辦呢?我們需要給軟體準備一套日誌系統,當出現任何錯誤的時候, 我們都可以去日誌系統裡去查,看哪裡出了問題,這樣在解決問題和bug的時候就多了一個幫手,那如何在python中建立這個日誌系統呢? 很簡單

  1. 匯入logging模組. 

  2. 簡單配置一下logging

  3. 出現異常的時候(except),向日志裡寫錯誤資訊. 

# filename: 檔名
# format: 資料的格式化輸出. 最終在日誌檔案中的樣子
#     時間-名稱-級別-模組: 錯誤資訊
# datefmt: 時間的格式
# level: 錯誤的級別權重, 當錯誤的級別權重大於等於leval的時候才會寫入檔案
logging.basicConfig(filename='x1.txt',
                            format='%(asctime)s - %(name)s - %(levelname)s -%
(module)s: %(message)s',
                            datefmt='%Y-%m-%d %H:%M:%S',
                            level=0) # 當前配置表示 10以上的分數會被寫入檔案

# CRITICAL = 50
# FATAL = CRITICAL
# ERROR = 40
# WARNING = 30
# WARN = WARNING
# INFO = 20
# DEBUG = 10
# NOTSET = 0
logging.critical("我是critical") # 50分. 最貴的
logging.error("我是error") # 40分
logging.warning("我是警告") # 警告 30
logging.info("我是基本資訊") # 20
logging.debug("我是除錯") # 10
logging.log(2, "我是自定義") # 自定義. 看著給分
    

  簡單做個測試,應用一下

class JackError(Exception):
    pass

for i in range(10):
    try:
        if i % 3 == 0:
            raise FileNotFoundError("檔案不在啊")
        elif i % 3 == 1:
            raise KeyError("鍵錯了")
        elif i % 3 == 2:
            raise JackError("傑克Exception")
    except FileNotFoundError:
        val = traceback.format_exc()
        logging.error(val)
    except KeyError:
        val = traceback.format_exc()
        logging.error(val)
    except JackError:
        val = traceback.format_exc()
        logging.error(val)
    except Exception:
        val = traceback.format_exc()
        logging.error(val)

  最後, 如果你係統中想要把日誌檔案分開,比如,一個大專案, 有兩個子系統, 那兩個子系統要分開記錄日誌,方便除錯,那怎麼辦呢? 注意,用上面的basicConfig是搞不定的,我們要藉助檔案助手(FileHandler)來幫我們完成日誌的分開記錄

import logging

# 建立一個操作日誌的物件logger(依賴FileHandler)
file_handler = logging.FileHandler('l1.log', 'a', encoding='utf-8')
file_handler.setFormatter(logging.Formatter(fmt="%(asctime)s - %(name)s - %
(levelname)s -%(module)s: %(message)s"))

logger1 = logging.Logger('s1', level=logging.ERROR)
logger1.addHandler(file_handler)

logger1.error('我是A系統')

# 再建立一個操作日誌的物件logger(依賴FileHandler)
file_handler2 = logging.FileHandler('l2.log', 'a', encoding='utf-8')
file_handler2.setFormatter(logging.Formatter(fmt="%(asctime)s - %(name)s -
%(levelname)s -%(module)s: %(message)s"))

logger2 = logging.Logger('s2', level=logging.ERROR)
logger2.addHandler(file_handler2)

logger2.error('我是B系統')