面向對象編程
1、概述
面向對象(Object Oriented)的英文縮寫是OO,它是一種設計思想。我們經常聽說的面向對象編程(Object Oriented Programming,即OOP)就是主要針對大型軟件設計而提出的,它可以使軟件設計更加靈活,並且能更好地進行代碼復用。
面向對象中的對象(Object),通常是指客觀世界中存在的對象,這個對象具有唯一性,對象之間個不相同,各有各的特點,每一個對象都有自己的運動規律和內部狀態;對象和對象之間又是可以相互聯系、相互作用的。
對象:是一個抽象概念,英文稱為Object,表示任意存在的事物。世間萬物皆對象,現實世間中隨處可以的一種事物就是對象,對象是事物存在的實體,如一個人。
通常將對象划分為兩個部分:靜態部分和動態部分。靜態部分被稱為【屬性】,任何對象都具備自身屬性,這些屬性不僅是客觀存在的,而且是不能被忽視的。如人的性別。動態部分指的是對象的行為,即對象執行的動作,如人可以行走。
面向過程編程通常具有下面的表現形式:
- 導入各種模塊
- 定義需要的全局變量
- 寫一個函數完成特定功能
- 寫一個函數完成特定功能
- ...
- 寫一個main函數作為程序入口
函數是核心,函數調用是關鍵,一切圍繞函數展開。
面向對象編程中,將函數和變量進一步封裝成類,類才是程序的基本元素,它將數據和操作緊密連結在一起,並保護數據不會被外界的函數意外改變。類和類的實例(也稱對象)是面向對象的核心概念,是和面向過程編程、函數式編程的根本區別。
面向對象編程通常具有如下表現形式:
- 導入各種模塊
- 設計各種全局變量
- 設計類
- 給每個類提供完整的一組操作
- 使用繼承來表現不同類之間的共同點
- 根據需求,決定是否寫一個main函數作為程序入口
類的形態:
class Human: def __init__(self,name,age): self.name = name self.age = age def print_age(self): print("{0}:{1}".format(self.name,self.age))
類具有封裝、繼承、多態三大特點。一個類定義了具有相似性質的一組對象。而繼承是對具有層次關系的類的屬性和操作進行共享的一種方式。所謂面向對象就是基於對象概念,以對象為中心,以類和繼承為構造機制,來認識、理解、刻畫客觀世界
和設計、構建相應的軟件系統。
2、術語
- 類(Class)---> 用來描述具有相同屬性和方法的對象集合。定義了該集合中每個對象所共有的屬性和方法。其中對象被稱作類的實例;
- 實例 ---> 也稱對象。通過類定義的初始化方法,賦予具體的值;
- 實例化 ---> 創建類的實例的過程或操作。
- 實例變量 ---> 定義在實例中的變量,只作用於當前實例。
- 類變量 ---> 類變量是所有實例公有的變量。類變量定義在類中,但在方法體之外。
- 數據成員 ---> 類變量、實例變量、方法、類方法、靜態方法和屬性等的統稱。
- 方法 ---> 類中定義的函數。
- 靜態方法 ---> 不需要實例化就可以有類執行的方法。
- 類方法 ---> 類方法是將類本身作為對象進行操作的方法。
- 方法重寫 ---> 從父類繼承的方法不能滿足子類的需求,可以對父類的方法進行重寫。
- 繼承 ---> 一個派生類(derived class)繼承父類(base class)的變量和方法。
- 多態 ---> 根據對象類型的不同以不同的方式進行處理。
3、類和實例
類,英文名字Class,類別,分類,聚類的意思。必須牢記類是抽象的模板,用來描述具有相同屬性和方法的對象集合。而實例是根據類創建出來的一個個具體的【對象】,每個對象都擁有相同的方法,但各種的數據可能不同。
Python使用class關鍵字來定義類:
class 類名(父類列表):
pass
類名通常采用駝峰式命名方式,盡量讓字面意思體現出類的作用。Python采用多繼承機制,一個類可以同時繼承多個父類(也叫基類、超類),繼承的基類有先后順序,寫在類名后的圓括號里。繼承的父類列表可以為空,此時圓括號可以省略。
Python3中,類似於 class Student:pass的方法沒有顯示繼承任何父類定義的類,默認就繼續object類。因為object是Python3中所有類的基類。
class Student: classroom = '101' address = 'guangzhou'
def __init__(self,name,age): self.name = name self.age = age def print_age(self): print("{0}:{1}".format(self.name,self.age))
默認情況下,使用obj = Student() 的方式就可以生成一個類的實例。但是通常每個類的實例都會有自己的實例變量,例如:name和age,為了在實例化的時候體現實例的不同,Python提供了一個def __init__(self)的實例化機制。
任何一個類中,名字為__init__的方法就是類的實例化方法,具有__init__方法的類在實例化的時候,會自動調用該方法,並傳遞對應的參數:
例如:
wang = Student("老王",24)
liu = Student("老劉",25)
實例變量和類變量
實例變量:
實例變量指的是實例本身擁有的變量,每個實例的變量在內存中都不一樣。Student類中__init__方法里的name和age就是兩個實例變量。通過實例變量名+圓點的方式調用實例變量。
print(wang.name)
print(wang.age)
print(liu.name)
print(liu.age)
********************************************
>>>老王
>>>24
>>>老劉
>>>25
類變量:
定義在類中,方法之外的變量,稱為類變量。類變量是所有實例公有的變量,每一個實例都可以訪問和修改類變量。在Student類中,classsroom和address兩個變量就是類變量。
可以通過【類名】或者【實例名】+ 圓點的方式訪問類變量。
例如:
Student.classroom
Student.address
wang.classroom
liu.address
使用實例變量和類變量的時候一定要注意,使用類似 : wang.name訪問變量的時候,實例會先在自己的實例變量列表里查找是否有這個實例變量,如果沒有,那么它就會去類變量列表中查找,如果還沒有,拋出異常;
Python動態語言的特點:可以隨時給實例添加新的實例變量,給類添加新的類變量和方法。因此在使用liu.classroom = '102' 的時候,要么是給已有的實例變量classroom重新賦值,要么就是新建一個liu專屬的實例變量classroom並賦值為"102"
例子:
class Student: classroom = "120" address = "廣州"
def __init__(self,name,age): self.name = name self.age = age def print_age(self): print("{0}:{1}".format(self.name,self.age)) wang = Student("老王",32) #創建一個實例
liu = Student("老劉",23) #創建一個實例
print(liu.classroom) #liu本身沒有classroom實例變量,所以去尋找類變量,它找到了
print(wang.classroom) print(Student.classroom) liu.classroom = "110" #關鍵的一步,實際是為liu創建了獨有的實例變量,只不過名字和類變量一樣,都叫做classroom。
print("再次訪問classroom",liu.classroom) #訪問到的是liu自己的實例變量classroom
print("老王沒有實例變量classroom",wang.classroom)#wang沒有實例變量classroom,依然訪問類變量classroom
print("類變量classroom保存不變:",Student.classroom) #保持不變
del liu.classroom #刪除li的實例變量classroom
print("恢復到原樣:",liu.classroom) print(wang.classroom) print(Student.classroom)
類的方法
Python類中包含實例方法、靜態方法和類方法三種方法。這些方法無論是在代碼編排中還是內存中都歸屬於類,區別在於傳入的參數和調用方式不同。在類的內部,使用def 關鍵字來定義一個方法。
實例方法
類的實例方法由實例調用,至少包含一個self參數,且為第一個參數。執行實例方法時,會自動調用該方法的實例賦值給self。self代表的是類的實例,而非類本身。
self不是關鍵字,而是Python約定的命名,可以自定義但是不建議自定義。
例如:
class Student: classroom = "120" address = "廣州" def __init__(self,name,age): self.name = name self.age = age def print_age(self): #實例方法 print("{0}:{1}".format(self.name,self.age))
wang = Student("老王",32) #創建一個實例 liu = Student("老劉",23) #創建一個實例
#調用方法
wang.print_age()
liu.print_age()
靜態方法
靜態方法由類調用,無默認參數。將實例方法中self去掉,然后在方法定義上加上@staticmethod,就成為靜態方法。它屬於類,和實例無關。使用類名.靜態方法的調用方式。(雖然也可以使用實例名.靜態方法的方式調用)
class Foo: @staticmethod def static_method(): pass
#調用方法
Foo.static_method()
類方法
類方法由類調用,采用@classmethod裝飾,至少傳入一個cls(指類本身,類似於self)參數。執行類方法時,自動將調用該方法的類賦值給cls。使用類名.類方法的調用方式(也可以使用實例名.類方法的方式調用)
class Foo: @classmethod def class_method(cls): pass Foo.class_method()
綜合例子:
class Foo: def __init__(self,name): self.name = name def ord_func(self): """實例方法"""
print("實例方法") @classmethod def class_func(cls): """類方法"""
print("類方法") @staticmethod def static_func(): """靜態方法,無默認參數"""
print("靜態方法") #調用實例方法
f = Foo("tom") f.ord_func() Foo.ord_func(f) # 雖然可以,但是建議不要這么調用
#調用類方法
Foo.class_func() f.class_func() #雖然可以,但是建議不要這么調用
#調用靜態方法
Foo.static_func() f.static_func() #雖然可以,但是建議不要這么調用
類、類方法、類變量、類的實例和實例變量在內存中如何保存?
類、類的所有方法以及類變量在內存中只有一份,所有的實例共享它們。而每一個實例都在內存中獨立的保存自己和自己的實例變量。
創建實例時,實例中除了封裝如name和age的實例變量之外,還會保存一個類對象指針,該值指向實例所屬的類地址。因此實例可以尋找到自己的類,並進行相關調用,而類無法尋找到自己的某個實例。
封裝、繼承和多態
面向對象編程有三大重要特征:封裝、繼承、多態。
封裝
封裝是指將【數據】和【具體操作】的實現代碼放在某個對象內部,使這些代碼的實現細節不被外界發現,外界只能通過接口使用該對象,而不能通過任何形式修改對象內部實現。
例如:
class Student: classroom = "120" address = "廣州"
def __init__(self,name,age): self.name = name self.age = age def print_age(self): print("{0}:{1}".format(self.name,self.age)) #以下是錯誤的用法 #類將它內部的變量和方法封裝起來,阻止外部的直接訪問
print(classroom) print(adress) print_age()
繼承
繼承機制實現了代碼的復用,多個類公用的代碼部分可以只在一個類中提供,而其它類只需要繼承這個類即可。
OOP程序設計中,定義一個新類的時候,新的類成為子類(Subclass),而被繼承的類稱為基類、父類、或者超類(Base class、Super class)。
繼承最大的好處就是子類獲得了父類的全部變量和方法的同時,又可以根據需要進行修改、擴展。
語法結構如下:
class Foo(superA,superB,superC....):
class DerivedClasssName(modname.BaseClassName): ##當父類定義在另外的模塊時
Python支持多父類的繼承機制,所以需要注意圓括號中基類的順序,若是基類中有相同的方法名,並且在子類使用時未指定,Python會從左至右搜索基類中是否包含該方法。
class People: def __init__(self,name,age,weight): print("父類的init方法") self.name = name self.age = age self.__weight = weight def speak(self): print("父類的speak方法") print("{0}說:我{1}歲。".format(self.name,self.age)) #單繼承
class Student(People): def __init__(self,name,age,weight,grade): #調用父類的實例方法
People.__init__(self,name,age,weight) self.grade = grade def speak(self): print("子類的speak方法") print("{0}說:我{1}歲了,我在讀{2}年級".format(self.name,self.age,self.grade)) s = Student('tian',10,20,3) s.speak()
Python3的繼承機制
- 子類在調用某個方法或變量的時候,首先在自己內部查找,如果沒有找到,則開始根據繼承機制在父類里查找。
- 根據父類定義中的順序,已【深度優先】的方式逐一查找父類。
例一:
繼承關系如下:

