【Python教程】06.類與單元測試

大綱

面向對象

面向對象編程是一種程序設計思想。
咱們到目前爲止所編寫的程序,都是基於面向過程的。
image.pngpython

面向過程就是咱們在寫程序過程當中,將需求拆成多個流程,每一個封裝一個方法,逐個調用。
好比買菜洗菜炒菜裝盤這四個步驟,咱們對於的方法就是buywashcookdish
若是需求有變化,如我還要買米、作飯,用鐵鍋炒菜,用微波爐作菜等等。這時候用面向過程就很是麻煩且會愈來愈亂。git

面向對象則是從另外一個角度來思考,它用對象來取代方法做爲程序的基礎
對象是對事物的抽象,如人能夠抽象成名字、年齡以及能作的事情,這就是
咱們在程序設計時,就只要建立這個對象,讓它來作它能作的事情
仍是買菜洗菜炒菜裝盤這個四個步驟,如今咱們就能夠有超市廚師兩個對象。超市負責買菜,廚師負責洗菜、炒菜、裝盤。
進一步廚師用什麼洗菜,用什麼作菜怎麼作、怎麼裝盤又是一系列的對象來處理。
超市這個對象提供買米買菜買肉等等功能,必要時還能夠繼續添加
除了基本的功能外,沃爾瑪和永輝賣的東西就有差異,即便一樣是賣米也會有差別。
這就是繼承,沃爾瑪和永輝都是超市的繼承,但並非相同的。github

class】就是對對象的描述
咱們要把人的名字和年齡以及作的事情用python的類寫出來。編程

建立一個類class
咱們建立一個user包,在包裏建立一個Person模塊。在模塊中編寫Person類。
image.png框架

class Person:
    def __init__(self):
        pass

class後面就是類名Person,其下的代碼塊就是類的定義。類名使用駝峯寫法
一個文件一個類,大部分狀況咱們會這樣作。Python中並不反對一個文件多個類
第一個在類中要定義的方法就是構造方法,方法名爲__init__。這是類默認存在的方法。
構造方法裏的第一個參數是self,表明對象本身默認類的方法第一個參數都是self單元測試

實例化:將類建立出來進行使用。
咱們建立一個main.py來實例化這個類。
image.png測試

from user import Person
p1 = Person.Person()
from user.Person import Person
p1 = Person()

上面兩種方式是在包下的類實例化方式,與方法的調用時同樣的。
對於類,因爲一文件一類的原則,咱們更但願能簡單一點調用到包裏的類。以下:spa

from user import Person
p1 = Person()
p2 = Person()

那麼咱們就要在包的__init__.py文件裏作文章了,寫入設計

from .Person import Person

而後咱們就能夠直接從包導入Person類了。3d

構造方法:用於初始化,每一個類被實例化時會默認調用的方法。

def __init__(self, name, age):
    self.name = name    # 名字
    self.age = age      # 年齡

咱們加入Person類有的數據,名字和年齡。
self表明本身,它出來的變量稱爲成員變量
self.nameself.age分別被賦值給構造方法傳入的參數name和age,用於初始化

實例化並獲取成員變量

p1 = Person('Green', 12)
print(p1.name, p1.age)

使用構造方法進行建立,而後直接用建立好的變量,點出成員變量便可。
成員變量原則上只能在構造方法聲明

成員方法:構造方法實際上是一個特殊的成員方法。

def sleep(self, t):
    print('{} sleeps for {} seconds'.format(self.name, t))

定義了一個sleep方法,一樣的第一個參數self,表明該方法是成員方法。
在成員方法內部,咱們就能夠用self來得到成員變量
調用:一樣使用的方式調用該方法

p2 = Person('Lucy', 14)
p2.sleep(10)

這時若是咱們須要在sleep前添加一步到牀上的步驟,咱們能夠這樣作,
添加一個go2bed成員方法,並在sleep方法中用self調用這個成員方法

class Person:

    def __init__(self, name, age):
        self.name = name    # 名字
        self.age = age      # 年齡
        
    def sleep(self, t):
        self.go2bed()
        print('{} sleeps for {} seconds.'.format(self.name, t))
        
    def go2bed(self):
        print(self.name + ' go to bed.')

訪問限制:對成員變量和成員方法進行限制,不讓使用者能夠直接使用。
成員變量做爲一個類的數據,不該該能夠直接被使用者修改,保證類被正確使用。
變量名用 兩個下劃線__】開頭的就是被限制訪問私有變量,只可內部訪問

def __init__(self, name, age):
    self.__name = name    # 名字
    self.__age = age      # 年齡

調用時報錯
image.png

若是須要獲取這兩個變量的值,或進行修改,咱們定義getset方法進行。

