Python入門筆記7 - 面向對象編程

  • 參考書《Python基礎教程(第三版)》—— Magnus Lie Hetland
  • 廖雪峯的python教程:面向對象編程

一、面向對象編程OOP

  1. OOP是一種程序設計思想。這是一種源自自然界的思想,我們在生活中會把自然地把各種具體事物歸類到某種抽象概念。比如我們把“小轎車”、“卡車”、“麪包車”等等統稱爲“車”,一個班級裏的“Mike”和“Alice”都是“student”。基於這種思想,OOP中我們先要抽象出Class,再根據Class創建Instance,最後用Instance組成整個程序

    • :一種抽象概念,比如我們定義的Class——Student,是指學生這個概念
    1. (對象)實例:是類的具現,比如一個個具體的Student
  2. OOP把對象作爲程序的基本單元,一個對象包含了

    • 屬性:數據
    • 方法:操作數據的函數
  3. 計算機程序視爲一組對象的集合,而每個對象都可以接收其他對象發過來的消息,並處理這些消息,計算機程序的執行就是一系列消息在各個對象之間傳遞。

  4. 在Python中,所有數據類型都可以視爲對象,當然也可以自定義對象。自定義的對象數據類型就是面向對象中的類(Class)的概念。

  5. 數據封裝繼承多態是面向對象的三大特點

二、類和對象

1. 基本概念

  1. 類是抽象的模板,實例是根據類創建出來的一個個具體的“對象”,每個對象都擁有相同的方法,但各自的數據可能不同。
  2. 在較舊的python版本中,"類型"和"類"是涇渭分明的,內置對象是基於類型的,自定義對象是基於類的,可以創建類但不能創建類型。但在python3中,已經沒有這種區別

2. 創建一個自定義類

#定義一個person類
class person:
	def set_name(self,name):
		self.name = name

	def get_name(self):
		return self.name

	def greet(self):
		print("Hello,world! I'm {}.".format(self.name))

foo = person()	#創建一個person對象,並用foo指向它
bar = person()
foo.set_name("A")
bar.set_name("B")
foo.greet()			#打印"Hello,world! I'm A"
bar.greet()			#打印"Hello,world! I'm B"
person.greet(foo)   #打印"Hello,world! I'm A"
  1. class創建獨立的命名空間,用於在其中定義函數

  2. 實例化foo = person()的時候,實際上是在內存某個位置創建了一個person對象,再把foo變量指向它。

  3. 通過上面的示例,可以看出self的作用:代指對象本身。如foo.set_name("A")時,其實傳遞了foo本身'A'兩個參數

  4. self參數也可以顯示給出,如果foo是一個person實例,可將foo.greet()視爲person.greet(foo)的簡寫,但後者的多態性更低

3. 屬性、函數和方法

  1. 和普通的函數相比,在類中定義的函數只有一點不同,就是第一個參數永遠是實例變量self,並且,調用時,不用傳遞該參數。除此之外,類的方法和普通函數沒有什麼區別,所以,你仍然可以用默認參數、可變參數、關鍵字參數和命名關鍵字參數。
  2. 可以將方法重新關聯一個普通函數
  3. 相應的,也可以讓普通變量指向類的成員方法,這樣如果成員方法有self,它們也可以訪問到self
# 將類方法重新關聯一個普通函數
class Cla:
	def method(self):
		print("I have a self")

def func():
	print("I dont...")

instance = Cla()
instance.method()		#打印I have a self

instance.method = func	#重新關聯函數(注意這裏只寫方法名和函數名,不要加括號)
instance.method()		#打印I dont...
# 讓普通變量指向類的成員方法
class Cla:
    I = '1234'
    def sing(self):
        print(self.I)

def func():
    print("I dont...")

inst = Cla()
func = inst.sing        #普通函數指向成員函數(關聯到類的實例inst)
func()                  #打印1234(訪問self成功)

FUN = inst.sing         #普通變量指向成員函數(關聯到類的實例inst)
FUN()                   #打印1234(訪問self成功)

4. 類的命名空間 & 對象的命名空間

  1. 在class語句中定義的代碼都是在一個特殊的命名空間(類的命名空間)內執行的,而類的所有成員都可以訪問這個命名空間
  2. 實例化類的對象時,也會給每個對象一個它自己的對象的命名空間
  3. 當我們定義了一個類屬性後,這個屬性雖然歸類所有,但類的所有實例都可以訪問到
  4. 看這個示例