class D: pass
class C(D): pass
class B(C): def show(self): print("我是B") class G: pass
class F(G): pass
class E(F): def show(self): print("我是E") class A(B,E): pass a = A() a.show()
#運行結果:我是B
類A中沒有show()方法,於是去父類查找,先從在B類中找(先后順序從左到右),結果找到了,執行B類的show()方法。
如果B類中沒有show方法,而是D類有呢?
class D: def show(self): print("我是D") class C(D): pass
class B(C): pass
class G: pass
class F(G): pass
class E(F): def show(self): print("我是E") class A(B,E): pass a = A() a.show() #輸出:我是D
執行結果是【我是D】,左邊具有深度優先權,當一條路走到黑頁面沒有找到的時候,才換另一條路。

例二:
類D和類G同時繼承了類H。當只有B和E有show方法的時候,和上面例子一樣,找到B就不找了,直接打印【我是B】,但如果只有H和E右show方法呢?

class H: def show(self): print("我是H") class D(H): pass
class C(D): pass
class B(C): pass
class G(H): pass
class F(G): pass
class E(F): def show(self): print("我是E") class A(B,E): pass a = A() a.show() #輸出:我是E
這種情況下繼承順序如下圖:

super()函數
子類中如果有與父類同名的成員,那么就會覆蓋父類里的成員。如果需要強制調用父類的成員呢?
使用super()函數,這是一個非常重要的函數,最常見的就是通過super調用父類實例化方法__init__
語法:super(子類名,self).方法名(),需要傳入子類名和self,調用的是父類里的方法,按父類的方法需要傳入參數。
class A: def __init__(self,name): self.name = name print("父類的__init__方法被執行了!") def show(self): print("父類的show方法被執行了") class B(A): def __init__(self,name,age): super(B,self).__init__(name=name) self.age = age def show(self): super(B,self).show() obj = B("tian",22) obj.show()
多態
class Animal: def kind(self): print("我是aniaml") class Dog(Animal): def kind(self): print("我是一只狗") class Cat(Animal): def kind(self): print("我是一只貓") class Pig(Animal): def kind(self): print("我是一只豬") #這個函數接收一個animal參數,並調用它的kind方法
def show_kind(aniaml): aniaml.kind() d = Dog() c = Cat() p = Pig() show_kind(d) show_kind(c) show_kind(p)
----------------------------
打印結果:
我是一只狗
我是一只貓
我是一只豬
狗、貓、豬都繼承了動物類,並各自重寫了kind方法。show_kind()函數接收一個aniaml參數,並調用它的kind方法。可以看出,無論我們給animal傳遞的是狗、貓、豬都能正確調用相應的方法,打印對應信息,這就是多態;
動態語言調用實例方法時不檢查類型,只要方法存在,參數正確,就可以調用;
成員保護和訪問限制
在類的內部,有各種變量和方法。這些數據成員,可以在類的外部通過實例或者類名進行調用。
例如:
class People: title = "人類"
def __init__(self,name,age): self.name = name self.age = age def print_age(self): print("{0}:{1}".format(self.name,self.age)) obj = People("tian",13) obj.age = 18 obj.print_age() print(People.title)
上面的調用方式是大多數情況下都需要的,但是往往我們也不希望所有的變量和方法能被外部訪問,需要針對性地保護某些成員,限制對這些成員的訪問。這樣的程序才是健壯、可靠的、也符合業務的邏輯。
在Python中,如果要讓內部成員不被外部訪問,可以在成員的名字前面加上兩個下划線__,這個成員就變成了一個私有成員(private)。私有成員只能在類的內部訪問,外部無法訪問。
class People: title = "人類"
def __init__(self,name,age): self.__name = name self.__age = age def print_age(self): print("{0}:{1}".format(self.__name,self.__age)) obj = People("tian",18) obj.__name #會報錯
如果要對__name和__age進行訪問和修改呢?在類的內部創建外部可以訪問的get和set方法;
class People: title = "人類"
def __init__(self,name,age): self.__name = name self.__age = age def print_age(self): print("{0}:{1}".format(self.__name,self.__age)) def get_name(self): return self.__name
def get_age(self): return self.__age
def set_name(self,name): self.__name = name def set_age(self,age): self.__age = age obj = People("tian",18) obj.get_name() obj.set_name("xiang")
這樣做,不但對數據進行了保護同時也提供了外部訪問的接口,而且在get_name,set_name這些方法中,可以添加驗證代碼等等各種操作,作用巨大;
那么,以雙下划線開頭的數據成員是不是一定就無法從外部訪問呢?其實也不是,從內部機制原來講,外部不能直接訪問__age是因為Python解釋器對外把__age變量改成了_People__age,也就是_類名__age(類名前是一個下划線)。
因此,投機取巧的話,可以通過_People__age在類的外部訪問__age變量:
obj = People("tian",23)
print(obj._People__name)
擴展:由於Python內部會對雙下划線開頭的私有成員進行名字變更,所以會出現下面情況:
class People: title = "人類"
def __init__(self,name,age): self.__name = name self.__age = age def print_age(self): print("{0}:{1}".format(self.__name,self.__age)) def get_name(self): return self.__name
def set_name(self,name): self.__name = name obj = People("tian",18) obj.__name = "xiang"
print("obj.__name:",obj.__name) #相當於給obj實例添加了一個新的實例變量__name,而不是對原有私有成員__name的重新賦值。
print("obj.get_name():",obj.get_name())
此外有些時候,你會看到一個下划線開頭的成員名,例如_name,這樣的數據成員在外部是可以訪問的,但是按照約定的規定,看到這樣的標識符時,意思就是【雖然我可以被外部訪問,但是請把我視為私有成員,不要訪問在外部訪問我】
類的成員與下划線總結:
- _name、_name_、_name_建議性的私有成員,不要在外部訪問。
- __name、__name_強制的私有成員,但是依然可以在外部訪問。
- __name__特殊成員,與私有性質無關,例如__doc__。
- name_、name__沒有任何特殊性,普通的標識符,但最好不要這么起名字。
