引子
假設我們要在我們的程式裡表示狗,狗有如下屬性:名字、品種、顔色。那麼可以先定義一個模闆,然後調用這個模闆生成各種狗。
def dog(name,d_type,color):
data = {
'name':name,
'd_type':d_type,
'color':color}
return data
d1 = dog('小七','拉布拉多','黃')
d2 = dog('旺财','中華田野犬','黃')
上面用函數作為模闆來定義資料确實很友善。然後我們要讓狗叫,那麼還需要在寫一個程式,簡單點,比如是這樣的
def dog(name,d_type,color):
data = {
'name':name,
'd_type':d_type,
'color':color}
return data
d1 = dog('小七','拉布拉多','黃')
d2 = dog('旺财','中華田野犬','黃')
def bark(d):
print("dog %s:Wang~Wang~Wang~~~"%d['name'])
bark(d2)
貌似也很完美,但是如果這是bark不小心引用了别的參數,比如一隻貓,那麼問題就來了。
def dog(name,d_type,color):
data = {
'name':name,
'd_type':d_type,
'color':color}
return data
d1 = dog('小七','拉布拉多','黃')
d2 = dog('旺财','中華田野犬','黃')
def cat(name,c_type,color):
data = {
'name':name,
'c_type':c_type,
'color':color}
return data
c1 = cat('比格沃斯','波斯貓','白') # 在定義一個貓
def bark(d):
print("dog %s:Wang~Wang~Wang~~~"%d['name'])
def miaow(c):
print("cat %s:Miao~Miao~Miaow~~~"%c['name'])
bark(c1) # 這裡是不是亂套了
miaow(d1)
是以這裡的問題就是,我們希望bark隻能調用狗,cat隻能調用貓。當然可以加上if判斷,但是很low。于是可以改成這樣:
def dog(name,d_type,color):
def bark():
print("dog %s:Wang~Wang~Wang~~~"%name)
data = {
'name':name,
'd_type':d_type,
'color':color,
'action':bark} # 把動作的函數寫到狗的函數裡,把函數名作為傳回字典裡的一個元素
return data
d1 = dog('小七','拉布拉多','黃')
d2 = dog('旺财','中華田野犬','黃')
def cat(name,c_type,color):
def miaow():
print("cat %s:Miao~Miao~Miaow~~~"%name)
data = {
'name':name,
'c_type':c_type,
'color':color,
'action':miaow}
return data
c1 = cat('比格沃斯','波斯貓','白')
d1['action']() # 這樣調用狗模闆裡的狗叫函數
c1['action']()
把狗叫的函數也寫到狗的模闆裡去,然後就實作了隻能由狗調用狗叫的需求了。
引子講完了,上面并不是面向對象。上面的問題也不是很複雜,但是如果問題再複雜可能就解決不下去了,我們需要面向對象來解決這類問題。
面向過程 VS 面向對象
程式設計範式
程式設計是程式員用特定的文法+資料結構+算法組成的代碼來告訴計算機如何執行任務的過程,實作一個任務的方式有很多種不同的方式,對這些不同的程式設計方式的特點進行歸納總結得出來的程式設計方式類别,即為程式設計範式。兩種最重要的程式設計範式分别是面向過程程式設計和面向對象程式設計,然後還有一個函數式程式設計。
面向過程程式設計(Procedural Programming)
面向過程程式設計,就是程式從上到下一步步執行,一步步從上到下,從頭到尾的解決問題。基本設計思路就是程式一開始是要着手解決一個大的問題,然後把一個大問題分解成很多個小問題或子過程,這些子過程再執行的過程再繼續分解直到小問題足夠簡單到可以在一個小步驟範圍内解決。
這樣做的問題也是顯而易見的,就是如果你要對程式進行修改,對你修改的那部分有依賴的各個部分你都也要跟着修改,随着程式越來越大,這種程式設計方式的維護難度會越來越高。
但是面向過程依然是有可取之處的,如果你隻是寫一些簡單的腳本,去做一些一次性任務,用面向過程的方式是極好的,但如果你要處理的任務是複雜的,且需要不斷疊代和維護的,那還是用面向對象最友善了。
面向對象程式設計(Object-Oriented Programming)
OOP程式設計是利用“類”和“對象”來建立各種模型來實作對真實世界的描述,使用面向對象程式設計的原因一方面是因為它可以使程式的維護和擴充變得更簡單,并且可以大大提高程式開發效率。另外,基于面向對象的程式可以使它人更加容易了解你的代碼邏輯,進而使團隊開發變得更從容。
面向對象程式設計介紹
無論用什麼形式來程式設計,我們都要明确記住以下原則:
- 寫重複代碼是非常不好的低級行為
- 你寫的代碼需要經常變更
面向對象的核心特性
- Class 類
- Object 對象
- Encapsulation 封裝
- Inheritance 繼承
- Polymorphism 多态
Class 類 和 Object 對象
類:一個類即是對一類擁有相同屬性的對象的抽象、藍圖、原型。在類中定義了這些對象的都具備的屬性(variables(data))、共同的方法。
對象:一個對象即是一個類的執行個體化後執行個體,一個類必須經過執行個體化後方可在程式中調用,一個類可以執行個體化多個對象,每個對象亦可以有不同的屬性。
首先要定義類,然後将類執行個體化,最後通過這個執行個體來調用類中的功能
這裡類名要用大駝峰規範來命名
# class是定義類,Dog是類名,括号中的object暫時知道是必填就好了
class Dog(object):
print("Hello , I am a dog.")
# 上面已經定義好了類,下面來将類執行個體化
d = Dog() # 但是這裡會把print列印出來
我們不希望在執行個體化的時候就把print列印出來,而是要在調用類中的功能的時候再列印,那麼上面的類還得修改一下。
class Dog(object):
# 定義一個函數,把print寫到函數裡,避免被直接調用執行
def sayhi(self):
print("Hello , I am a dog.")
d = Dog() # 這是這裡就不會直接列印了
d.sayhi() # 這樣來調用執行類裡的sayhi
d = Dog()
這步叫執行個體化。先去執行個體化,然後
d.sayhi()
再去調用它的功能
上面的例子我們沒有傳入參數,現在把引子裡的例子改成類,傳入屬性:
class Dog(object):
# 參數要寫在__init__這個函數裡,這個叫構造函數或構造方法
def __init__(self,name,d_type,color):
self.name = name # 将傳入的傳輸傳給self
self.type = d_type
self.color = color
# 下面的函數叫做類的方法
def bark(self):
print("dog %s: Wang~Wang~Wang~~~"%self.name) # 這裡就可以調用self裡的值了
d1 = Dog('旺财','中華田野犬','黃') # 先執行個體化
d1.bark() # 然後調用功能
self,就是執行個體本身。你執行個體化時python會自動把這個執行個體本身通過self參數傳進去。上面例子中的self就是d1。再通俗一點,self就是調用目前方法的對象。
執行個體化後産生的對象,就叫執行個體,是這個類的執行個體。
d1 = Dog('旺财','中華田野犬','黃')
這個就是進行執行個體化,産生了Dog這個類的一個執行個體d1,而self就是這個執行個體本身
上面的
__init__
叫做構造函數,或者叫構造方法,也就是初始化的方法
bark
函數叫做類的方法,我們可以根據需要寫多個方法
我們寫一個給狗吃東西的函數food,把食物通過參數傳入
再寫一個給狗改名字的函數,這裡牽涉到修改對象的屬性值,也就是初始化的内容可以後期修改
再寫一個自報名字的函數,看看改名的效果:
class Dog(object):
def __init__(self,name,d_type,color):
self.name = name
self.type = d_type
self.color = color
def bark(self):
print("dog %s: Wang~Wang~Wang~~~"%self.name)
def eat(self,food):
print("%s 正在吃 %s"%(self.name,food))
def rename(self,new_name):
"給狗改名"
print("%s 改名為 %s"%(self.name,new_name))
self.name = new_name # 這裡改變了對象的屬性
def say_name(self):
"報名字"
print("我的名字是:%s"%self.name)
d1 = Dog('旺财','中華田野犬','黃')
d1.bark()
d1.eat("骨頭") # 把骨頭傳給了food
d1.say_name() # 現在的名字
d1.rename("小黃") # 改名
d1.say_name() # 再看看名字變了沒
總結:
類 ==》 執行個體化 ==》 執行個體(對象):類經過執行個體化後變成了執行個體也就是對象
__init__
:構造函數
self.name = name
:屬性,或者叫成員變量、字段
def bark(self)
:方法,或者叫動态屬性
封裝,也就是把客觀事物封裝成抽象的類,并且類可以把自己的資料和方法隻讓可信的類或者對象操作,對不可信的進行資訊隐藏
私有屬性
之前使用的屬性并不是公有屬性,而是叫成員屬性,這些屬性是隻屬于它的對象的。公有屬性最後講。
私有屬性在類的外部不不可通路的。我們來增加一個私有屬性hp,然後寫一個函數來操作hp這個私有屬性。定義私有屬性使用
self.__屬性名
class Cat(object):
def __init__(self,name,c_type,color,hp=100):
self.name = name
self.c_type = c_type
self.color = color
self.__hp = hp # 定義為私有屬性
def be_hit(self,damage):
"造成傷害,扣除hp值"
print("%s 受到了 %d點 傷害"%(self.name,damage))
print("目前hp:%d,傷害:%d,剩餘hp:%d"
%(self.__hp,damage,self.__hp-damage))
self.__hp -= damage
c1 = Cat('比格沃斯','波斯貓','白')
print("name:",c1.name) # 公有屬性可以正常擷取到
#print(c1.__hp) # 這句會報錯,類的外部是擷取不到私有屬性的
c1.be_hit(10) # 這裡是通過類内部的方法列印的私有屬性的數值
c1.be_hit(5)
既然可以通過内部方法通路私有屬性,我們可以将私有屬性寫到函數裡return,提供一個給外部通路的方法,但是不能修改。另外其實也是有方法可以強制通路的。
class Cat(object):
def __init__(self,name,c_type,color,hp=100):
self.name = name
self.c_type = c_type
self.color = color
self.__hp = hp
def be_hit(self,damage):
print("%s 受到了 %d點 傷害"%(self.name,damage))
print("目前hp:%d,傷害:%d,剩餘hp:%d"
%(self.__hp,damage,self.__hp-damage))
self.__hp -= damage
def get_hp(self):
"提供方法,傳回私有屬性hp的值"
return self.__hp
c1 = Cat('比格沃斯','波斯貓','白')
print(c1.get_hp()) # 通過内部提供的方法來通路私有屬性
print(c1._Cat__hp) # 其實也可以強制通路到私有屬性,并這個是可以修改私有屬性值的
對象名._類名__私有屬性名
: 強制通路私有屬性,可讀寫
公有屬性
公有屬性,所有屬于這個類的對象都可以通路的屬性,才叫公有屬性。
之前的例子中,類Dog定義了2個對象d1 和 d2 ,但是d1裡的屬性是隻屬于d1的,無法通過d2來通路,這些都是叫成員屬性
在類裡直接定義的屬性,既公有屬性。
class Dog(object):
called = "狗" # 在類裡定義公有屬性
def __init__(self,name,d_type,color):
self.name = name
self.type = d_type
self.color = color
d1 = Dog('旺财','中華田野犬','黃')
d2 = Dog('小七','拉布拉多','黃')
print(d1.called)
print(d2.called)
Dog.called = "犬" # 通過類更改類公有屬性
print(d1.called) # 通過類修改,所有對象都會變
print(d2.called)
d1.called = "看門狗" # 通過對象更改,其實這句是建立了d1裡的成員屬性
# 同時有成員屬性和公有屬性,則使用成員屬性
print(d1.called) # 這裡列印的是剛才建立的成員屬性
print(d2.called) # 這裡還是原來的公有屬性
Dog.called = "dog" # 再通過類更改公有屬性
print(d1.called) # 這裡沒有變化,因為成員屬性還在,并且沒變
print(d2.called) # 這裡變化了,因為這裡隻有公有屬性
del d1.called # 把成員屬性從記憶體中清楚
print(d1.called) # 現在全部是公有屬性值了
print(d2.called)
d1.called = "看門狗"
d2.called = "導盲犬" # 現在全部都有成員屬性了
print(d1.called)
print(d2.called)
print(Dog.called) # 直接通過類而不是對象擷取的一定是公有屬性
這裡公有屬性和成員屬性的情況和之前學的全局變量和局部變量是一樣的效果。
展開講一下函數的情況,類中def定義的所有函數我們也可以了解為是公有的。那麼也可以通過定義成員的方法來替換原來的公有的方法:
class Dog(object):
called = "狗"
def __init__(self,name,d_type,color):
self.name = name
self.type = d_type
self.color = color
def bark(self):
print("dog %s: Wang~Wang~Wang~~~"
%self.name)
'''
在類外面再定義一個函數,這裡無法使用self傳入變量了
如果要傳入參數,那麼下面調用的時候也得帶參數傳入,這樣調用方法也變了
'''
def bark():
"自己再定義一個bark"
print("TEST")
d1 = Dog('旺财','中華田野犬','黃')
d2 = Dog('小七','拉布拉多','黃')
d1.bark() # 正常調用類中的方法
d2.bark()
d1.bark = bark # 通過這個方法來實作給d1一個自己的成員方法
d1.bark() # 現在調用的不是公有方法,而是d1自己的成員方法
d2.bark()
del d1.bark # 清除d1的bark方法後,d1有可以正常調用類的bark方法了
d1.bark()
d2.bark()
析構方法 和 構造方法
方法和函數:在類裡面定義的函數就是這個類的方法,是以方法和函數這兩個詞有時候會混用,主要看你是在描述什麼東西
構造方法:之前在為類傳入參數的時候用到了構造函數,構造函數其實就是在生成1個執行個體的時候自動運作的函數,是以通過構造函數我們可以實作在生成執行個體的時候自動把參數傳遞給self
析構方法:和構造方法差不多,就是在一個執行個體被銷毀的時候自動運作的函數
class test(object):
def __init__(self): # 構造函數
print("init in the test")
def __del__(self): # 析構函數
print("del in the test")
input("準備執行個體化對象")
obj = test()
input("準備銷毀對象")
del obj
input("執行完畢")
把一些收尾的工作寫在析構函數裡,在你銷毀這個對象的時候就自動執行,比如關閉所有的客戶連接配接、關閉所有打開的檔案等等。具體怎麼用得到了以後用的時候才知道了。
繼承是指這樣一種能力:它可以使用現有類的所有功能,并在無需重新編寫原來的類的情況下對這些功能進行擴充。
通過繼承建立的新類稱為“子類”或“派生類”。
被繼承的類稱為“基類”、“父類”或“超類”。
繼承的過程,就是從一般到特殊的過程。
要實作繼承,可以通過“繼承”(Inheritance)和“組合”(Composition)來實作。
要同時繼承多個類有2種方法:多重繼承和多級繼承
繼承概念的實作方式主要有2種:實作繼承、接口繼承。
- 實作繼承是指使用基類的屬性和方法而無需額外編碼的能力
- 接口繼承是指僅使用屬性和方法的名稱,但是子類必須提供實作的能力(子類重構父類方法)
抽象類:僅定義将由子類建立的一般屬性和方法。
OOP開發範式大緻為:劃分對象→抽象類→将類組織成為階層化結構(繼承和合成) →用類與執行個體進行設計和實作幾個階段。
簡單的繼承例子:
class Human(object):
def talk(self):
print("Hello World")
class Chinese(Human): # 這樣就繼承了Human這個類
pass # 什麼也不寫,但是我們有繼承啊
h1 = Chinese() # 執行個體化1個chinese的對象
h1.talk() # 雖然Chinaes沒有talk方法,但是繼承了Human的talk方法
子類也可以有自己的方法,還可能重構父類的方法:
class Human(object):
def talk(self):
print("Hello World")
class Chinese(Human): # 這樣就基礎了Human這個類
"這次我們來寫幾個方法"
def greatWall(self): # 定義一個新的方法
print("長城長啊長")
def talk(self): # 重構父類的方法
print("你好,世界")
h1 = Chinese() # 執行個體化1個chinese的對象
h1.talk() # 調用的是重構後的新方法
h1.greatWall() # 在子類中定義的新方法
再加上類的參數:
先隻寫上父類的構造函數,子類不寫。子類就是完全繼承父類構造函數。
class Human(object):
def __init__(self,name,age): # 父類裡有構造函數
self.name = name
self.age = age
def talk(self):
print("Hello World")
class Chinese(Human):
pass # 子類沒有構造函數,就直接繼承父類的構造函數
#h1 = Chinese() # 沒有參數會報錯,因為構造函數需要傳入2個參數
h1 = Chinese("張三",33) # 執行個體化的時候,需要根據構造函數的要求傳入參數
h1.talk()
再來看子類的屬性,上面是完全繼承父類的屬性,那麼就不用寫構造函數。
也可以完全無視父類的屬性,那麼直接重構自己的構造函數就好了。
複雜一點情況,繼承父類的屬性,但是還要有自己的額外屬性。
class Human(object):
def __init__(self,name,age): # 父類的構造函數
self.name = name
self.age = age
def talk(self):
print("Hello World")
class Chinese(Human):
def __init__(self,name,age,kungfu): # 先繼承,再重構
#Human.__init__(self,name,age) # 調用父類的構造函數,實作繼承
super(Chinese,self).__init__(name,age) # 和上面那句效果一樣,用這種寫法
self.kungfu = kungfu # 再增加自己重構的屬性
def talk(self): # 這裡順便把函數也重構了,但是我們要保留父類裡的部分
Human.talk(self) # 方法也可以和屬性一樣,實作繼承和重構
print("你好,世界")
h1 = Chinese("張三",33,"少林") # 執行個體化的時候,需要根據構造函數的要求傳入參數
h1.talk()
print(h1.__dict__) # 順便來看一下這個的作用
子類要繼承父類屬性并且有自己的特有屬性,需要先繼承,再重構。通過父類的名字調用執行父類的構造函數,實作繼承,然後可以在後面寫上自己需要重構的代碼。
函數也是可以用這個方法來調用父類的方法,添加自己的代碼,實作在父類的基礎上重構自己特有的部分
__dict__
屬性:可以檢視對象所有的屬性和值,以字典的形式。
多繼承
class Chinese(Human):
這個是繼承的文法,括号中的變量可以傳入多個類,用逗号隔開就實作了多繼承。比如:
class Chinese(Human,Person):
上課說用的不多,也沒展開。是以暫時就知道這個文法就好了。
下面也是在多繼承的時候才會有差別的内容
新式類 和 經典類
python3裡已經沒有這個問題了,并且現在都是用的新式類的寫法。不過還是舉個例子說明一下繼承的順序,這個是多繼承的情況下會遇到的問題。
定義一個基類A,然後是A的兩個子類B和C。最後來個孫子類D,D要繼承B和C。每個類裡都定義一個屬性n,寫在構造方法裡
class A(object):
pass
def __init__(self):
self.n = "A"
class B(A):
pass
def __init__(self):
self.n = "B"
class C(A):
pass
def __init__(self):
self.n = "C"
class D(B,C):
pass
#def __init__(self):
#self.n = "D"
d1 = D()
print(d1.n)
如果D有構造方法,那麼結果一定是D。然後依次将上面的構造方法也注釋掉,看看D的繼承順序。結果是B-C-A。這個叫廣度查找。
經典類就不試了,要在python2裡才會有深度查找的效果。在python3裡還是廣度查找。
- 新式類,廣度查找 B-C-A
- 經典類,深度查找 B-A-C
文法上的差別,都用新式類就好了,經典類知道一下,看到的時候别不認識。
定義類的文法:
class A:
# 經典類寫法,python2裡和下面的寫法有差別。python3裡不必顯示聲明,這麼寫也是新式類了。
class A(object):
# 新式類寫法,咱就這麼寫
調用父類方法的文法:
Human.__init__(self,name,age)
# 經典類寫法,這個是子類構造函數裡實作先繼承的那句代碼
super(Chinese,self).__init__(name,age)
不單是構造函數,其他函數也一樣,盡量都super,不是多繼承的話兩個都一樣。但是絕對不要混用。
子類完全繼承父類的通用形式:
super(Son, self).__init__(*args, **kwargs)
# 這裡Son代指這個子類的類名。
這樣不需要知道父類裡有多少參數,總之是把所有父類的參數都繼承過來了。一般這句寫在子類的構造函數的開頭,然後後面再寫上子類需要在構造函數裡附加的内容。
多态,簡單點說:"一個接口,多種實作",指一個基類中派生出了不同的子類,且每個子類在繼承了同樣的方法名的同時又對父類的方法做了不同的實作,這就是同一種事物表現出的多種形态。
多态允許将子類的對象當作父類的對象使用,某父類型的引用指向其子類型的對象,調用的方法是該子類型的方法。這裡引用和調用方法的代碼編譯前就已經決定了,而引用所指向的對象可以在運作期間動态綁定
封裝可以隐藏實作細節,使得代碼子產品化;繼承可以擴充已存在的代碼子產品(類);它們的目的都是為了,代碼重用。而多态則是為了實作另一個目的,接口重用。多态的作用,就是為了類在繼承和派生的時候,保證使用“家譜”中任一類的執行個體的某一屬性時的正确調用。
舉例:Pyhon 很多文法都是支援多态的,比如 len(),sorted(), 你給len傳字元串就傳回字元串的長度,傳清單就傳回清單長度。
講了那麼多,到底是要通過多态做什麼?就是要,通過父類調子類,python不直接支援,但是可以間接支援。
class People(object): # 先定義一個基類
def talk(self): # 基類的talk方法,我們不希望被調用,寫一個會抛出錯誤的代碼
"如果基類的這個方法被調用,就抛出一個錯誤"
raise NotImplementedError("Subclass must implement abstract method")
class Chinese(People): # 這個是子類
def talk(self): # 重構talk方法
print("你好,世界")
class American(People):
def talk(self):
print("Hello World")
# 如果調用了基類的方法,會根據raise裡定義的,抛出一個錯誤。去掉下面的注釋測試一下
#p1 = People() # 執行個體化一個基類
#p1.talk() # 調用基類的talk方法
# 執行個體化2個對象
c1 = Chinese()
a1 = American()
# 通過子類調用自己的方法當然沒問題。要用多态就是要使用統一的接口來實作這2條指令
c1.talk()
a1.talk()
# 多态是要用父類調用子類
#People.talk(c1) # 這樣是最好的,真正的直接實作多态的方法,但是Python不支援
#People.talk(a1)
# 間接支援多态的方法,定義一個函數作為統一的接口
def People_talk(obj):
obj.talk()
# 用新定義的接口,調用不同的子類,每次用的都是這個子類裡重構的那個方法
People_talk(c1) # 傳入一個c1對象,實際就是執行c1.talk()
People_talk(a1) # 傳入一個a1對象,實際就是執行a1.talk()
間接支援多态,新定義一個函數,用參數傳入一個對象。然後再函數中調用這個對象的一個方法。
主動抛出一個錯誤,上面在基類裡使用了一個raise指令,可以實作主動觸發一個錯誤,可以定義這個錯誤的類型和消息。這裡的作用就是驗證這個方法沒有被調用到,貌似一般都是用一句print來驗證的,這個也很進階。
補充
面向對象的應用場景
學了那麼多面向對象,但是什麼時候用呢?畢竟在python裡我們使用面向過程的方法也是一樣可以實作的。課上總結了3種場景,推薦使用面向對象的方法來實作,并且确實更好。
- 根據一個模闆來建立某些東西的時候
- 縱向擴充
- 橫向擴容
第一條從前面引子開始就在舉例子了。後面兩個名詞用在這是我自己總結的。
縱向擴充,對一個對象有多個不同的操作,比如連接配接一個伺服器、執行一條指令、上傳一個檔案、斷開與伺服器的連接配接。把這種對同一個對象執行的不同的操作寫在一個類裡,每一種操作就是類裡的一個函數
橫向擴充,原本有很多個函數都需要傳公共的參數的時候,可以都寫到一個類裡。比如有很多個操作都需要和伺服器互動,那麼就都會需要位址、端口、密碼這些參數,然後不同的方法又需要不同的其他參數。每次定義函數以及之後調用函數都會重複的引用這幾個重複的參數。
# 面向過程定義3個函數,其中都會用到3個一樣的參數
def f1(host,port,pwd,arg1):
pass
def f2(host,port,pwd,arg1,arg2):
pass
def f3(host,port,pwd,arg1,arg2,arg3):
pass
# 調用的時候也要反複的來引用這些參數
f1(1,2,3,4)
f2(1,2,3,4,5)
f3(1,2,3,4,5,6)
# 面向對象來做同樣的事情,重複的參數寫到構造方法裡
class Foo(object):
def __init__(self,host,port,pwd):
self.host = host
self.port = port
self.pwd = pwd
def f1(arg1):
pass
def f2(arg1,arg2):
pass
def f3(arg1,arg2,arg3):
pass
# 調用的時候先把重複的參數寫在一個對象裡,然後可以分别調用這個對象的不同的方法
obj = Foo(1,2,3)
obj.f1(4)
obj.f2(4,5)
obj.f3(4,5,6)
類中的其他方法
類中的函數我們叫方法,預設在類中定義的函數都是儲存在類中,要調用這個方法需要通過對象。叫做執行個體方法,就是必須是執行個體化之後才能使用的方法,之前都是這種執行個體方法。
靜态方法,就是一種普通函數,儲存在類中,可以通過類來調用。使用裝飾器@staticmethod定義靜态方法。
class Foo(object):
def f1(self): # 這裡的self參數是必須的
print("test in f1")
@staticmethod # 這個是靜态方法
def f2(): # 這裡的參數不是必須的了
print("test in f2")
Foo.f2() # 并沒有建立對象,直接通過類調用了靜态方法,類似函數了
obj.f2() # 當然由于有繼承,通過對象也能夠調用類中的靜态方法
obj = Foo()
obj.f1()
上面的例子中,f1是執行個體方法,f2就是靜态方法。
這裡的f1不要這麼用,因為這個方法裡并沒有用到對象裡面的任何東西。但是這麼寫,要調用f1必須得先執行個體化一個對象,但是其實并不需這個對象,而且還浪費空間。是以這種情況下,按照f2那樣定義成一個靜态方法會更好。
在别的純面向對象的語言裡,也是提供靜态方法的。通過這種靜态方法,可以讓我們直接就能執行這個方法了。
另外除了靜态方法和執行個體方法,還有一個類方法,這個貌似和靜态方法在python裡差不多,下面直接看看差別。
靜态方法 和 類方法的差別
類方法可以将類作為參數傳入,在繼承的時候,子類會優先調用子類的屬性和方法。
靜态方法,無法傳入類和對象,是以無論在哪個級别,永遠都是調用基類的屬性和方法
class Father(object):
test = 'This is in Father'
@classmethod
def test_classmethod(cls):
print(cls.test) # 類方法,可以将類作為參數
@staticmethod
def test_staticmethod():
print(Father.test) # 靜态方法,沒有參數傳入,隻能用自己的類名,調用自己這個類的屬性
class Son(Father):
test = "This is in Son"
Father.test_classmethod()
Father.test_staticmethod()
Son.test_classmethod() # 繼承後的子類的類方法的結果不同了
Son.test_staticmethod()
類方法和靜态方法的作用就是可以通過類來調用類中的屬性和方法,而不需要先進行執行個體化。在還沒有執行個體化生成對象的時候,隻能調用類中的這兩種方法。
類的組合關系
類與類之間除了可以是繼承關系,還可以有組合關系。比如有兩個類,學生和老師,那麼可以在這之上在定義一個學校成員類,把學生和老師公有的屬性和方法放在學校成員類裡,作為父類。而學生和老師就作為兩個子類繼承父類。這是是之前将的繼承,類于類之間是一種屬于的關系。
現在又有一個類,課程類。課程不屬于任何的人,但是老師有教授的課程,學生也有學習的課程。我們希望可以通過老師來獲得他所教授的課程,也就是獲得課程類的屬性和方法。是以課程和老師之間又需要有這某種關系,這是就需要用到組合關系。
假設一個老師隻教一門課,先搞清楚組合關系,不考慮教多門課。
如果沒有組合關系,我們大概可以把課程的屬性作為老師的屬性,如下:
class Teacher(object):
def __init__(self,name,age,course_name,period,title):
"名字、年齡、課程名、課時、職稱"
self.name = name
self.age = age
self.course_name = course_name
self.period = period
self.title = title
當然我們還會有學生類,這會用到這些屬性,除了職稱。通過繼承關系我們可以把名字和年齡作為一個父類,讓老師和學生繼承,但是課程和課時就無法利用繼承關系實作代碼的重複使用了。
class Course(object):
def __init__(self,name,period):
"課程名、課時"
self.name = name
self.period = period
class People(object):
def __init__(self,name,age):
"名字、年齡"
self.name = name
self.age = age
class Teacher(People):
def __init__(self,name,age,title):
"名字、年齡、職稱"
super(Teacher,self).__init__(name,age)
self.title = title
class Student(People):
def __init__(self,name,age):
super(Student,self).__init__(name,age)
t1 = Teacher("Bob",32,"教授")
print(t1.name,t1.age,t1.title)
上面隻是把繼承關系寫好了,單獨把課程定義為了一個類,但是并沒有把課程組合和别的類組合起來。
方法一:先把課程執行個體化,将執行個體化後的課程作為People類的一個屬性傳入
class Course(object):
def __init__(self,name,period):
"課程名、課時"
self.name = name
self.period = period
class People(object):
def __init__(self,name,age,course):
"名字、年齡、課程"
self.name = name
self.age = age
self.course = course
class Teacher(People):
def __init__(self,name,age,course,title):
super(Teacher,self).__init__(name,age,course)
self.title = title # 教師比學生多一個職稱屬性
class Student(People):
def __init__(self,name,age,course):
super(Student,self).__init__(name,age,course)
c1 = Course("python",360) # 先執行個體化一個課程
t1 = Teacher("Bob",32,c1,"教授") # 課程是教師的一個屬性
print(t1.course.name,t1.course.period) # 通過教師執行個體來調用課程類的屬性
class Course(object):
def __init__(self,name,period):
"課程名、課時"
self.name = name
self.period = period
class People(object):
def __init__(self,name,age,course_name,period):
self.name = name
self.age = age
self.course = Course(course_name,period) # 在構造函數裡完成執行個體化
class Teacher(People):
def __init__(self,name,age,course_name,period,title):
super(Teacher,self).__init__(name,age,course_name,period)
self.title = title
class Student(People):
def __init__(self,name,age,course_name,period):
super(Student,self).__init__(name,age,course)
t1 = Teacher("Bob",32,"python",360,"教授") # 需要将課程類的屬性在執行個體化的時候一起傳入
print(t1.course.name,t1.course.period) # 調用方法都是一樣的
作業:選課系統
- 建立北京、上海 2 所學校
- 建立linux , python , go 3個課程 , linux\py 在北京開, go 在上海開
- 課程包含,周期,價格,通過學校建立課程
- 通過學校建立班級, 班級關聯課程、講師
- 建立學員時,選擇學校,關聯班級
- 建立講師角色時要關聯學校,
-
提供至少兩個角色接口
7.1 學員視圖, 可以注冊, 交學費, 選擇班級,
7.2 講師視圖, 講師可管理自己的班級, 上課時選擇班級, 檢視班級學員清單 , 修改所管理的學員的成績
7.3 管理視圖,建立講師, 建立班級,建立課程
- 上面的操作産生的資料都通過pickle序列化儲存到檔案裡