class test:
    n = 0				#這個屬性n定義在類命名空間。實例化後,每個對象也會在其自己的局部命名空間有一個名爲n的屬性
    def addClass(self):
		test.n += 1		#這個方法給類命名空間的n加1

    def addSelf(self):	#這個方法給對象自己的屬性加1
		self.n += 1

t1 = test()
t2 = test()
print(t1.n,t2.n,test.n)	#打印0 0 0


t1.addClass()
print(t1.n,t2.n,test.n)	#打印1 1 1

t1.addSelf()			#這裏操作了t1的屬性n,這會"遮蓋"類命名空間中的n,在這之後進行test.n += 1將不會對t1.n產生作用(類似函數形參"遮蓋"全局變量)
print(t1.n,t2.n,test.n)	#打印2 1 1

t2.addClass()
print(t1.n,t2.n,test.n)	#打印2 2 2(可見這裏t1.n沒有發生變化了,因爲t1.n已將test.n遮蓋)

t1.addClass()
print(t1.n,t2.n,test.n)	#打印2 3 3
  1. 分析:
    • 定義類時,屬性n會定義在類的命名空間中
    • 對此類實例化爲對象時,對象的命名空間中可以訪問到類命名空間的屬性n,在“遮蓋”發生之前,修改類命名空間的n會改變所有對象的屬性n的值
    • 一旦對某個對象的屬性n被修改(通過self方法,或者直接進行t1.n=100之類的賦值),這個對象命名空間中,對象的屬性n就會“遮蓋”類屬性n
    • 這類似函數形參“遮蓋”全局變量
    • 注意,直到“覆蓋”發生前,對象的屬性值一直和類的屬性值相同
  2. 再看幾個對比示例
class test:
    #n = 0				#不顯式給出屬性
    def initN(self):
		test.n = 0		#間接給出類屬性n
		self.n = 0		#間接給出對象屬性n

    def addClass(self):
		test.n += 1

    def addSelf(self):
		self.n += 1

t1 = test()
t2 = test()
t1.initN()		#間接定義類屬性n和對象屬性n,這時直接發生“覆蓋”	
t2.initN()		
print(t1.n,t2.n,test.n)	#打印0 0 0

t1.addClass()
print(t1.n,t2.n,test.n)	#打印0 0 1

t2.addClass()
print(t1.n,t2.n,test.n)	#打印0 0 2
	
t1.addSelf()
print(t1.n,t2.n,test.n)	#打印1 0 2

-------------------------------------
class test:
    #n = 0				#不顯式給出屬性
    def initN(self):
		test.n = 0
		self.n = 0

    def addClass(self):
		test.n += 1

    def addSelf(self):
		self.n += 1

t1 = test()
t2 = test()
#t1.initN()		#這裏註釋了一句
t2.initN()		#這句執行後,test.n和self.n屬性才存在,而且t2對象中發生了遮蓋,t1對象沒有遮蓋(如果這句也註釋,n沒有定義,下一句會報錯test對象沒有屬性n)
print(t1.n,t2.n,test.n)	#打印0 0 0

t1.addClass()
print(t1.n,t2.n,test.n)	#打印1 0 1

t2.addClass()
print(t1.n,t2.n,test.n)	#打印2 0 2

t1.addSelf()
print(t1.n,t2.n,test.n)	#打印3 0 2	到這裏t1的n才發生“遮蓋”

t2.addClass()
print(t1.n,t2.n,test.n)	#打印3 0 3

-------------------------------------
class test:
	#n = 0#不顯式給出屬性
	def initN(self):
	    test.n = 0
	    self.n = 0
	
	def addClass(self):
		test.n += 1
	
	def addSelf(self):
		self.n += 1

t1 = test()
t2 = test()
#t1.initN()		#不用init方法定義屬性n
#t2.initN()
t1.n = 0		#類外定義屬性n,這樣三個n會分別處於t1、t2和test的命名空間,相互間不影響
t2.n = 0
test.n = 0
print(t1.n,t2.n,test.n)	#打印0 0 0

t1.addClass()
print(t1.n,t2.n,test.n)	#打印0 0 1

t2.addClass()
print(t1.n,t2.n,test.n)	#打印0 0 2

t1.addSelf()
print(t1.n,t2.n,test.n)	#打印1 0 2

t2.addClass()
print(t1.n,t2.n,test.n)	#打印1 0 3
  1. python不會在執行前對你編寫的類檢查屬性是否存在,python是一個解釋型語言,只有解釋到調用屬性的時候纔會去查找對象命名空間或類命名空間中的這個屬性,並進行可能的報錯。在解釋到調用這個屬性前,在任意位置定義這個屬性都可以避免解釋器報錯

