約束和異常處理

AF1y發表於2018-11-12

本節主要內容:

1.類的約束

2.異常處理

3.自定義異常

4.日誌

一.類的約束

⾸先, 你要清楚. 約束是對類的約束.  比如. 現在. 你是一個項⽬經理. 然後呢. 你給手下 的人分活. 張三, 你處理一下普通使用者登入,

李四, 你處理一下會員登入, 王五, 你處理一下管理員登入. 那這個時候呢. 他們就開始分別取寫他們的功能了了. 但是呢. 你要知道,

程式設計師不一定會有那麼好的默契. 很有可能三個人會寫完全三個不同的方法. 就比如這樣:

# 貼吧
# 專案經理(級別高一點兒)
class Base:
    def login(self): # 強制子類做xxxx事
        raise NotImplementedError("子類沒有實現該方法") # 報錯. 拋異常

# 1. 普通賬號  -->  張三
class Normal(Base):
    def login(self):
        print("普通賬號的登入")

# 2. 吧務  - > 李四
class Member(Base):
    def login(self):
        print("吧務的登入")

# 3. 百度員工  -> 王五
class Admin(Base):
    def login(self): # 方法的覆蓋和重寫
        print("管理員的登入")

# 專案經理
def wodetian(obj):
    obj.login()

n = Normal()
wodetian(n)

m = Member()
wodetian(m)

a = Admin()
wodetian(a)

 

 然後呢, 他們把這樣的程式碼交給你了.  你看一眼. 張三和王五還算OK 這個李四寫的是 什麼鬼?  denglu…….難受不.

但是好歹能用. 還能湊合. 但是這時. 你這邊要使用了. 問題就來了. 

對於張三和王五的程式碼. 沒有問題. 但是李四的. 你是不是呼叫不了. 那如何避免這樣的 問題呢?  我們要約束程式的

結構. 也就是說. 在分配任務之前就應該把功能定義好. 然後分別交給底下的程式設計師來完成相應的功能. 

 

約束的作用:規範程式碼,約束是對類的約束

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

1.提取父類,然後在父類中定義好辦法.在這個方法中什麼都不用幹,就丟擲一個異常就可以了,這樣所有的子類就必須重寫這個方法.

否則,訪問的時候就會報錯.

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

 

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

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 login(obj):
    print("準備驗證碼...")
    obj.login()
    print("進入主頁...")

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

 

 在執行到login(a)的時候程式會報錯. 原因是, 此時訪問的login()是父類中的方法. 但是父類中的方法會丟擲一個異常. 所以報錯.

這樣程式設計師就不得不寫login方法了. 從而對子類進行了相應的約束.

在本示例中. 要注意. 我們丟擲的是Exception異常. 而Exception是所有異常的根. 我們無法通過這個異常來判斷出程式是因為什麼

報的錯. 所以. 最好是換一個比較專業的錯誤資訊. 最好是換成NotImplementError. 其含義是. “沒有實現的錯誤”. 這樣程式設計師或者項

⽬經理理可以一目了然的知道是什麼錯了. 就好比. 你犯錯了. 我就告訴你犯錯了. 你也不知道哪里錯了. 這時我告訴你, 你xxx錯了.

你改也好改不是? 

第二套方案: 寫抽象類和抽象方法. 這種方案相對來說比上一個麻煩一些. 需要給大家先引入一個抽象的概念我們如果寫了一個方法,

不知道方法的內部應該到底寫什麼.那這個方法其實就應該是一個抽象的方法.如果一個類中包含抽象方法,那麼這個類一定是抽象類

抽象類是不能有例項物件的.建立物件的時候會報錯.

 

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

from abc import ABCMeta,abstractmethod
# 類中包含了抽象方法,那此時這個類就是個抽象類.注意:抽象類可以有普通方法
class Animal(metaclass=ABCMeta):
    @abstractmethod
    def chi(self):
        pass
# 抽象類不能建立物件
class Dog(Animal):
    # 子類必須實現父類中的抽象方法,否則子類也是抽象類
    def chi(self):
        print("躺著吃")

class Cat(Animal):
    def he(self):
        print("喝水")

d = Dog()
d.chi()
c = Cat()
c.he()

 

通過程式碼我們能發現,這裡的父類Animal對子類Dog和Cat進行了約束

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

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

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

二.異常處理

異常:程式在執行過程中產生的錯誤.

def cul(a,b):
    return a/b

ret = cul(10/0)
print(ret)

結果:

Traceback (most recent call last):
  File "D:/python課件及作業/約束/121.py", line 4, in <module>
    ret = cul(10/0)
ZeroDivisionError: division by zero

 

什麼是錯誤,除法中除數不能是0.那如果真的出了這個錯.我們不可能吧一堆錯誤資訊拋給客戶,那該如何處理?

def cul(a,b):
    return a/b

try:
    ret = cul(10/0)
    print(ret)
except Exception as e:
    print("除數不能是0")

 

try..except的作用就是當程式執行時出現了錯誤,就執行except後面的程式碼.在和這個過程中.當程式碼出現錯誤的時候,

系統會產生⼀個異常物件. 然後這個異常會向外拋. 被except攔截. 並把接收到的異常物件賦值給e. 那這里的e就是

異常物件. 那這里的 Exception是什麼? Exception是所有異常的基類, 也就是異常的根. 換句話說. 所有的錯誤都是

