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

約束和異常處理

本節主要內容:

1. 類的約束

2. 異常處理理

3. 自定義異常

4. MD5加密

5. 日誌

一、類的約束

  首先,要清楚。約束是對類的約束。每個人和每個人寫程式碼用的方法名和類名都不一樣假如你處理一個使用者登入,有三個不同級別的登陸要求分別分給三個人寫你要知道程式設計師不一定有那麼好的默契。很有可能三個人會寫不同的方法,比如就像是這樣:

class Normal: # 張三, 普通⼈登入
     def login(self):
         pass

class Member: # 李四, 會員登入
     def denglu(self):
         pass
 
class Admin: # 王五, 管理員登入
    def login(self):
         pass

  寫出了這樣的程式碼,李四寫的方法名什麼鬼???denglu拼音看著就難受。但是功能實現了,但是你這邊要測試了。問題來了

class Normal: # 張三, 普通⼈登入
     def login(self):
         pass

class Member: # 李四, 會員登入
     def denglu(self):
         pass
 
class Admin: # 王五, 管理員登入
    def login(self):
         pass
def login(obj):
    print("準備驗證碼.......")
   obj.login()
    print("進⼊主⻚.......")
n = Normal()
m = Member()
a = Admin()
login(n)
login(m)
login(a)

  你會發現除了李四的程式碼其他的都可以呼叫。如何避免這樣的問題呢?我們要約束下程式的結構。在分配任務之前就應該把功能定義好。然後分別交給底下的程式設計師來完成相應的功能。

  在python中有兩種約束的方法來解決這樣的問題

  1.提取父類。然後再父類中定義好方法。在這個方法中什麼都不用做,丟擲異常就可以了。所有的子類必須重寫這個類否則就會報錯拋異常。

class Base:
    def login(self):
        raise NotImplementedError("需要重寫該方法")

class Normal(Base):
    def login(self):
        print("普通人登陸")
class Member(Base):
    def login(self):
        print("會員登陸")
class Admin(Base):
    def denglu(self):
        print("管理員登陸")

#整合所有的方法
def test(obj):
    obj.login()

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

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

  在本⽰例中. 要注意. 我們丟擲的是Exception異常. ⽽Exception是所有異常的根. 我們⽆法通
過這個異常來判斷出程式是因為什麼報的錯. 所以. 最好是換⼀個比較專業的錯誤資訊. 最好

是換成NotImplementError. 其含義是. "沒有實現的錯誤". 這樣程式設計師或者項⽬經理可以⼀⽬
瞭然的知道是什麼錯了. 就好比. 你犯錯了. 我就告訴你犯錯了. 你也不知道哪⾥錯了. 這時我
告訴你, 你xxx錯了. 你改也好改不是?

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

  抽象類是不能例項的,也就無法建立例項物件與之相應。所以抽象類無法建立物件。建立物件的時候會報錯。

  在python中編寫一個抽象類比較麻煩。需要引入abc模組中的ABCMeta和abstractmethod這兩個內容。

  這裡有一個例子:

from abc import ABCMeta, abstractmethod
class Animal(metaclass=ABCMeta):
    @abstractmethod 
    def chi(self):pass #抽象方法

    def dong(self): #抽象類中也可以有例項方法
        print("會動")

class Cat(Animal):
    def chi(self): #給抽象方法的具體實現呼叫
        print("貓吃魚")
cat = Cat()
cat.chi()
cat.dong()

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

  繼續解決登陸的問題:

from abc import ABCMeta,abstractmethod #需要先導包
class Base(metaclass=ABCMeta): #定義抽象類
    @abstractmethod
    def login(self):pass #定義抽象方法
class Normal(Base):
    def login(self): #給抽象方法具體實現
        print("普通使用者")
class Member(Base): 
    def login(self): #給抽象方法具體實現
        print("會員使用者")
class Admin(Base):
    def denglu(self): #報錯,這裡用的是denglu並非login
        print("管理員使用者")
def test(obj):
    obj.login()

n = Normal()
test(n)

m = Member()
test(m)

a = Admin() #報錯,這裡用的是denglu並非login
test(a)

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

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

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

二、異常處理

  首先,我們先了解下什麼是異常。異常就是程式執行過程中產生的錯誤。那如果程式出現了異常。怎麼處理呢?在之前我們已經寫過類似的程式碼了。

  下面我們先製造一個錯誤。來看看異常什麼樣子。