5. __init__方法

  1. 從上面的可以看出類屬性的添加很自由,但因此也有點混亂。我們可以最好定義一個特殊的__init__方法來規範類的屬性,這個特殊的方法被稱爲構造方法
  2. 由於類可以起到模板的作用,因此,可以在創建實例的時候,把一些我們認爲必須綁定的屬性強制填寫進去
class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score
  1. 有了__init__方法,在創建實例的時候,就不能傳入空的參數了,必須傳入與__init__方法匹配的參數。self不需要傳,Python解釋器自己會把實例變量傳進去
>>> bart = Student('Bart Simpson', 59)
>>> bart.name
'Bart Simpson'
>>> bart.score
59

三、數據封裝

1. 讀取器和設置器方法

  1. 封裝:向外部隱藏不必要的細節,這樣無需知道對象的構造就能使用它
  2. 一個類的成員變量(屬性)應當對類外隱藏,以降低類聚性。如果想要在類外讀寫類的屬性,應該通過讀取器和設置器方法。
class test:
    I = 1
    def getI(self):		#讀取器方法
		return self.I
    def setI(self,n):	#設置器方法
		self.I = n

T = test()
print(T.getI())	#打印1
T.setI(100)		#(推薦這種修改方法)
print(T.getI())	#打印100
T.I = 0			#(這種修改方法不好)
print(T.getI())	#打印0
  1. 此類有一個屬性I,並對I設置了存取器方法,但由於I沒有對類外隱藏,可以從外部直接修改I的值,這樣的封裝不好,應當將I設置爲私有,存取器設置爲公共,提升封裝性

2. “私有屬性”

  1. 在python中沒有爲私有屬性提供直接的支持,我們只能用一些手段來近似做到這一點:在私有成員名稱前加兩個下劃線。這樣處理的方法或屬性,在類外不能直接訪問,而在類內可以
    • 事實上,加兩個下劃線意義是:對成員進行名稱轉換,在類內沒有變化,在類外必須加"_類名"前綴才能訪問,因此
    • 這種方法雖然不能徹底禁止類外訪問,但是它發出了強烈的信號,不要這樣做!
class test:
    __I = 1				
    def __getI(self):
		return self.__I			#類內可以訪問__I

    def getI(self):
		return self.__getI()	#類內可以訪問self.__getI()

    def setI(self,n):
		self.__I = n

T = test()
print(T.getI())		#打印1
#print(T.__getI())	#報錯'test' object has no attribute '__getI'
#print(T.__I)		#報錯'test' object has no attribute '__I'

#實際上發生了改名
print(T._test__I)		#打印1
print(T._test__getI())	#打印1

四、繼承

1. 普通繼承

  1. 繼承:如果已經寫了一個類,現在又要寫一個很相似的類,可以通過繼承繼承原有類的方法和屬性,並在此基礎上修改。
  2. 繼承得到的新的class稱爲子類(Subclass),而被繼承的class稱爲基類、父類或超類(Base class、Super class)
  3. 繼承最大的好處是子類獲得了父類的全部功能
  4. 也可以對子類增加一些方法
  5. 當子類和父類都存在同名run()方法時,我們說,子類的run()覆蓋了父類的run(),在代碼運行的時候,總是會調用子類的run()。這樣,我們就獲得了繼承的另一個好處:多態
#基類
class Animal(object):
    def run(self):
        print('Animal is running...')
        
#子類
class Dog(Animal):
    pass

#子類
class Cat(Animal):
	def run(self):
        print('Cat is running...')

dog = Dog()
dog.run()	#打印Animal is running...

cat = Cat()
cat.run()	#打印Cat is running...

2. 多繼承

  1. 多重繼承一個子類可能有多個超類/基類
class Talker:
	def talk(self):
		print("Hi,my value is:",self.value)

class Calculator:
	def calculator(self,expression):
		self.value = eval(expression)

class TC(Calculator,Talker):
	pass

tc = TC()
tc.calculator("1+2*3")
tc.talk()		#打印 Hi,my value is:7
  1. 使用多重繼承,要注意多個基類的方法不應出現同名否則繼承時排在最前的類的方法會“遮住”其他類的同名方法

    1. 假設有基類ABC且它們都有方法talk()
      • class sonClass(A,B,C) 這樣A.talk()會遮住其他倆個
      • class sonClass(B,A,C) 這樣B.talk()會遮住其他倆個

