Python3 面向對象
Python從設計之初就已經是一門面向對象的語言,正因為如此,在Python中創建一個類和對象是很容易的。本章節我們將詳細介紹Python的面向對象編程。
如果你以前沒有接觸過面向對象的編程語言,那你可能需要先了解一些面向對象語言的一些基本特征,在頭腦里頭形成一個基本的面向對象的概念,這樣有助於你更容易的學習Python的面向對象編程。
接下來我們先來簡單的了解下面向對象的一些基本特征。
面向對象編程——Object Oriented Programming,簡稱OOP,是一種程序設計思想。OOP把對象作為程序的基本單元,一個對象包含了數據和操作數據的函數。
面向過程的程序設計把計算機程序視為一系列的命令集合,即一組函數的順序執行。為了簡化程序設計,面向過程把函數繼續切分為子函數,即把大塊函數通過切割成小塊函數來降低系統的復雜度。
而面向對象的程序設計把計算機程序視為一組對象的集合,而每個對象都可以接收其他對象發過來的消息,並處理這些消息,計算機程序的執行就是一系列消息在各個對象之間傳遞。
在Python中,所有數據類型都可以視為對象,當然也可以自定義對象。自定義的對象數據類型就是面向對象中的類(Class)的概念。
面向對象的設計思想是抽象出Class,根據Class創建Instance。
面向對象的抽象程度又比函數要高,因為一個Class既包含數據,又包含操作數據的方法。
概述
- 面向過程:根據業務邏輯從上到下寫壘代碼
- 函數式:將某功能代碼封裝到函數中,日后便無需重復編寫,僅調用函數即可
- 面向對象:對函數進行分類和封裝,讓開發“更快更好更強...”
面向過程編程最易被初學者接受,其往往用一長段代碼來實現指定功能,開發過程中最常見的操作就是粘貼復制,即:將之前實現的代碼塊復制到現需功能處。
面向對象技術簡介
- 類(Class): 用來描述具有相同的屬性和方法的對象的集合。它定義了該集合中每個對象所共有的屬性和方法。對象是類的實例。
- 類變量:類變量在整個實例化的對象中是公用的。類變量定義在類中且在函數體之外。類變量通常不作為實例變量使用。
- 數據成員:類變量或者實例變量用於處理類及其實例對象的相關的數據。
- 方法重寫:如果從父類繼承的方法不能滿足子類的需求,可以對其進行改寫,這個過程叫方法的覆蓋(override),也稱為方法的重寫。
- 實例變量:定義在方法中的變量,只作用於當前實例的類。
- 繼承:即一個派生類(derived class)繼承基類(base class)的字段和方法。繼承也允許把一個派生類的對象作為一個基類對象對待。例如,有這樣一個設計:一個Dog類型的對象派生自Animal類,這是模擬"是一個(is-a)"關系(例圖,Dog是一個Animal)。
- 實例化:創建一個類的實例,類的具體對象。
- 方法:類中定義的函數。
- 對象:通過類定義的數據結構實例。對象包括兩個數據成員(類變量和實例變量)和方法。
和其它編程語言相比,Python 在盡可能不增加新的語法和語義的情況下加入了類機制。
Python中的類提供了面向對象編程的所有基本功能:類的繼承機制允許多個基類,派生類可以覆蓋基類中的任何方法,方法中可以調用基類中的同名方法。
對象可以包含任意數量和類型的數據。
創建類和對象
面向對象編程是一種編程方式,此編程方式的落地需要使用 “類” 和 “對象” 來實現,所以,面向對象編程其實就是對 “類” 和 “對象” 的使用。
類就是一個模板,模板里可以包含多個函數,函數里實現一些功能
對象則是根據模板創建的實例,通過實例對象可以執行類中的函數
- class是關鍵字,表示類
- 創建對象,類名稱后加括號即可
ps:類中的函數第一個參數必須是self(詳細見:類的三大特性之封裝)
類中定義的函數叫做 “方法”
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# 創建類
class
Foo:
def
Bar(
self
):
print
'Bar'
def
Hello(
self
, name):
print
'i am %s'
%
name
# 根據類Foo創建對象obj
obj
=
Foo()
obj.Bar()
#執行Bar方法
obj.Hello(
'wupeiqi'
)
#執行Hello方法
|
誒,你在這里是不是有疑問了?使用函數式編程和面向對象編程方式來執行一個“方法”時函數要比面向對象簡便
- 面向對象:【創建對象】【通過對象執行方法】
- 函數編程:【執行函數】
觀察上述對比答案則是肯定的,然后並非絕對,場景的不同適合其的編程方式也不同。
總結:函數式的應用場景 --> 各個函數之間是獨立且無共用的數據
面向對象三大特性
面向對象的三大特性是指:封裝、繼承和多態。
一、封裝
封裝,顧名思義就是將內容封裝到某個地方,以后再去調用被封裝在某處的內容。
所以,在使用面向對象的封裝特性時,需要:
- 將內容封裝到某處
- 從某處調用被封裝的內容
第一步:將內容封裝到某處
self 是一個形式參數,當執行 obj1 = Foo('wupeiqi', 18 ) 時,self 等於 obj1
當執行 obj2 = Foo('alex', 78 ) 時,self 等於 obj2
所以,內容其實被封裝到了對象 obj1 和 obj2 中,每個對象中都有 name 和 age 屬性,在內存里類似於下圖來保存。
第二步:從某處調用被封裝的內容
調用被封裝的內容時,有兩種情況:
- 通過對象直接調用
- 通過self間接調用
1、通過對象直接調用被封裝的內容
上圖展示了對象 obj1 和 obj2 在內存中保存的方式,根據保存格式可以如此調用被封裝的內容:對象.屬性名
class Foo: def __init__(self, name, age): self.name = name self.age = age obj1 = Foo('wupeiqi', 18) print obj1.name # 直接調用obj1對象的name屬性 print obj1.age # 直接調用obj1對象的age屬性 obj2 = Foo('alex', 73) print obj2.name # 直接調用obj2對象的name屬性 print obj2.age # 直接調用obj2對象的age屬性
2、通過self間接調用被封裝的內容
執行類中的方法時,需要通過self間接調用被封裝的內容
class Foo: def __init__(self, name, age): self.name = name self.age = age def detail(self): print self.name print self.age obj1 = Foo('wupeiqi', 18) obj1.detail() # Python默認會將obj1傳給self參數,即:obj1.detail(obj1),所以,此時方法內部的 self = obj1,即:self.name 是 wupeiqi ;self.age 是 18 obj2 = Foo('alex', 73) obj2.detail() # Python默認會將obj2傳給self參數,即:obj1.detail(obj2),所以,此時方法內部的 self = obj2,即:self.name 是 alex ; self.age 是 78
綜上所述,對於面向對象的封裝來說,其實就是使用構造方法將內容封裝到 對象 中,然后通過對象直接或者self間接獲取被封裝的內容。
類定義
面向對象最重要的概念就是類(Class)和實例(Instance),必須牢記類是抽象的模板,比如Student類,而實例是根據類創建出來的一個個具體的“對象”,每個對象都擁有相同的方法,但各自的數據可能不同。
語法格式如下:
class ClassName: <statement-1> . . . <statement-N>
類的定義就像函數定義( def
語句),要先執行才能生效。(你當然可以把它放進 if
語句的某一分支,或者一個函數的內部。)
實際應用中,類定義包含的語句通常是函數定義,不過其它語句也是可以的而且有時還會很有用——后面我們會再回來討論。類中的函數定義通常有一個特殊形式的參數列表,這是由方法調用的協議決定的——同樣后面會解釋這些。
進入類定義部分后,會創建出一個新的命名空間,作為局部作用域——因此,所有的賦值成為這個新命名空間的局部變量。特別是這里的函數定義會綁定新函數的名字。
類定義正常退出時,一個 類對象 也就創建了。基本上它是對類定義創建的命名空間進行了一個包裝;我們在下一節將進一步學習類對象的知識。原始的局部作用域(類定義引入之前生效的那個)得到恢復,類對象在這里綁定到類定義頭部的類名(例子中是 ClassName
)。
類實例化后,可以使用其屬性,實際上,創建一個類之后,可以通過類名訪問其屬性。
以Student類為例,在Python中,定義類是通過class
關鍵字:
class Student(object): pass
class
后面緊接着是類名,即Student
,類名通常是大寫開頭的單詞,緊接着是(object)
,表示該類是從哪個類繼承下來的,繼承的概念我們后面再講,通常,如果沒有合適的繼承類,就使用object
類,這是所有類最終都會繼承的類。
定義好了Student
類,就可以根據Student
類創建出Student
的實例,創建實例是通過類名+()實現的:
>>> bart = Student() >>> bart <__main__.Student object at 0x10a67a590> >>> Student <class '__main__.Student'>
可以看到,變量bart
指向的就是一個Student
的實例,后面的0x10a67a590
是內存地址,每個object的地址都不一樣,而Student
本身則是一個類。
可以自由地給一個實例變量綁定屬性,比如,給實例bart
綁定一個name
屬性:
>>> bart.name = 'Bart Simpson' >>> bart.name 'Bart Simpson'
由於類可以起到模板的作用,因此,可以在創建實例的時候,把一些我們認為必須綁定的屬性強制填寫進去。通過定義一個特殊的__init__
方法,在創建實例的時候,就把name
,score
等屬性綁上去:
class Student(object): def __init__(self, name, score): self.name = name self.score = score
注意到__init__
方法的第一個參數永遠是self
,表示創建的實例本身,因此,在__init__
方法內部,就可以把各種屬性綁定到self
,因為self
就指向創建的實例本身。
有了__init__
方法,在創建實例的時候,就不能傳入空的參數了,必須傳入與__init__
方法匹配的參數,但self
不需要傳,Python解釋器自己會把實例變量傳進去:
>>> bart = Student('Bart Simpson', 59) >>> bart.name 'Bart Simpson' >>> bart.score 59
和普通的函數相比,在類中定義的函數只有一點不同,就是第一個參數永遠是實例變量self
,並且,調用時,不用傳遞該參數。除此之外,類的方法和普通函數沒有什么區別,所以,你仍然可以用默認參數、可變參數、關鍵字參數和命名關鍵字參數。
數據封裝
面向對象編程的一個重要特點就是數據封裝。在上面的Student
類中,每個實例就擁有各自的name
和score
這些數據。我們可以通過函數來訪問這些數據,比如打印一個學生的成績:
>>> def print_score(std): ... print('%s: %s' % (std.name, std.score)) ... >>> print_score(bart) Bart Simpson: 59
但是,既然Student
實例本身就擁有這些數據,要訪問這些數據,就沒有必要從外面的函數去訪問,可以直接在Student
類的內部定義訪問數據的函數,這樣,就把“數據”給封裝起來了。這些封裝數據的函數是和Student
類本身是關聯起來的,我們稱之為類的方法:
class Student(object): def __init__(self, name, score): self.name = name self.score = score def print_score(self): print('%s: %s' % (self.name, self.score))
要定義一個方法,除了第一個參數是self
外,其他和普通函數一樣。要調用一個方法,只需要在實例變量上直接調用,除了self
不用傳遞,其他參數正常傳入:
>>> bart.print_score() Bart Simpson: 59
這樣一來,我們從外部看Student
類,就只需要知道,創建實例需要給出name
和score
,而如何打印,都是在Student
類的內部定義的,這些數據和邏輯被“封裝”起來了,調用很容易,但卻不用知道內部實現的細節。
封裝的另一個好處是可以給Student
類增加新的方法,比如get_grade
:
class Student(object): ... def get_grade(self): if self.score >= 90: return 'A' elif self.score >= 60: return 'B' else: return 'C'
同樣的,get_grade
方法可以直接在實例變量上調用,不需要知道內部實現細節:
>>> bart.get_grade() 'C'
小結
類是創建實例的模板,而實例則是一個一個具體的對象,各個實例擁有的數據都互相獨立,互不影響;
方法就是與實例綁定的函數,和普通函數不同,方法可以直接訪問實例的數據;
通過在實例上調用方法,我們就直接操作了對象內部的數據,但無需知道方法內部的實現細節。
和靜態語言不同,Python允許對實例變量綁定任何數據,也就是說,對於兩個實例變量,雖然它們都是同一個類的不同實例,但擁有的變量名稱都可能不同。
訪問限制
但是,從前面Student類的定義來看,外部代碼還是可以自由地修改一個實例的name
、score
屬性:
>>> bart = Student('Bart Simpson', 98) >>> bart.score 98 >>> bart.score = 59 >>> bart.score 59
如果要讓內部屬性不被外部訪問,可以把屬性的名稱前加上兩個下划線__
,在Python中,實例的變量名如果以__
開頭,就變成了一個私有變量(private),只有內部可以訪問,外部不能訪問,所以,我們把Student類改一改:
class Student(object): def __init__(self, name, score): self.__name = name self.__score = score def print_score(self): print('%s: %s' % (self.__name, self.__score))
改完后,對於外部代碼來說,沒什么變動,但是已經無法從外部訪問實例變量.__name
和實例變量.__score
了:
>>> bart = Student('Bart Simpson', 98) >>> bart.__name Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Student' object has no attribute '__name'
這樣就確保了外部代碼不能隨意修改對象內部的狀態,這樣通過訪問限制的保護,代碼更加健壯。
但是如果外部代碼要獲取name和score怎么辦?可以給Student類增加get_name
和get_score
這樣的方法:
class Student(object): ... def get_name(self): return self.__name def get_score(self): return self.__score
如果又要允許外部代碼修改score怎么辦?可以再給Student類增加set_score
方法:
class Student(object): ... def set_score(self, score): self.__score = score
你也許會問,原先那種直接通過bart.score = 59
也可以修改啊,為什么要定義一個方法大費周折?因為在方法中,可以對參數做檢查,避免傳入無效的參數:
class Student(object): ... def set_score(self, score): if 0 <= score <= 100: self.__score = score else: raise ValueError('bad score')
需要注意的是,在Python中,變量名類似__xxx__
的,也就是以雙下划線開頭,並且以雙下划線結尾的,是特殊變量,特殊變量是可以直接訪問的,不是private變量,所以,不能用__name__
、__score__
這樣的變量名。
有些時候,你會看到以一個下划線開頭的實例變量名,比如_name
,這樣的實例變量外部是可以訪問的,但是,按照約定俗成的規定,當你看到這樣的變量時,意思就是,“雖然我可以被訪問,但是,請把我視為私有變量,不要隨意訪問”。
雙下划線開頭的實例變量是不是一定不能從外部訪問呢?其實也不是。不能直接訪問__name
是因為Python解釋器對外把__name
變量改成了_Student__name
,所以,仍然可以通過_Student__name
來訪問__name
變量:
>>> bart._Student__name 'Bart Simpson'
但是強烈建議你不要這么干,因為不同版本的Python解釋器可能會把__name
改成不同的變量名。
總的來說就是,Python本身沒有任何機制阻止你干壞事,一切全靠自覺。
最后注意下面的這種錯誤寫法:
>>> bart = Student('Bart Simpson', 98) >>> bart.get_name() 'Bart Simpson' >>> bart.__name = 'New Name' # 設置__name變量! >>> bart.__name 'New Name'
表面上看,外部代碼“成功”地設置了__name
變量,但實際上這個__name
變量和class內部的__name
變量不是一個變量!內部的__name
變量已經被Python解釋器自動改成了_Student__name
,而外部代碼給bart
新增了一個__name
變量。不信試試:
>>> bart.get_name() # get_name()內部返回self.__name 'Bart Simpson'
類對象
類對象支持兩種操作:屬性引用和實例化。
屬性引用使用和 Python 中所有的屬性引用一樣的標准語法:obj.name。
類對象創建后,類命名空間中所有的命名都是有效屬性名。所以如果類定義是這樣:
#!/usr/bin/python3 class MyClass: """一個簡單的類實例""" i = 12345 def f(self): return 'hello world' # 實例化類 x = MyClass() # 訪問類的屬性和方法 print("MyClass 類的屬性 i 為:", x.i) print("MyClass 類的方法 f 輸出為:", x.f())
實例化類:
# 實例化類 x = MyClass() # 訪問類的屬性和方法
以上創建了一個新的類實例並將該對象賦給局部變量 x,x 為空的對象。
執行以上程序輸出結果為:
MyClass 類的屬性 i 為: 12345 MyClass 類的方法 f 輸出為: hello world
很多類都傾向於將對象創建為有初始狀態的。因此類可能會定義一個名為 __init__() 的特殊方法(構造方法),像下面這樣:
def __init__(self): self.data = []
類定義了 __init__() 方法的話,類的實例化操作會自動調用 __init__() 方法。所以在下例中,可以這樣創建一個新的實例:
x = MyClass()
當然, __init__() 方法可以有參數,參數通過 __init__() 傳遞到類的實例化操作上。例如:
>>> class Complex: ... def __init__(self, realpart, imagpart): ... self.r = realpart ... self.i = imagpart ... >>> x = Complex(3.0, -4.5) >>> x.r, x.i (3.0, -4.5)
實例對象
現在我們可以用實例對象做什么?實例對象唯一可用的操作就是屬性引用。有兩種有效的屬性名:數據屬性和方法。
數據屬性 相當於 Smalltalk 中的"實例變量"或 C++ 中的"數據成員"。數據屬性不需要聲明;和局部變量一樣,它們會在第一次給它們賦值時生成。例如,如果 x
是上面創建的 MyClass
的實例,下面的代碼段將打印出值 16
而不會出現錯誤:
x.counter = 1 while x.counter < 10: x.counter = x.counter * 2 print(x.counter) del x.counter
實例屬性引用的另一種類型是方法。方法是"屬於"一個對象的函數。(在 Python,方法這個術語不只針對類實例:其他對象類型也可以具有方法。例如,列表對象有 append、insert、remove、sort 方法等等。但是在后面的討論中,除非明確說明,我們提到的方法特指類實例對象的方法。)
實例對象的方法的有效名稱依賴於它的類。根據定義,類中所有函數對象的屬性定義了其實例中相應的方法。所以在我們的示例中, x.f
是一個有效的方法的引用,因為 MyClass.f
是一個函數,但 x.i
不是,因為 MyClass.i
不是一個函數。但 x.f
與 MyClass.f
也不是一回事 —— 它是一個 方法對象 ,不是一個函數對象。
方法對象
通常情況下,方法在綁定之后被直接調用:
x.f()
在 MyClass
的示例中,這將返回字符串 'hello world'
。然而,也不是一定要直接調用方法: x.f
是一個方法對象,可以存儲起來以后調用。例如:
xf = x.f while True: print(xf())
會不斷地打印 hello world
。
調用方法時到底發生了什么?你可能已經注意到,上面 x.f()
的調用沒有參數,即使 f()
函數的定義指定了一個參數。該參數發生了什么問題?當然如果函數調用中缺少參數 Python 會拋出異常——即使這個參數實際上沒有使用……
實際上,你可能已經猜到了答案:方法的特別之處在於實例對象被作為函數的第一個參數傳給了函數。在我們的示例中,調用 x.f()
完全等同於 MyClass.f(x)
。一般情況下,以n 個參數的列表調用一個方法就相當於將方法所屬的對象插入到列表的第一個參數的前面,然后以新的列表調用相應的函數。
如果你還是不明白方法的工作原理,了解一下它的實現或許有幫助。引用非數據屬性的實例屬性時,會搜索它的類。如果這個命名確認為一個有效的函數對象類屬性,就會將實例對象和函數對象封裝進一個抽象對象:這就是方法對象。以一個參數列表調用方法對象時,它被重新拆封,用實例對象和原始的參數列表構造一個新的參數列表,然后函數對象調用這個新的參數列表。
類和實例變量
一般來說,實例變量用於對每一個實例都是唯一的數據,類變量用於類的所有實例共享的屬性和方法:
class Dog: kind = 'canine' # class variable shared by all instances def __init__(self, name): self.name = name # instance variable unique to each instance >>> d = Dog('Fido') >>> e = Dog('Buddy') >>> d.kind # shared by all dogs 'canine' >>> e.kind # shared by all dogs 'canine' >>> d.name # unique to d 'Fido' >>> e.name # unique to e 'Buddy'
正如在 名稱和對象 討論的, 可變 對象,例如列表和字典,的共享數據可能帶來意外的效果。例如,下面代碼中的tricks 列表不應該用作類變量,因為所有的Dog 實例將共享同一個列表:
class Dog: tricks = [] # mistaken use of a class variable def __init__(self, name): self.name = name def add_trick(self, trick): self.tricks.append(trick) >>> d = Dog('Fido') >>> e = Dog('Buddy') >>> d.add_trick('roll over') >>> e.add_trick('play dead') >>> d.tricks # unexpectedly shared by all dogs ['roll over', 'play dead']
這個類的正確設計應該使用一個實例變量:
class Dog: def __init__(self, name): self.name = name self.tricks = [] # creates a new empty list for each dog def add_trick(self, trick): self.tricks.append(trick) >>> d = Dog('Fido') >>> e = Dog('Buddy') >>> d.add_trick('roll over') >>> e.add_trick('play dead') >>> d.tricks ['roll over'] >>> e.tricks ['play dead']
補充說明
數據屬性會覆蓋同名的方法屬性;為了避免意外的命名沖突,這在大型程序中可能帶來極難發現的 bug,使用一些約定來減少沖突的機會是明智的。可能的約定包括大寫方法名稱的首字母,使用一個唯一的小寫的字符串(也許只是一個下划線)作為數據屬性名稱的前綴,或者方法使用動詞而數據屬性使用名詞。
數據屬性可以被方法引用,也可以由一個對象的普通用戶(“客戶端”)使用。換句話說,類是不能用來實現純抽象數據類型的。事實上,Python 中不可能強制隱藏數據——一切基於約定。(另一方面,如果需要,使用 C 編寫的 Python 實現可以完全隱藏實現細節並控制對象的訪問;這可以用來通過 C 語言擴展 Python。)
客戶應該謹慎的使用數據屬性——客戶可能通過隨意使用他們的數據屬性而使那些由方法維護的常量變得混亂。注意:只要能避免沖突,客戶可以向一個實例對象添加他們自己的數據屬性,而不會影響方法的正確性——再次強調,命名約定可以避免很多麻煩。
從方法內部引用數據屬性(或其他方法)並沒有快捷方式。我覺得這實際上增加了方法的可讀性:當瀏覽一個方法時,在局部變量和實例變量之間不會出現令人費解的情況。
通常,方法的第一個參數稱為 self
。這僅僅是一個約定:名字 self
對 Python 而言絕對沒有任何特殊含義。但是請注意:如果不遵循這個約定,對其他的 Python 程序員而言你的代碼可讀性就會變差,而且有些類 查看 器程序也可能是遵循此約定編寫的。
類屬性的任何函數對象都為那個類的實例定義了一個方法。函數定義代碼不一定非得定義在類中:也可以將一個函數對象賦值給類中的一個局部變量。例如:
# Function defined outside the class
def f1(self, x, y): return min(x, x+y) class C: f = f1 def g(self): return 'hello world' h = g
現在 f
、g
和 h
都是類 C
中引用函數對象的屬性,因此它們都是 C
的實例的方法 —— h
完全等同於 g
。請注意,這種做法通常只會使閱讀程序的人產生困惑。
方法可以通過使用 self
參數的方法屬性,調用其他方法:
class Bag: def __init__(self): self.data = [] def add(self, x): self.data.append(x) def addtwice(self, x): self.add(x) self.add(x)
方法可以像普通函數那樣引用全局命名。與方法關聯的全局作用域是包含這個方法的定義的模塊(類)。(類本身永遠不會作為全局作用域使用。)盡管很少有好的理由在方法中使用全局數據,全局作用域確有很多合法的用途:其一是方法可以調用導入全局作用域的函數和方法,也可以調用定義在其中的類和函數。通常,包含此方法的類也會定義在這個全局作用域,在下一節我們會了解為何一個方法要引用自己的類。
每個值都是一個對象,因此每個值都有一個類(也稱它的類型)。它存儲為 object.__class__
。
類的方法
在類地內部,使用def關鍵字可以為類定義一個方法,與一般函數定義不同,類方法必須包含參數self,且為第一個參數:
#!/usr/bin/python3 #類定義 class people: #定義基本屬性 name = '' age = 0 #定義私有屬性,私有屬性在類外部無法直接進行訪問 __weight = 0 #定義構造方法 def __init__(self,n,a,w): self.name = n self.age = a self.__weight = w def speak(self): print("%s 說: 我 %d 歲。" %(self.name,self.age)) # 實例化類 p = people('tom',10,30) p.speak()
執行以上程序輸出結果為:
tom 說: 我 10 歲。
繼承和多態
在OOP程序設計中,當我們定義一個class的時候,可以從某個現有的class繼承,新的class稱為子類(Subclass),而被繼承的class稱為基類、父類或超類(Base class、Super class)。
當然,一個語言特性不支持繼承是配不上“類”這個名字的。派生類定義的語法如下所示:
class DerivedClassName(BaseClassName): <statement-1> . . . <statement-N>
BaseClassName
必須與派生類定義在一個作用域內。用其他任意表達式代替基類的名稱也是允許的。這可以是有用的,例如,當基類定義在另一個模塊中時:
class DerivedClassName(modname.BaseClassName):
派生類定義的執行過程和基類是相同的。類對象創建后,基類會被保存。這用於解析屬性的引用:如果在類中找不到請求的屬性,搜索會在基類中繼續。如果基類本身是由別的類派生而來,這個規則會遞歸應用。
派生類的實例化沒有什么特殊之處: DerivedClassName()
創建類的一個新的實例。方法的引用按如下規則解析: 搜索對應的類的屬性,必要時沿基類鏈逐級搜索,如果找到了函數對象這個方法引用就是合法的。
派生類可以重寫基類中的方法。因為方法調用同一個對象中的其它方法時沒有特權,基類的方法調用同一個基類的方法時,可能實際上最終調用了派生類中的覆蓋方法。(對於 C++ 程序員:Python 中的所有方法實際上都是 虛
的。)
派生類中的覆蓋方法可能是想要擴充而不是簡單的替代基類中的重名方法。有一個簡單的方法可以直接調用基類方法:只要調用 BaseClassName.methodname(self, arguments)
。有時這對於客戶端也很有用。(要注意只有 BaseClassName
在同一全局作用域定義或導入時才能這樣用。)
Python 有兩個用於繼承的函數:
- 使用
isinstance()
來檢查實例類型:isinstance(obj, int)
只有obj.__class__
是int
或者是從int
派生的類時才為True
。 - 使用
issubclass()
來檢查類的繼承:issubclass(bool, int)
是True
因為bool
是int
.的子類。然而,issubclass(float, int)
為False
,因為float
不是int
的子類。
繼承,面向對象中的繼承和現實生活中的繼承相同,即:子可以繼承父的內容。
例如:
貓可以:喵喵叫、吃、喝、拉、撒
狗可以:汪汪叫、吃、喝、拉、撒
如果我們要分別為貓和狗創建一個類,那么就需要為 貓 和 狗 實現他們所有的功能,如下所示:
偽代碼:
class 貓: def 喵喵叫(self): print '喵喵叫' def 吃(self): # do something def 喝(self): # do something def 拉(self): # do something def 撒(self): # do something class 狗: def 汪汪叫(self): print '喵喵叫' def 吃(self): # do something def 喝(self): # do something def 拉(self): # do something def 撒(self): # do something 偽代碼
上述代碼不難看出,吃、喝、拉、撒是貓和狗都具有的功能,而我們卻分別的貓和狗的類中編寫了兩次。如果使用 繼承 的思想,如下實現:
動物:吃、喝、拉、撒
貓:喵喵叫(貓繼承動物的功能)
狗:汪汪叫(狗繼承動物的功能)
偽代碼:
class 動物: def 吃(self): # do something def 喝(self): # do something def 拉(self): # do something def 撒(self): # do something # 在類后面括號中寫入另外一個類名,表示當前類繼承另外一個類 class 貓(動物): def 喵喵叫(self): print '喵喵叫' # 在類后面括號中寫入另外一個類名,表示當前類繼承另外一個類 class 狗(動物): def 汪汪叫(self): print '喵喵叫' 偽代碼
實例:
class Animal: def eat(self): print "%s 吃 " %self.name def drink(self): print "%s 喝 " %self.name def shit(self): print "%s 拉 " %self.name def pee(self): print "%s 撒 " %self.name class Cat(Animal): def __init__(self, name): self.name = name self.breed = '貓' def cry(self): print '喵喵叫' class Dog(Animal): def __init__(self, name): self.name = name self.breed = '狗' def cry(self): print '汪汪叫' # ######### 執行 ######### c1 = Cat('小白家的小黑貓') c1.eat() c2 = Cat('小黑的小白貓') c2.drink() d1 = Dog('胖子家的小瘦狗') d1.eat() 代碼實例
所以,對於面向對象的繼承來說,其實就是將多個類共有的方法提取到父類中,子類僅需繼承父類而不必一一實現每個方法。
注:除了子類和父類的稱謂,你可能看到過 派生類 和 基類 ,他們與子類和父類只是叫法不同而已。
學習了繼承的寫法之后,我們用代碼來是上述阿貓阿狗的功能:
class Animal: def eat(self): print "%s 吃 " %self.name def drink(self): print "%s 喝 " %self.name def shit(self): print "%s 拉 " %self.name def pee(self): print "%s 撒 " %self.name class Cat(Animal): def __init__(self, name): self.name = name self.breed = '貓' def cry(self): print '喵喵叫' class Dog(Animal): def __init__(self, name): self.name = name self.breed = '狗' def cry(self): print '汪汪叫' # ######### 執行 ######### c1 = Cat('小白家的小黑貓') c1.eat() c2 = Cat('小黑的小白貓') c2.drink() d1 = Dog('胖子家的小瘦狗') d1.eat() 代碼實例
比如,我們已經編寫了一個名為Animal
的class,有一個run()
方法可以直接打印:
class Animal(object): def run(self): print('Animal is running...')
當我們需要編寫Dog
和Cat
類時,就可以直接從Animal
類繼承:
class Dog(Animal): pass class Cat(Animal): pass
對於Dog
來說,Animal
就是它的父類,對於Animal
來說,Dog
就是它的子類。Cat
和Dog
類似。
繼承有什么好處?最大的好處是子類獲得了父類的全部功能。由於Animial
實現了run()
方法,因此,Dog
和Cat
作為它的子類,什么事也沒干,就自動擁有了run()
方法:
dog = Dog() dog.run() cat = Cat() cat.run()
運行結果如下:
Animal is running...
Animal is running...
當然,也可以對子類增加一些方法,比如Dog類:
class Dog(Animal): def run(self): print('Dog is running...') def eat(self): print('Eating meat...')
繼承的第二個好處需要我們對代碼做一點改進。你看到了,無論是Dog
還是Cat
,它們run()
的時候,顯示的都是Animal is running...
,符合邏輯的做法是分別顯示Dog is running...
和Cat is running...
,因此,對Dog
和Cat
類改進如下:
class Dog(Animal): def run(self): print('Dog is running...') class Cat(Animal): def run(self): print('Cat is running...')
再次運行,結果如下:
Dog is running... Cat is running...
當子類和父類都存在相同的run()
方法時,我們說,子類的run()
覆蓋了父類的run()
,在代碼運行的時候,總是會調用子類的run()
。這樣,我們就獲得了繼承的另一個好處:多態。
要理解什么是多態,我們首先要對數據類型再作一點說明。當我們定義一個class的時候,我們實際上就定義了一種數據類型。我們定義的數據類型和Python自帶的數據類型,比如str、list、dict沒什么兩樣:
a = list() # a是list類型 b = Animal() # b是Animal類型 c = Dog() # c是Dog類型
判斷一個變量是否是某個類型可以用isinstance()
判斷:
>>> isinstance(a, list) True >>> isinstance(b, Animal) True >>> isinstance(c, Dog) True
看來a
、b
、c
確實對應着list
、Animal
、Dog
這3種類型。
但是等等,試試:
>>> isinstance(c, Animal) True
>>> isinstance(c, Animal) True
看來c
不僅僅是Dog
,c
還是Animal
!
不過仔細想想,這是有道理的,因為Dog
是從Animal
繼承下來的,當我們創建了一個Dog
的實例c
時,我們認為c
的數據類型是Dog
沒錯,但c
同時也是Animal
也沒錯,Dog
本來就是Animal
的一種!
所以,在繼承關系中,如果一個實例的數據類型是某個子類,那它的數據類型也可以被看做是父類。但是,反過來就不行:
>>> b = Animal() >>> isinstance(b, Dog) False
Dog
可以看成Animal
,但Animal
不可以看成Dog
。
要理解多態的好處,我們還需要再編寫一個函數,這個函數接受一個Animal
類型的變量:
def run_twice(animal): animal.run() animal.run()
當我們傳入Animal
的實例時,run_twice()
就打印出:
>>> run_twice(Animal()) Animal is running... Animal is running...
當我們傳入Dog
的實例時,run_twice()
就打印出:
>>> run_twice(Dog()) Dog is running... Dog is running...
當我們傳入Cat
的實例時,run_twice()
就打印出:
>>> run_twice(Cat()) Cat is running... Cat is running...
看上去沒啥意思,但是仔細想想,現在,如果我們再定義一個Tortoise
類型,也從Animal
派生:
class Tortoise(Animal): def run(self): print('Tortoise is running slowly...')
當我們調用run_twice()
時,傳入Tortoise
的實例:
>>> run_twice(Tortoise()) Tortoise is running slowly... Tortoise is running slowly...
你會發現,新增一個Animal
的子類,不必對run_twice()
做任何修改,實際上,任何依賴Animal
作為參數的函數或者方法都可以不加修改地正常運行,原因就在於多態。
多態的好處就是,當我們需要傳入Dog
、Cat
、Tortoise
……時,我們只需要接收Animal
類型就可以了,因為Dog
、Cat
、Tortoise
……都是Animal
類型,然后,按照Animal
類型進行操作即可。由於Animal
類型有run()
方法,因此,傳入的任意類型,只要是Animal
類或者子類,就會自動調用實際類型的run()
方法,這就是多態的意思:
對於一個變量,我們只需要知道它是Animal
類型,無需確切地知道它的子類型,就可以放心地調用run()
方法,而具體調用的run()
方法是作用在Animal
、Dog
、Cat
還是Tortoise
對象上,由運行時該對象的確切類型決定,這就是多態真正的威力:調用方只管調用,不管細節,而當我們新增一種Animal
的子類時,只要確保run()
方法編寫正確,不用管原來的代碼是如何調用的。這就是著名的“開閉”原則:
對擴展開放:允許新增Animal
子類;
對修改封閉:不需要修改依賴Animal
類型的run_twice()
等函數。
繼承還可以一級一級地繼承下來,就好比從爺爺到爸爸、再到兒子這樣的關系。而任何類,最終都可以追溯到根類object,這些繼承關系看上去就像一顆倒着的樹。比如如下的繼承樹:
Python 同樣支持類的繼承,如果一種語言不支持繼承,類就沒有什么意義。派生類的定義如下所示:
class DerivedClassName(BaseClassName1): <statement-1> . . . <statement-N>
需要注意圓括號中基類的順序,若是基類中有相同的方法名,而在子類使用時未指定,python從左至右搜索 即方法在子類中未找到時,從左到右查找基類中是否包含方法。
BaseClassName(示例中的基類名)必須與派生類定義在一個作用域內。除了類,還可以用表達式,基類定義在另一個模塊中時這一點非常有用:
class DerivedClassName(modname.BaseClassName):
實例
#!/usr/bin/python3 #類定義 class people: #定義基本屬性 name = '' age = 0 #定義私有屬性,私有屬性在類外部無法直接進行訪問 __weight = 0 #定義構造方法 def __init__(self,n,a,w): self.name = n self.age = a self.__weight = w def speak(self): print("%s 說: 我 %d 歲。" %(self.name,self.age)) #單繼承示例 class student(people): grade = '' def __init__(self,n,a,w,g): #調用父類的構函 people.__init__(self,n,a,w) self.grade = g #覆寫父類的方法 def speak(self): print("%s 說: 我 %d 歲了,我在讀 %d 年級"%(self.name,self.age,self.grade)) s = student('ken',10,60,3) s.speak()
執行以上程序輸出結果為:
ken 說: 我 10 歲了,我在讀 3 年級
多繼承
那么問題又來了,多繼承呢?
- 是否可以繼承多個類
- 如果繼承的多個類每個類中都定了相同的函數,那么那一個會被使用呢?
1、Python的類可以繼承多個類,Java和C#中則只能繼承一個類
2、Python的類如果繼承了多個類,那么其尋找方法的方式有兩種,分別是:深度優先和廣度優先
- 當類是經典類時,多繼承情況下,會按照深度優先方式查找
- 當類是新式類時,多繼承情況下,會按照廣度優先方式查找
經典類和新式類,從字面上可以看出一個老一個新,新的必然包含了跟多的功能,也是之后推薦的寫法,從寫法上區分的話,如果 當前類或者父類繼承了object類,那么該類便是新式類,否則便是經典類。
經典類多繼承:
class D: def bar(self): print 'D.bar' class C(D): def bar(self): print 'C.bar' class B(D): def bar(self): print 'B.bar' class A(B, C): def bar(self): print 'A.bar' a = A() # 執行bar方法時 # 首先去A類中查找,如果A類中沒有,則繼續去B類中找,如果B類中么有,則繼續去D類中找,如果D類中么有,則繼續去C類中找,如果還是未找到,則報錯 # 所以,查找順序:A --> B --> D --> C # 在上述查找bar方法的過程中,一旦找到,則尋找過程立即中斷,便不會再繼續找了 a.bar() 經典類多繼承
新式類多繼承:
class D(object): def bar(self): print 'D.bar' class C(D): def bar(self): print 'C.bar' class B(D): def bar(self): print 'B.bar' class A(B, C): def bar(self): print 'A.bar' a = A() # 執行bar方法時 # 首先去A類中查找,如果A類中沒有,則繼續去B類中找,如果B類中么有,則繼續去C類中找,如果C類中么有,則繼續去D類中找,如果還是未找到,則報錯 # 所以,查找順序:A --> B --> C --> D # 在上述查找bar方法的過程中,一旦找到,則尋找過程立即中斷,便不會再繼續找了 a.bar() 新式類多繼承
經典類:首先去A類中查找,如果A類中沒有,則繼續去B類中找,如果B類中么有,則繼續去D類中找,如果D類中么有,則繼續去C類中找,如果還是未找到,則報錯
新式類:首先去A類中查找,如果A類中沒有,則繼續去B類中找,如果B類中么有,則繼續去C類中找,如果C類中么有,則繼續去D類中找,如果還是未找到,則報錯
注意:在上述查找過程中,一旦找到,則尋找過程立即中斷,便不會再繼續找了
Python同樣有限的支持多繼承形式。多繼承的類定義形如下例:
class DerivedClassName(Base1, Base2, Base3): <statement-1> . . . <statement-N>
對於大多數用途,在最簡單的情況下,你可以認為繼承自父類的屬性搜索是從左到右的深度優先搜索,不會在同一個類中搜索兩次,即使層次會有重疊。因此,如果在 DerivedClassName
中找不到屬性,它搜索 Base1
,然后(遞歸)基類中的 Base1
,如果沒有找到,它會搜索 Base2
,依此類推。
事實上要稍微復雜一些;為了支持合作調用 super()
,方法解析的順序會動態改變。這種方法在某些其它多繼承的語言中也有並叫做call-next-method,它比單繼承語言中的super調用更強大。
動態調整順序是必要的,因為所有的多繼承都會有一個或多個菱形關系(從最底部的類向上,至少會有一個父類可以通過多條路徑訪問到)。例如,所有的類都繼承自 object
,所以任何多繼承都會有多條路徑到達 object
。為了防止基類被重復訪問,動態算法線性化搜索順序,每個類都按從左到右的順序特別指定了順序,每個父類只調用一次,這是單調的(也就是說一個類被繼承時不會影響它祖先的次序)。所有這些特性使得設計可靠並且可擴展的多繼承類成為可能。有關詳細信息,請參閱https://www.python.org/download/releases/2.3/mro/。
需要注意圓括號中父類的順序,若是父類中有相同的方法名,而在子類使用時未指定,python從左至右搜索 即方法在子類中未找到時,從左到右查找父類中是否包含方法。
#!/usr/bin/python3 #類定義 class people: #定義基本屬性 name = '' age = 0 #定義私有屬性,私有屬性在類外部無法直接進行訪問 __weight = 0 #定義構造方法 def __init__(self,n,a,w): self.name = n self.age = a self.__weight = w def speak(self): print("%s 說: 我 %d 歲。" %(self.name,self.age)) #單繼承示例 class student(people): grade = '' def __init__(self,n,a,w,g): #調用父類的構函 people.__init__(self,n,a,w) self.grade = g #覆寫父類的方法 def speak(self): print("%s 說: 我 %d 歲了,我在讀 %d 年級"%(self.name,self.age,self.grade)) #另一個類,多重繼承之前的准備 class speaker(): topic = '' name = '' def __init__(self,n,t): self.name = n self.topic = t def speak(self): print("我叫 %s,我是一個演說家,我演講的主題是 %s"%(self.name,self.topic)) #多重繼承 class sample(speaker,student): a ='' def __init__(self,n,a,w,g,t): student.__init__(self,n,a,w,g) speaker.__init__(self,n,t) test = sample("Tim",25,80,4,"Python") test.speak() #方法名同,默認調用的是在括號中排前地父類的方法
執行以上程序輸出結果為:
我叫 Tim,我是一個演說家,我演講的主題是 Python
總結
- 面向對象是一種編程方式,此編程方式的實現是基於對 類 和 對象 的使用
- 類 是一個模板,模板中包裝了多個“函數”供使用
- 對象,根據模板創建的實例(即:對象),實例用於調用被包裝在類中的函數
- 面向對象三大特性:封裝、繼承和多態
靜態語言 vs 動態語言
對於靜態語言(例如Java)來說,如果需要傳入Animal
類型,則傳入的對象必須是Animal
類型或者它的子類,否則,將無法調用run()
方法。
對於Python這樣的動態語言來說,則不一定需要傳入Animal
類型。我們只需要保證傳入的對象有一個run()
方法就可以了:
class Timer(object): def run(self): print('Start...')
這就是動態語言的“鴨子類型”,它並不要求嚴格的繼承體系,一個對象只要“看起來像鴨子,走起路來像鴨子”,那它就可以被看做是鴨子。
Python的“file-like object“就是一種鴨子類型。對真正的文件對象,它有一個read()
方法,返回其內容。但是,許多對象,只要有read()
方法,都被視為“file-like object“。許多函數接收的參數就是“file-like object“,你不一定要傳入真正的文件對象,完全可以傳入任何實現了read()
方法的對象。
小結
繼承可以把父類的所有功能都直接拿過來,這樣就不必重零做起,子類只需要新增自己特有的方法,也可以把父類不適合的方法覆蓋重寫。
動態語言的鴨子類型特點決定了繼承不像靜態語言那樣是必須的。
方法重寫
如果你的父類方法的功能不能滿足你的需求,你可以在子類重寫你父類的方法,實例如下:
#!/usr/bin/python3 class Parent: # 定義父類 def myMethod(self): print ('調用父類方法') class Child(Parent): # 定義子類 def myMethod(self): print ('調用子類方法') c = Child() # 子類實例 c.myMethod() # 子類調用重寫方法
執行以上程序輸出結果為:
調用子類方法
類屬性與方法
類的私有屬性
__private_attrs:兩個下划線開頭,聲明該屬性為私有,不能在類地外部被使用或直接訪問。在類內部的方法中使用時 self.__private_attrs。
類的方法
在類地內部,使用def關鍵字可以為類定義一個方法,與一般函數定義不同,類方法必須包含參數self,且為第一個參數
類的私有方法
__private_method:兩個下划線開頭,聲明該方法為私有方法,不能在類地外部調用。在類的內部調用 slef.__private_methods。
私有變量
在 Python 中不存在只能從對象內部訪問的“私有”實例變量。然而,有一項大多數 Python 代碼都遵循的公約:帶有下划線(例如_spam
)前綴的名稱應被視為非公開的 API 的一部分(無論是函數、 方法還是數據成員)。它應該被當做一個實現細節,將來如果有變化恕不另行通知。
因為有一個合理的類私有成員的使用場景(即為了避免名稱與子類定義的名稱沖突),Python 對這種機制有簡單的支持,叫做name mangling。__spam
形式的任何標識符(前面至少兩個下划線,后面至多一個下划線)將被替換為 _classname__spam
, classname
是當前類的名字。此重整無需考慮該標識符的句法位置,只要它出現在類的定義的范圍內。
Name mangling 有利於子類重寫父類的方法而不會破壞類內部的方法調用。例如:
class Mapping: def __init__(self, iterable): self.items_list = [] self.__update(iterable) def update(self, iterable): for item in iterable: self.items_list.append(item) __update = update # private copy of original update() method class MappingSubclass(Mapping): def update(self, keys, values): # provides new signature for update() # but does not break __init__() for item in zip(keys, values): self.items_list.append(item)
請注意名稱改編的目的主要是避免發生意外;訪問或者修改私有變量仍然是可能的。這在特殊情況下,例如調試的時候,還是有用的。
注意傳遞給 exec
或 eval()
的代碼沒有考慮要將調用類的類名當作當前類;這類似於 global
語句的效果,影響只限於一起進行字節編譯的代碼。相同的限制適用於 getattr()
、setattr()
和 delattr()
,以及直接引用 __dict__
時。
實例
類的私有屬性實例如下:
#!/usr/bin/python3 class JustCounter: __secretCount = 0 # 私有變量 publicCount = 0 # 公開變量 def count(self): self.__secretCount += 1 self.publicCount += 1 print (self.__secretCount) counter = JustCounter() counter.count() counter.count() print (counter.publicCount) print (counter.__secretCount) # 報錯,實例不能訪問私有變量
執行以上程序輸出結果為:
1 2 2 Traceback (most recent call last): File "test.py", line 16, in <module> print (counter.__secretCount) # 報錯,實例不能訪問私有變量 AttributeError: 'JustCounter' object has no attribute '__secretCount'
類的私有方法實例如下:
#!/usr/bin/python3 class Site: def __init__(self, name, url): self.name = name # public self.__url = url # private def who(self): print('name : ', self.name) print('url : ', self.__url) def __foo(self): # 私有方法 print('這是私有方法') def foo(self): # 公共方法 print('這是公共方法') self.__foo() x = Site('python', 'www.python.com') x.who() # 正常輸出 x.foo() # 正常輸出 x.__foo() # 報錯 #報錯,外部不能調用私有方法
類的專有方法:
- __init__ : 構造函數,在生成對象時調用
- __del__ : 析構函數,釋放對象時使用
- __repr__ : 打印,轉換
- __setitem__ : 按照索引賦值
- __getitem__: 按照索引獲取值
- __len__: 獲得長度
- __cmp__: 比較運算
- __call__: 函數調用
- __add__: 加運算
- __sub__: 減運算
- __mul__: 乘運算
- __div__: 除運算
- __mod__: 求余運算
- __pow__: 稱方
運算符重載
Python同樣支持運算符重載,我么可以對類的專有方法進行重載,實例如下:
#!/usr/bin/python3 class Vector: def __init__(self, a, b): self.a = a self.b = b def __str__(self): return 'Vector (%d, %d)' % (self.a, self.b) def __add__(self,other): return Vector(self.a + other.a, self.b + other.b) v1 = Vector(2,10) v2 = Vector(5,-2) print (v1 + v2)
以上代碼執行結果如下所示:
Vector(7,8)
獲取對象信息
使用type()
首先,我們來判斷對象類型,使用type()
函數:
基本類型都可以用type()
判斷:
>>> type(123) <class 'int'> >>> type('str') <class 'str'> >>> type(None) <type(None) 'NoneType'>
如果一個變量指向函數或者類,也可以用type()
判斷:
>>> type(abs) <class 'builtin_function_or_method'> >>> type(a) <class '__main__.Animal'>
但是type()
函數返回的是什么類型呢?它返回對應的Class類型。如果我們要在if
語句中判斷,就需要比較兩個變量的type類型是否相同:
>>> type(123)==type(456) True >>> type(123)==int True >>> type('abc')==type('123') True >>> type('abc')==str True >>> type('abc')==type(123) False
判斷基本數據類型可以直接寫int
,str
等,但如果要判斷一個對象是否是函數怎么辦?可以使用types
模塊中定義的常量:
>>> import types >>> def fn(): ... pass ... >>> type(fn)==types.FunctionType True >>> type(abs)==types.BuiltinFunctionType True >>> type(lambda x: x)==types.LambdaType True >>> type((x for x in range(10)))==types.GeneratorType True
使用isinstance()
對於class的繼承關系來說,使用type()
就很不方便。我們要判斷class的類型,可以使用isinstance()
函數。
我們回顧上次的例子,如果繼承關系是:
object -> Animal -> Dog -> Husky
那么,isinstance()
就可以告訴我們,一個對象是否是某種類型。先創建3種類型的對象:
>>> a = Animal() >>> d = Dog() >>> h = Husky()
然后,判斷:
>>> isinstance(h, Husky) True
沒有問題,因為h
變量指向的就是Husky對象。
再判斷:
>>> isinstance(h, Dog) True
h
雖然自身是Husky類型,但由於Husky是從Dog繼承下來的,所以,h
也還是Dog類型。換句話說,isinstance()
判斷的是一個對象是否是該類型本身,或者位於該類型的父繼承鏈上。
因此,我們可以確信,h
還是Animal類型:
>>> isinstance(h, Animal) True
同理,實際類型是Dog的d
也是Animal類型:
>>> isinstance(d, Dog) and isinstance(d, Animal) True
但是,d
不是Husky類型:
>>> isinstance(d, Husky) False
能用type()
判斷的基本類型也可以用isinstance()
判斷:
>>> isinstance('a', str) True >>> isinstance(123, int) True >>> isinstance(b'a', bytes) True
並且還可以判斷一個變量是否是某些類型中的一種,比如下面的代碼就可以判斷是否是list或者tuple:
>>> isinstance([1, 2, 3], (list, tuple)) True >>> isinstance((1, 2, 3), (list, tuple)) True
使用dir()
如果要獲得一個對象的所有屬性和方法,可以使用dir()
函數,它返回一個包含字符串的list,比如,獲得一個str對象的所有屬性和方法:
>>> dir('ABC') ['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__',
'__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center',
'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier',
'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind',
'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
類似__xxx__
的屬性和方法在Python中都是有特殊用途的,比如__len__
方法返回長度。在Python中,如果你調用len()
函數試圖獲取一個對象的長度,實際上,在len()
函數內部,它自動去調用該對象的__len__()
方法,所以,下面的代碼是等價的:
>>> len('ABC') 3 >>> 'ABC'.__len__() 3
我們自己寫的類,如果也想用len(myObj)
的話,就自己寫一個__len__()
方法:
>>> class MyDog(object): ... def __len__(self): ... return 100 ... >>> dog = MyDog() >>> len(dog) 100
剩下的都是普通屬性或方法,比如lower()
返回小寫的字符串:
>>> 'ABC'.lower() 'abc'
僅僅把屬性和方法列出來是不夠的,配合getattr()
、setattr()
以及hasattr()
,我們可以直接操作一個對象的狀態:
>>> class MyObject(object): ... def __init__(self): ... self.x = 9 ... def power(self): ... return self.x * self.x ... >>> obj = MyObject()
緊接着,可以測試該對象的屬性:
>>> hasattr(obj, 'x') # 有屬性'x'嗎? True >>> obj.x 9 >>> hasattr(obj, 'y') # 有屬性'y'嗎? False >>> setattr(obj, 'y', 19) # 設置一個屬性'y' >>> hasattr(obj, 'y') # 有屬性'y'嗎? True >>> getattr(obj, 'y') # 獲取屬性'y' 19 >>> obj.y # 獲取屬性'y' 19
如果試圖獲取不存在的屬性,會拋出AttributeError的錯誤:
>>> getattr(obj, 'z') # 獲取屬性'z' Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'MyObject' object has no attribute 'z'
可以傳入一個default參數,如果屬性不存在,就返回默認值:
>>> getattr(obj, 'z', 404) # 獲取屬性'z',如果不存在,返回默認值404 404
也可以獲得對象的方法:
>>> hasattr(obj, 'power') # 有屬性'power'嗎? True >>> getattr(obj, 'power') # 獲取屬性'power' <bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>> >>> fn = getattr(obj, 'power') # 獲取屬性'power'並賦值到變量fn >>> fn # fn指向obj.power <bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>> >>> fn() # 調用fn()與調用obj.power()是一樣的 81
小結
通過內置的一系列函數,我們可以對任意一個Python對象進行剖析,拿到其內部的數據。要注意的是,只有在不知道對象信息的時候,我們才會去獲取對象信息。如果可以直接寫:
sum = obj.x + obj.y
就不要寫:
sum = getattr(obj, 'x') + getattr(obj, 'y')
一個正確的用法的例子如下:
def readImage(fp): if hasattr(fp, 'read'): return readData(fp) return None
假設我們希望從文件流fp中讀取圖像,我們首先要判斷該fp對象是否存在read方法,如果存在,則該對象是一個流,如果不存在,則無法讀取。hasattr()
就派上了用場。
請注意,在Python這類動態語言中,根據鴨子類型,有read()
方法,不代表該fp對象就是一個文件流,它也可能是網絡流,也可能是內存中的一個字節流,但只要read()
方法返回的是有效的圖像數據,就不影響讀取圖像的功能。
實例屬性和類屬性
由於Python是動態語言,根據類創建的實例可以任意綁定屬性。給實例綁定屬性的方法是通過實例變量,或者通過self
變量:
class Student(object): def __init__(self, name): self.name = name s = Student('Bob') s.score = 90
但是,如果Student
類本身需要綁定一個屬性呢?可以直接在class中定義屬性,這種屬性是類屬性,歸Student
類所有:
class Student(object): name = 'Student'
當我們定義了一個類屬性后,這個屬性雖然歸類所有,但類的所有實例都可以訪問到。來測試一下:
>>> class Student(object): ... name = 'Student' ... >>> s = Student() # 創建實例s >>> print(s.name) # 打印name屬性,因為實例並沒有name屬性,所以會繼續查找class的name屬性 Student >>> print(Student.name) # 打印類的name屬性 Student >>> s.name = 'Michael' # 給實例綁定name屬性 >>> print(s.name) # 由於實例屬性優先級比類屬性高,因此,它會屏蔽掉類的name屬性 Michael >>> print(Student.name) # 但是類屬性並未消失,用Student.name仍然可以訪問 Student >>> del s.name # 如果刪除實例的name屬性 >>> print(s.name) # 再次調用s.name,由於實例的name屬性沒有找到,類的name屬性就顯示出來了 Student
從上面的例子可以看出,在編寫程序的時候,千萬不要把實例屬性和類屬性使用相同的名字,因為相同名稱的實例屬性將屏蔽掉類屬性,但是當你刪除實例屬性后,再使用相同的名稱,訪問到的將是類屬性。