def get_age(self):
    return self.__age
    
def set_age(self, age):
    self.__age = age
    
def get_name(self):
    return self.__name

咱們但願age能夠進行設置,而name沒有set方法。這樣成員變量的任何修改都在咱們的控制範圍內。

成員方法也使用兩個下劃線來作限制。
__go2bed就是隻能內部訪問的方法了。

class Person:
    
    def __init__(self, name, age):
        self.__name = name    # 名字
        self.__age = age      # 年齡
    
    def sleep(self, t):
        self.__go2bed()
        print('{} sleeps for {} seconds.'.format(self.__name, t))
        
    def __go2bed(self):
        print(self.__name + ' go to bed.')
        
    def get_name(self):
        return self.__name
        
    def get_age(self):
        return self.__age

主要目的是:
一、不但願使用者調用到。
二、對用戶提供的方法也能夠叫作接口。接口就是使用者能夠調用到的全部方法了。這樣對於使用者來講也是最好的。

繼承

繼承是在已有類【父類】的基礎上,再編寫一個子類,其擁有父類的全部內容。

from user import Person
class Student(Person):
    pass

在類名Student後面用括號抱起來的Person就是父類。
此時這個子類沒有任何的代碼,可是它已經繼承了父類全部的內容,使用方法同樣。

from user import Student
student1 = Student('Green', 12)
print(student1.get_name())

全部方法變量自動繼承。

object類:默認的Python繼承類。
沒有作繼承的類,其實都默認繼承了python的object類【對象類】。

class Person(object):

object能夠不寫。

添加成員變量和方法:子類通常擁有比父類更強大的功能。
在Students類中,咱們添加一個grade變量來標識年級,並添加一個新方法。

class Student(Person):

    def __init__(self, name, age, grade=1):
        Person.__init__(self, name, age)    # 父類構造方法
        self.__grade = grade                # 年級
    
    def get_grade(self):
        return self.__grade

代碼中的Person.__init__表示了咱們在新的構造方法中先調用了Person父類構造方法
這樣咱們就減小了重複的代碼。

複寫成員方法修改父類的方法內容。
方法的複寫必須保證方法名參數父類一致

def sleep(self, t):
    print('student ' + self.get_name())
    Person.sleep(self, t)           # 父類方法

經過調用父類的方法來擴充該方法。或者直接不調用父類方法所有重寫也是能夠的。

def sleep(self, t):
    print('student {} sleeps for {} seconds.'.format(self.__name, t))

子類的方法覆蓋父類的方法,在被調用時就會運行子類方法。
複寫實現了面向對象中多態的概念。

此時咱們發現代碼報錯了,緣由在於__name變量是隻能Person類內部訪問的成員變量。
只能使用self.get_name()來得到名字,子類要如何調用父類的私有變量?

繼承中的訪問限制
兩個下劃線使得該成員變量和方法只能在這個類中內部使用。
在繼承中成員變量和方法除了不能被外部調用外,其子類應該是能夠正常使用的。
使用一個下劃線_】開頭定義的成員變量和方法就能夠實現。

def __init__(self, name, age):
    self._name = name    # 名字
    self._age = age      # 年齡

def _go2bed(self):
    print(self._name + ' go to bed.')

這樣剛纔的複寫方法中就可使用了。

def sleep(self, t):
    print('student {} sleeps for {} seconds.'.format(self._name, t))

訪問限制分類
public:公開權限,無下劃線【self.name】,任何地方均可以訪問修改。
protected:保護權限,單下劃線【self._name】,只容許本身和其子類訪問修改。
private:私有權限,雙下劃線【self.__name】,只容許本身訪問修改。
根據咱們的須要,選擇對應的權限進行開發。

多態

判斷變量類型typeisinstance

p1 = Person('Green', 12)
s1 = Student('Green', 12)
print(type(s1))
print(type(p1))

輸出:
image.png
type能夠得到該變量的類型

print(isinstance(p1, Person))
print(isinstance(p1, Student))

輸出:image.png
isinstance能夠直接判斷變量是否是某個類型
對於父類變量結果與下面的代碼相同:

print(type(p1) == Person)
print(type(p1) == Student)

但對於子類,狀況並不相同

子類的類型判斷

type(s1) == Person          # False
type(s1) == Student         # True
isinstance(s1, Person)      # True
isinstance(s1, Student)     # True

咱們發現子類變量在type的相等判斷中,並不等於父類。
但在isinstance判斷中,子類被判斷爲與父類同樣,這就是多態
一個類能夠是本身,也能夠是其父類,邏輯上也是通的,學生也是一我的。
反之則不對,一我的未必是學生。

多態的用處