3. 深入探討繼承

  1. 有以下類
# 這裏的Filter類是一個基類,它本身並不過濾任何東西
class Filter:
    def init(self):
		self.blocked = []

    def filter(self,sequence):
		return [x for x in sequence if x not in self.blocked]

# SPAMFilter是從Filter繼承過來的,它是Filter的一個子類
class SPAMFilter(Filter):
    def init(self):
self.blocked = ['SPAM']

s = SPAMFilter()
s.init()
print(s.filter(['SPAM','SPAME']))	#打印['SPAME']
  1. 要確定一個類是否是另一個類的子類:內置方法issubclass
>>> issubclass(SPAMFilter,Filter)
True
>>> issubclass(Filter,SPAMFilter)
False
  1. 有一個類,想知道它的基類:訪問類的特殊屬性__bases__
>>> print(SPAMFilter.__bases__)
<class '__main__.Filter'>
>>> print(Filter.__bases__)
(<class 'object'>,)	
  1. 有一個實列,要確定它是否是某個類的實例:內置方法isinstance
>>> s = SPAMFilter()
>>> isinstance(s,SPAMFilter)
True
>>> isinstance(s,Filter)	#間接實例返回也是True
True
>>> isinstance(s,str)		#isinstance也可用於類型(如這裏的字符串類型str)
False
  1. 有一個對象,想知道它的類:訪問對象的特殊屬性__class__
>>> print(s.__class)
<class '__main__.SPAMFilter'>
  1. 繼承子類中,構造函數的處理:參考Python 子類繼承父類構造函數說明

4. super方法

  1. super方法允許在子類中調用父類的方法,詳見:python super詳解

五、多態

1. 子類對象是基類對象

  1. 在上面isinstance部分,我們知道:
    1. 子類對象是基類對象,也就是說在繼承關係中,如果一個實例的數據類型是某個子類,那它的數據類型也可以被看做是父類。
    2. 基類對象不是子類對象

2. 多態的威力

  • 看這個示例(以下來自廖雪峯的教程
#基類Animal(這裏是從object類繼承,這是python2的寫法,python3可以不寫object)
class Animal(object):
    def run(self):
        print('Animal is running...')

#從Animal繼承得到3個子類
class Dog(Animal):
    def run(self):
        print('Dog is running...')

class Cat(Animal):
    def run(self):
        print('Cat is running...')

class Tortoise(Animal):
    def run(self):
        print('Tortoise is running slowly...')

#編寫一個普通函數,參數是一個animal類型的變量
def run_twice(animal):
    animal.run()
    animal.run()

#下面說明多態的好處
>>> run_twice(Animal())
Animal is running...
Animal is running...

>>> run_twice(Dog())
Dog is running...
Dog is running...

>>> run_twice(Cat())
Cat is running...
Cat is running...

>>> run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...
  • 新增一個Animal的子類,不必對run_twice()做任何修改,實際上,任何依賴Animal作爲參數的函數或者方法都可以不加修改地正常運行,原因就在於多態

  • 由於Animal類型有run()方法,因此,傳入的任意類型,只要是Animal類或者子類,就會自動調用實際類型的run()方法,這就是多態的意思:

  • 對於一個變量,我們只需要知道它是Animal類型,無需確切地知道它的子類型,就可以放心地調用run()方法

  • 調用方只管調用,不管細節,而當我們新增一種Animal的子類時,只要確保run()方法編寫正確,不用管原來的代碼是如何調用的。

3. 開閉原則

  1. 對擴展開放:允許新增Animal子類;
  2. 對修改封閉:不需要修改依賴Animal類型的run_twice()等函數。

4. 鴨子類型

  1. 對於靜態語言(例如Java)來說,如果需要傳入Animal類型,則傳入的對象必須是Animal類型或者它的子類,否則,將無法調用run()方法。

  2. 對於Python這樣的動態語言來說,則不一定需要傳入Animal類型。我們只需要保證傳入的對象有一個run()方法就可以了

  3. python語言設計類型時遵循了“鴨子原則”:一個對象只要“看起來像鴨子,走起路來像鴨子”,那它就可以被看做是鴨子。因此一個有了run()方法的類會被認爲是Animal類型的。即使它並不符合繼承體系

  4. 由於這一點,我們可以像下面這樣繞過參數必爲Animal類型的限制

class Test():
    def run(self):
            print("Test")

run_twice(Test())	#打印兩遍Test
					

六、獲取對象信息

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章