def chu(a, b):
     return a/b
ret = chu(10, 0)
print(ret)

  除法中除數不能為零。如果真的出了這個錯誤你不能把這一堆資訊拋給客戶,要不然肯定給客戶打死,那如何處理呢?

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

結果:
除數不能是0

  try.....except是什麼意思呢?就是嘗試執行程式碼。如果出現錯誤。就執行except後面的程式碼。

  系統產生一個異常物件(面向物件一切皆物件),這個異常物件會外拋。被except攔截。並把接收的異常物件賦值給e,Exception是所有異常的父類或基類,也就是異常的根。但是這樣寫好些有點太籠統了。所有的錯誤都會被認為是Exception。當程式出現多種錯誤的時候就不好分類了,最好是出什麼異常就用什麼來處理。這樣更加合理所以在try.....except語句中還可以寫更多的except

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

  解讀: 程式先執⾏操作, 然後如果出錯了會走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
# 如果調⽤⽅不處理異常. 那產⽣的錯誤將會繼續向外拋. 最後就拋給了⽤戶

# 如果調⽤⽅處理了異常. 那麼錯誤就不會丟給⽤戶. 程式也能正常進⾏
try:
     add(10, "20")
except Exception as e:
     print("報錯了. ⾃⼰處理去吧")

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

 

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

  自定義異常,非常簡單。只要你的類繼承了Exception類。那你的類就是一個異常類。就這麼簡單。比如。你要寫一個男澡堂子程式。如果這是來個女的。是不是要丟擲一個異常,一個性別異常。

class GenderError(Exception):
    pass

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

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

p1 = Person("wusir","男")
p2 = Person("小紅","女") #丟擲異常:GenderError("性別不對,這裡是男澡堂")

#處理異常
try:
    nan_zao_tang(p1)
    nan_zao_tang(p2)
except GenderError as e:
    print(e)
except Exception as e:
    print("反正就是報錯了")

  這就搞定了,但是,但是如果真的報錯了。我們最好能看到錯誤源自哪裡,怎麼辦呢?需要引入另一個模組traceback。這個模組可以獲取到我們每個方法的呼叫資訊。

  又被稱為堆疊資訊,這個資訊對我們排查錯誤很有幫助的。

import traceback
class GenderError(Exception):
    pass

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

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

p1 = Person("wusir","男")
p2 = Person("小紅","女") #丟擲異常:GenderError("性別不對,這裡是男澡堂")

#處理異常
try:
    nan_zao_tang(p1)
    nan_zao_tang(p2)
except GenderError as e:
    val = traceback.format_exc()
    print(e)
    print(val)
except Exception as e:
    print("反正就是報錯了")


結果:

性別不對,這裡是男澡堂
Traceback (most recent call last):
  File "D:/python_workspace_s18/day 20 約束 異常處理 MD5 日誌處理/練習.py", line 57, in <module>
    nan_zao_tang(p2)
  File "D:/python_workspace_s18/day 20 約束 異常處理 MD5 日誌處理/練習.py", line 49, in nan_zao_tang
    raise GenderError("性別不對,這裡是男澡堂")
GenderError: 性別不對,這裡是男澡堂

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

四、MD5加密

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

import hashlib
obj = hashlib.md5()
obj.update("python".encode("utf-8"))
s = obj.hexdigest()
print(s)


結果:
23eeeb4347bdd26bfc6b7ee9a3b755dd

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

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

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

import hashlib
obj = hashlib.md5(b"adfaf;lkjfakl;djfiowueriojksafjk") #在這裡加點小料
obj.update("python".encode("utf-8"))
s = obj.hexdigest()
print(s)

結果:
0971516bb92c3a2f821f561bf34a67fc

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

  接下來就是MD5的應用:

import hashlib
obj = hashlib.md5(b"ajfkljakldjfkljalkjfklajf") #加點小料
obj.update("123456".encode("utf-8"))
s = obj.hexdigest()
print(s) #先運算從密碼的密文是什麼

def my_md5(s):
    obj = hashlib.md5(b"ajfkljakldjfkljalkjfklajf") #加點小料
    obj.update(s.encode("utf-8")) #加密的必須是位元組
    miwen = obj.hexdigest()
    return miwen
username = input("請輸入使用者名稱:")
password = input("請輸入密碼:")
if username == "python" and my_md5(password) == "5ae2b66c95540260b97b75aa4b4e35f9": #判斷是不是密碼的密文
    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系統')