Exception的子類物件. 我們看到的ZeroDivisionError 其實就是Exception的子類. 那這樣 寫好像有點兒問題. Exception

表示所有的錯誤. 太籠統了了. 所有的錯誤都會被認為是Exception. 當程式中出現多種錯誤的時候, 就不好分類了了, 最

好是出什麼異常就⽤用什麼來處理. 這樣就更加合理了. 所以在try…execpt語句中. 還可以寫更多的except.

完整的異常處理寫法(語法):


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

 

解讀:程式先執行操作,然後如果出錯了會走except中的程式碼.如果不出錯,執行else中的程式碼.不論出不出錯,最後都要

執行finally中的語句,一般我們用try…except就夠用了.頂多加上finally.finally一般用來作為收尾工作.

 

以上是處理異常,我們在執行程式碼的過程中如果出現了一些條件上的不對等.根本不符合我的程式碼邏輯.比如,引數.我要求

傳遞的是一個數字,而客戶非得傳遞一個字串.那我們該如何處理來通知客戶呢?

方案一:直接返回即可.

方案二:丟擲一個異常.

那如何丟擲異常呢?我們要用到關鍵字raise

def add(a,b):
    """
    傳遞兩個整數,求和
    :param a: 
    :param b: 
    :return: 
    """
    if not type(a) == int or not type(b) == int:
        #當程式執行到這句話的時候,正割函式的呼叫就會被中斷,並向外丟擲一個異常
        raise Exception("不是整數,無法求和")
    return  a + b

# 如果呼叫方不處理異常,那產生的錯誤將會繼續向外拋,最後就拋給了使用者
#  如果呼叫方處理了異常. 那麼錯誤就不會丟給使用者. 程式也能正常執行
try:
    add("胡辣湯",1)
except Exception as e:

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

如果呼叫方處理了異常. 那程式可以正常的進行執行.

 

三.自定義異常

自定義異常:非常簡單,只要你的類繼承了Exception類,那你的類就是一個異常類.

import traceback

class GenderError(Exception):
    pass

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

    def goto_WC(self):
        if self.gender == "":
            print("進來吧")
        else:
            raise  GenderError("錯了,不可以進來")

try:
    p1 = Person("Andy","")
    p1.goto_WC()
    p2 = Person("Amy","")
    p2.goto_WC()
except GenderError as e:
    val = traceback.format_exc()
    print("你不是男的,別來啊")
    print(val)
except Exception as e:
    print("其他錯誤)

結果:

進來吧
你不是男的,別來啊
Traceback (most recent call last):
File “D:/python課件及作業/約束/約束.py”, line 83, in <module>
p2.goto_WC()
File “D:/python課件及作業/約束/約束.py”, line 77, in goto_WC
raise GenderError(“錯了,不可以進來”)
GenderError: 錯了,不可以進來

 

我們在除錯的時候最好是能看到錯誤院子哪裡,那怎麼辦?

上面的程式碼引入了另一個模組traceback,這個模組可以獲取到我們每個方法的呼叫資訊.又被稱為堆疊資訊,

這個資訊對我們拍錯是很有幫助的

四.日誌

在編寫任何一款軟體的時候,都會出現各種各樣的問題或者bug,這些問題或者bug一般都會在測試的時候處理掉.

但是多多少少的都會出現一些意想不到的異常或者錯誤.那這個時候,我們是不知道哪裡出現了問題.因為很多都

不是必現的bug.如果是必現的,測試的時候肯定能測出來.最頭疼的就是這種不必現的bug.自己執行沒有問題,但

是到客戶那一用就出問題.那怎麼辦?我們需要給軟體準備一套日誌系統.當出現任何錯誤的時候.我們都可以去日

志系統裡去檢視.看哪裡出了問題.這樣解決問題和bug的時候就多了一個幫手.那如何在python中建立這個日誌系

統呢?很簡單:

1.匯入logging模組.

2.簡單配置一下logging

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

import logging
#filename:檔名
# format:資料的格式化輸出.最終在日誌檔案中的樣子
#       時間-名稱-級別-模組: 錯誤資訊
# datefmt:時間的格式
# level:錯誤的級別權重,當錯誤的級別權重大於等於leval的時候才會寫入檔案
logging.basicConfig(filename="x1.log",format=`%(asctime)s - %(name)s - %(levelname)s -%(module)s:`
                                             `%(message)s`,datefmt=`%Y-%m-%d %H:%M:%S`, level=10)
#當前配置表示10以上的分數會被寫入日誌檔案
# critical = 50
# fatal = critical
# error = 40
# warning = 30
# warn = warning
# info = 20
# debug = 10
# notest = 0
logging.critical("我是critical")  # 50分.最貴的
logging.error("我是error")    # 40分
logging.warning("我是warning")    # 警告 30分
logging.info("我是基本資訊")  # 20
logging.debug("我是測試")   # 10
logging.log(2,"我是自定義")  # 自定義,看著給分

在上面這個模板的基礎上做個簡單的測試,應用下

import traceback
class JackError(Exception):
    pass

for i in range(10):
    try:
        if i % 3 == 0:
            raise FileNotFoundError("檔案不在啊")
        if i % 3 == 1:
            raise KeyError("鍵錯了")
        if i % 3 == 2:
            raise JackError
    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("日誌1",level=logging.ERROR)
logger1.addHandler(file_Handler)

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

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

logger2 = logging.Logger("日誌2",level=logging.ERROR)
logger2.addHandler(file_Handler)