def poly_test(person):
    if not isinstance(person, Person):
        raise Exception('數據類型傳入錯誤')
    return person.sleep(10)

這個方法裏,咱們對傳入的類型進行了限定。
這樣咱們就能夠直接傳入Student、Person或者其餘子類均可以。
sleep也是父類就有方法,直接調用不會有問題。
當咱們新增一個子類,如Programmer類,咱們就不須要對這個方法進行修改,直接使用。
根據對象的真實類型運行,就是多態的用處。

靜態

除了成員變量和成員方法,類中還能夠定義靜態屬性類變量類方法
靜態的意思是不須要實例化就可使用。

類變量的數據屬於全部實例共享一個。
直接建立在類的定義中與方法同級。調用時使用類名 . 變量名

class Programmer(Person):
    
    programmers = {}    # 靜態變量
    
    def __init__(self, name, age):
        Person.__init__(self, name, age)
        if name not in Programmer.programmers:
            Programmer.programmers[name] = 0
        Programmer.programmers[name] += 1

這裏作了一個統計,統計同一姓名的人有幾個。每建立一個實例就統計一次。

from user import Programmer
p = Programmer('Green', 18)
print(Programmer.programmers)

類方法是直接屬於類的方法。
定義時,與成員方法不一樣的是第一個參數不是self而是cls【這個類】。並須要在方法上添加一個裝飾器

@classmethod
def count_by_name(cls, name):
    return cls.programmers[name]

@開頭並寫在def上面一行的就是裝飾器,具體內容之後的高階語法再介紹。
@classmethod就定義了下面的方法是一個類方法,且沒有self參數,由於沒有實例化。
第一參數是cls,表明了當前類,能夠經過cls調用類變量,至關於Programmer.programmers
使用時直接經過類名 . 方法調用,無需實例化

count = Programmer.count_by_name('Green')

靜態方法與類方法相似,但更爲獨立,僅僅表現爲放在類裏面託管的方法。
定義時,沒有self參數也沒有cls參數。下面是上個方法的靜態方法版本。

@staticmethod
def count_by_name(name):
    return Programmer.programmers[name]

@staticmethod就定義了下面的方法是一個靜態方法。其餘都與類方法同樣。
使用上,靜態方法通常與類沒有什麼關係,不會涉及類的操做,是一個比較獨立的方法。

繼承問題:類變量與子類共享,父類沒有該類變量。
類方法一樣能夠繼承和複寫,有複寫時要調用這個類的類方法。
靜態方法也同樣。

單元測試

單元測試是一個測試框架
咱們直接來看一個簡單的示例代碼。

import unittest         # 單元測試框架
from user import Student    # 要測試的類


class TestStudentMethods(unittest.TestCase):

    def test_student(self):         # 測試用例
        s = Student('Green', 12)
        self.assertEqual(12, s.get_age())
        self.assertEqual('Green', s.get_name())

if __name__ == '__main__':  # 看成爲啓動文件時
    unittest.main()

unittest是要引入的包。建立一個類繼承unittest.TestCase類。而後其餘方法就不須要了。
咱們直接開始寫測試用例。以test開頭的成員方法就是一個用例
定義完類後,咱們啓動該測試unittest.main()
前面的if語句判斷了該文件是否啓動文件,也是一種經常使用的方法。
啓動後,單元測試會自動執行全部用例,並給出結果。

測試用例

def test_student(self):         # 測試用例
    s = Student('Green', 12)
    self.assertEqual(12, s.get_age())
    self.assertEqual('Green', s.get_name())

先進行一些操做代碼,就是咱們的用例過程,這裏建立了一個Student對象。
而後就要使用單元測試類給咱們提供的assert系列方法來判斷結果是否正確。
assertEqual是最經常使用的方法之一了,參數1寫入指望值,參數2寫入要驗證的數據
在運行過程當中就會進行對比,若是二者相等則正確,不等則錯誤。
經常使用的assert方法還有:
assertNotEqualassertTrue 、assertFalseassertIsNoneassertIsNotNoneassertIn等。
一個單元測試能夠包含不少測試用例,一次進行測試。

測試結果:全部assert都正確的狀況下,即測試經過。
image.png
有沒有經過的用例時,會告訴你哪一個用例的哪一個assert有問題,與指望值不符。
image.png

練習

一、模擬一個小遊戲,編寫一個Sprite類【擁有血量和攻擊力兩個屬性,並進行攻擊】。
而後繼承出Monster類和Hero類,讓二者每回合互相攻擊一次,直到有一方死亡。
每次攻擊扣除對方的血量是攻擊力加減N的隨機值。
二、編寫一個進行接口請求的單元測試。


github: https://github.com/lvancer/course_python

相關文章
相關標籤/搜索