本節內容
- 面向對象的概念
- 類的封裝
- 類的繼承
- 類的多態
- 靜態方法、類方法 和 屬性方法
- 類的特殊成員方法
- 繼承層級關系中子類的實例對象對屬性的查找順序問題
一、面向對象的概念
1. "面向對象(OOP)"是什么?
簡單點說,“面向對象”是一種編程范式,而編程范式是按照不同的編程特點總結出來的編程方式。俗話說,條條大路通羅馬,也就說我們使用不同的方法都可以達到最終的目的,但是有些辦法比較快速、安全且效果好,有些方法則效率低下且效果不盡人意。同樣,編程也是為了解決問題,而解決問題可以有多種不同的視角和思路,前人把其中一些普遍適用且行之有效的編程模式歸結為“范式”。常見的編程范式有:
- 面向過程編程:OPP(Procedure Oriented Programing)
- 面向對象編程:OOP(Object Oriented Programing)
- 函數式編程:(Functional Programing)
面向過程編程的步驟:
1)分析出解決問題所需要的步驟;
2)用函數把這些步驟一次實現;
3)一個一個地調用這些函數來解決問題;
面向對象編程的步驟:
1)把構成問題的事務分解、抽象成各個對象;
2)結合這些對象的共有屬性,抽象出類;
3)類層次化結構設計--繼承 和 合成;
4)用類和實例進行設計和實現來解決問題。
關于面向對象編程 與 面向過程編程的區別與優缺點可以參考這篇文章
2. 面向對象編程的特點
面向對象編程達到了軟件工程的3個目標:重用性、靈活性、擴展性,而這些目標是通過以下幾個主要特點實現的:
- 封裝: 可以隱藏實現細節,使代碼模塊化
- 繼承: 可以通過擴展已存在的類來實現代碼重用,避免重復編寫相同的代碼
- 多態: 封裝和繼承的目的都是為了實現 代碼重用, 而多態是為了實現 接口重用,使得類在繼承和派生的時候能夠保證任何一個類的實例都能正確調用約定好的屬性和方法。簡單來說,就是為了約定相同的屬性和方法名稱。
需要說明的是,Python不像Java中又專門的“接口”定義,Python中的接口與類沒有什么區別,但是我們可以通過在一個用于當做接口的類中所定義的方法體中
raise NotImplementedError
異常,來強制子類必須重新實現該方法。
3. 面向對象編程的使用場景
我們知道,Python既可以面向過程編程,也可以面向對象編程。那么什么場景下應該使用面向對象編程呢?如果我們僅僅是寫一個簡單的腳本來跑一些簡單的任務,我們直接用面向過程編程就好了,簡單,快速。當我們需要實現一個復雜的系統時,或者以下場景下,就需要使用面向對象編程:
- 場景1: 當多個函數需要傳入多個共同的參數時,可以將這些函數封裝到一個類中,并將這些參數提取為這個類的屬性;
- 場景2: 當需要根據一個模板來創建某些東西時,可以通過類來完成。
二、類的封裝
封裝是面向對象的主要特征之一,是對象和類概念的主要特性。簡單的說,一個類就是一個封裝了數據以及操作這些數據的方法的邏輯實體,它向外暴露部分數據和方法,屏蔽具體的實現細節。除此之外,在一個對象內部,某些數據或方法可以是私有的,這些私有的數據或方法是不允許外界訪問的。通過這種方式,對象對內部數據提供了不同級別的保護以防止程序中無關的部分意外的改變或錯誤使用了對象的私有部分,比如java中修飾類變量和方法的相關關鍵字有:private、protected, public等。下面我們通過類的定義和實例化的實例來說明一下Python中的是如何實現對這些不同等級數據的保護的。
1. 類的定義
類的定義是對顯示事務的抽象過程和能力,類是一個對象/實例的模板,也是一個特殊的對象/實例(因為Pythobn中一切皆對象,所以類本身也是一個對象)
現在我們來定義個Person類,它有以下3個屬性:
- nationality:國籍
- name:姓名
- id:身份證號碼
假設現在我們有以下幾個前提:
- 所有人的國籍基本都是相同的,且允許直接通過類或實例來訪問,允許隨意修改
- 大部分人的姓名是不同的,且允許直接通過類的實例來訪問和隨意修改
- 所有人的身份證號碼都是不一樣的,且不允許直接通過類或實例來訪問或隨意修改
import uuid
class Person(object):
nationality = 'China'
def __init__(self, name):
self.name = name
self.__id = str(uuid.uuid1())
def hello(self):
print('Hi, i am %s, from %s, my id is %s' % (self.name, self.nationality, self.__id))
def get_and_print_id(self):
print(self.__id)
return self.__id
2. 類的實例化
類實例化的方式:類名([參數...])
,參數是__init__方法中除了第一個self參數之外的其他參數,上面定義的這個Person類中,實例化時需要傳遞的參數只有一個name。比如我們來實例化3個Person對象,他們的name分別是 tom 和 jerry:
tom = Person('tom')
jerry = Person('jerry')
jack = Person('jack')
3. 不同保護等級的屬性說明
公有屬性/類屬性
直接定義在class下的屬性就是公有屬性/類屬性,比如上面那個Person類中的nationality屬性。“公有”的意思是這個屬性是這個類的所有實例對象共同所有的,因此默認情況下這個屬性值值保留一份,而不會為該類的每個實例都保存一份。
print(Person.nationality, tom.nationality, jerry.nationality, jack.nationality)
tom.nationality = 'USA'
print(Person.nationality, tom.nationality, jerry.nationality, jack.nationality)
Person.nationality = 'India'
print(Person.nationality, tom.nationality, jerry.nationality, jack.nationality)
輸出結果如下:
China China China China
China USA China China
India USA India India
結論:
- 公有屬性/靜態屬性 可以直接通過類直接訪問,也可以直接通過實例進行訪問;
- 通過類的某個實例對公有屬性進行修改,實際上對為該實例添加了一個與類的公有屬性名稱相同的成員屬性,對真正的公有屬性是沒有影響的,因此它不會影響其他實例獲取的該公有屬性的值;
- 通過類對公有屬性進行修改,必然是會改變公有屬性原有的值,他對該類所有的實例是都有影響的。
成員屬性/實例屬性
成員屬性,又稱成員變量 或 實例屬性,也就是說這些屬性是 該類的每個實例對象單獨持有的屬性。成員屬性需要在類的__init__方法中進行聲明,比如上面的Person類中定義的name屬性就是一個成員屬性。
print(tom.name, jerry.name, jack.name)
jerry.name = 'jerry01'
print(tom.name, jerry.name, jack.name)
輸出結果:
tom jerry jack
tom jerry01 jack
來看看能不能直接通過類訪問成員屬性
print(Person.name)
輸出結果:
Traceback (most recent call last):
...
AttributeError: type object 'Person' has no attribute 'name'
結論:
- 成員屬性可以直接通過實例對象來訪問和更改;
- 成員屬性是每個實例對象獨有的,某個實例對象的成員屬性被更改不會影響其他實例對象的相同屬性的值;
- 成員屬性的值不能通過類來訪問和修改;
私有屬性
私有屬性和成員屬性一樣,是在__init__方法中進行聲明,但是屬性名需要以雙下劃線__開頭,比如上面定義的Person中的__id屬性。私有屬性是一種特殊的成員屬性,它只允許在實例對象的內部(成員方法或私有方法中)訪問,而不允許在實例對象的外部通過實例對象或類來直接訪問,也不能被子類繼承。
通過實例對象訪問私有屬性:
print(tom.__id)
輸出結果
Traceback (most recent call last):
...
AttributeError: 'Person' object has no attribute '__id'
通過類訪問私有屬性:
print(Person.__id)
輸出結果:
Traceback (most recent call last):
...
AttributeError: type object 'Person' has no attribute '__id'
通過類的成員方法訪問私有屬性:
tom.hello()
jerry.hello()
jack.hello()
輸出結果:
Hi, i am tom, from China, my id is b6ac08c6-9dae-11e7-993f-208984d7aa83
Hi, i am jerry, from China, my id is b6ac08c7-9dae-11e7-b508-208984d7aa83
Hi, i am jack, from China, my id is b6ac08c8-9dae-11e7-9ace-208984d7aa83
結論:
- 私有變量不能通過類直接訪問;
- 私有變量也不能通過實例對象直接訪問;
- 私有變量可以通過成員方法進行訪問。
那么要訪問私有變量怎么辦呢? 有兩種辦法:
辦法1:通過一個專門的成員方法返回該私有變量的值,比如上面定義的get_id()方法,搞過java的同學很自然就會想到java類中的set和get方法。
tom_id = tom.get_id()
jerry_id = jerry.get_id()
jack_id = jack.get_id()
print(tom_id, jerry_id, jack_id)
輸出結果:
46bc6b5c-9dd6-11e7-8306-208984d7aa83 46cbfe68-9dd6-11e7-b5d1-208984d7aa83 46cbfe69-9dd6-11e7-9b5c-208984d7aa83
辦法2:通過 實例對象._類名__私有變量名
的方式來訪問
print(tom._Person__id, jerry._Person__id, jack._Person__id)
輸出結果:
e1f4ee86-9dd6-11e7-a186-208984d7aa83 e1f5b1f8-9dd6-11e7-b1c3-208984d7aa83 e1f5b1f9-9dd6-11e7-b74a-208984d7aa83
總結
- 公有屬性、成員屬性 和 私有屬性 的受保護等級是依次遞增的;
- 私有屬性 和 成員屬性 是存放在已實例化的對象中的,每個對象都會保存一份;
- 公有屬性是保存在類中的,只保存一份;
- 哪些屬性應該是公有屬性的,哪些屬性應該是私有屬性 需要根據具體業務需求來確定。
三、類的繼承
1. 繼承的相關概念
繼成 和 組合 是類的兩個最主要的關系,而 繼承 關系的類之間是有層級的。被繼承的類被稱為 父類、基類 或 超類 ;繼承的類被稱為 子類 或 派生類。
2. 繼承的作用
繼承 是一個從一般到特殊的過程, 子類可以繼承現有類的所有功能,而不需要重新實現代碼。簡單來說就是 繼承提高了代碼重用性和擴展性。
3. 繼承的分類
Python中類的繼承按照父類中的方法是否已實現可分為兩種:
- 實現繼承 :指直接繼承父類的屬性和已定義并實現的的方法;
- 接口繼承 :僅繼承父類類的屬性和方法名稱,子類必須自行實現方法的具體功能代碼。
如果是根據要繼承的父類的個數來分,有可以分為:
- 單繼承: 只繼承1個父類
- 多繼承: 繼承多個父類
通常,我們都是用 單繼承 ,很少用到 多繼承。
4. 類繼承實例
類的繼承關系
父類/基類/超類--Person
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
def walk(self):
print('%s is walking...' % self.name)
def talk(self):
print('%s is talking...' % self.name )
子類--Teacher
class Teacher(Person):
def __init__(self, name, age, level, salary):
super(Teacher, self).__init__(name, age)
self.level = level
self.salary = salary
def teach(self):
print('%s is teaching...' % self.name)
子類--Class
class Student(Person):
def __init__(self, name, age, class_):
Person.__init__(self, name, age)
self.class_ = class_
def study(self):
print('%s is studying...' % self.name)
子類實例化
t1 = Teacher('張老師', 33, '高級教師', 20000)
s1 = Student('小明', 13, '初一3班')
t1.talk()
t1.walk()
t1.teach()
s1.talk()
s1.walk()
s1.study()
輸出結果:
張老師 is talking...
張老師 is walking...
張老師 is teaching...
小明 is talking...
小明 is walking...
小明 is studying...
繼承說明
- Teacher類 和 Student類 都繼承 Person類,因此Teacher和Student是Person的子類/派生類,而Person是Teacher和Student的父類/基類/超類;
- Teacher和Student對Person的繼承屬于實現繼承,且是單繼承;
- Teacher類繼承了Person的name和age屬性,及talk()和walk()方法,并擴展了自己的level和salary屬性,及teach()方法;
- Student類繼承了Person的name和age屬性,及talk()和walk()方法,并擴展了自己的class屬性,及study()方法;
- Teacher和Student對Person類屬性和方法繼承體現了 “代碼的重用性”, 而Teacher和Student擴展的屬性和方法體現了 “靈活的擴展性”;
- 子類需要在自己的__init__方法中的第一行位置調用父類的構造方法,上面給出了兩種方法:
- super(子類名, self).__init__(父類構造參數),如super.(Teacher, self).__init__(name, age)
- 父類名.__init__(self, 父類構造參數),如Person.__init__(self, name, age),這是老式的用法。
- 子類 Teacher 和 Student 也可以在自己的類定義中 重新定義 父類中的talk()和walk()方法,改變其實現代碼,這叫做方法重寫。
關于多繼承,以及多繼承時屬性查找順序(廣度優先、深度優先)的問題會在下面進行單獨說明。
四、類的多態
多態是指,相同的成員方法名稱,但是成員方法的行為(代碼實現)卻各不相同。這里所說的多態是通過 繼承接口的方式實現的。Java中有interface,但是Python中沒有。Python中可以通過在一個成員方法體中拋出一個NotImplementedError
異常來強制繼承該接口的子類在調用該方法前必須先實現該方法的功能代碼。
接口--Animal
class Animal(object):
def __init__(self, name):
self.name = name
def walk(self):
raise NotImplemented('Subclass must implement the abstract method by self')
def talk(self):
raise NotImplemented('Subclass must implement the abstract method by self')
子類--Dog
class Dog(Animal):
pass
執行代碼:
dog = Dog('大黃')
dog.talk()
輸出結果:
Traceback (most recent call last):
...
raise NotImplemented('Subclass must implement the abstract method by self')
TypeError: 'NotImplementedType' object is not callable
可見,此時子類必須自己先實現talk方法才能調用。
實現了接口方法的子類--Dog 和 Duck
class Dog(Animal):
def talk(self):
print('%s is talking:旺旺...' % self.name)
def walk(self):
print('%s 是一條小狗,用4條腿走路' % self.name)
class Duck(Animal):
def talk(self):
print('%s is talking: 嘎嘎...' % self.name)
def walk(self):
print('%s 是一只鴨子,用兩條腿走路' % self.name)
執行代碼:
dog = Dog('大黃')
dog.talk()
dog.walk()
duck = Duck('小白')
duck.talk()
duck.walk()
輸出結果:
大黃 is talking:旺旺...
大黃 是一條小狗,用4條腿走路
小白 is talking: 嘎嘎...
小白 是一只鴨子,用兩條腿走路
由此可知:
- 接口的所有子類擁有接口中定義的所有同名的方法;
- 接口的所有子類在調用接口中定義的方法時,必須先自己實現方法代碼;
- 接口的各個子類在實現接口中同一個方法時,具體的代碼實現各不相同,這就是多態。
五、屬性方法、類方法、靜態方法
上面提到過,類中封裝的是數據和操作數據的方法。數據就是屬性,且上面已經介紹過了屬性分為:
公有屬性/類變量、成員屬性/實例變量 和 私有屬性。現在我們來說說類中的方法,類中的方法分為以下幾種:
成員方法: 上面定義的都是成員方法,通常情況下,它們與成員屬性相似,是通過類的實例對象去訪問;成員方法的第一個參數必須是當前實例對象,通常寫為self;實際上,我們也可以通過類名來調用成員方法,只是此時我們需要手動的傳遞一個該類的實例對象給成員方法的self參數,這樣用明顯不是一種優雅的方法,因此基本不會這樣使用。
私有方法: 以雙下劃線開頭的成員方法就是私有方法,與私有屬性類似,只能在實例對象內部訪問,且不能被子類繼承;私有方法的第一個參數也必須是當前實例對象本身,通常寫為self;
類方法: 以@classmethod來裝飾的成員方法就叫做類方法,它要求第一次參數必須是當前類。與公有屬性/靜態屬性 相似,除了可通過實例對象進行訪問,還可以直接通過類名去訪問,且第一個參數表示的是當前類,通常寫為cls;另外需要說明的是,類方法只能訪問公有屬性,不能訪問成員屬性,因此第一個參數傳遞的是代表當前類的cls,而不是表示實例對象的self。
靜態方法: 以@staticmethod來裝飾的成員方法就叫做靜態方法,靜態方法通常都是通過類名去訪問,且嚴格意義上來講,靜態方法已經與這個類沒有任何關聯了,因為靜態方法不要求必須傳遞實例對象或類參數,這種情況下它不能訪問類中的任何屬性和方法。
屬性方法: 這個比較有意思,是指可以像訪問成員屬性那樣去訪問這個方法;它的第一個參數也必須是當前實例對象,且該方法必須要有返回值。
我們先來定義這樣一個類:
import uuid
class Person(object):
nationality = 'China'
def __init__(self, name, age):
self.name = name
self.age = age
self.__id = str(uuid.uuid1())
# 成員方法/實例方法
def sayHello(self):
print('Hello, i am %s from %s, i am %d years old.' % (self.name, self.nationality, self.age))
# 私有方法
def __func0(self):
print('private method: func0')
print(self.name, self.age, self.__id, self.nationality)
# 類方法
@classmethod
def func1(cls):
print(cls.nationality)
# 靜態方法
@staticmethod
def func2(a, b):
print(a + b)
# 屬性方法
@property
def func3(self):
return '%s: %d' % (self.name, self.age)
執行代碼:
p = Person('Tom', 18)
p.sayHello()
Person.sayHello(p)
Person.func1()
p.func1()
Person.func2(3, 4)
p.func2(3, 4)
print(p.func3)
輸出結果:
Hello, i am Tom from China, i am 18 years old.
Hello, i am Tom from China, i am 18 years old.
China
China
7
7
Tom: 18
總結:
- 成員方法也可以通過類名去訪問,但是有點多此一舉的感覺;
- 類方法和靜態方法也可以通過實例對象去訪問,但是通常情況下都是通過類名直接訪問的;
- 最重要的一條總結:類的各種方法,能訪問哪些屬性實際上是跟方法的參數有關的:
- 比如成員方法要求第一個參數必須是一個該類的實例對象,那么實例對象能訪問的屬性,成員方法都能訪問,而且還能訪問私有屬性;
- 再比如,類方法要求第一個參數必須是當前類,因此它只能訪問到類屬性/公有屬性,而訪問不到成員屬性 和 私有屬性;
- 再比如,靜態方法對參數沒有要求,也就意味著我們可以任意給靜態方法定義參數;假如我們給靜態方法定義了表示當前類的參數,那么就可以訪問類屬性/公有屬性;假如我們給靜態方法定義了表示當前類的實例對象的參數,那么就可以訪問成員屬性;假如我們沒有給靜態方法定義這兩個參數,那么就不能訪問該類或實例對象的任何屬性。
六、類的特殊成員屬性及特殊成員方法
我們上面提到過:名稱以雙下劃線__開頭的屬性是私有屬性,名稱以雙下劃線__開頭的方法是私有方法。這里我們要來說明的是,Python的類中有一些內置的、特殊的屬性和方法,它們的名稱是以雙下劃線__開頭,同時又以雙下劃線__結尾。這些屬性和方法不再是私有屬性和私有方法,它們是可以在類的外部通過實例對象去直接訪問的,且它們都有著各自特殊的意義,我們可以通過這些特殊屬性和特殊方法來獲取一些重要的信息,或執行一些有用的操作。
1. 類的特殊成員屬性
屬性名稱 | 說明 |
---|---|
__doc__ | 類的描述信息 |
__module__ | 表示當前操作的對象對應的類的定義所在的模塊名 |
__class__ | 表示當前操作的對象對應的類名 |
__dict__ | 一個字典,保存類的所有的成員(包括屬性和方法)或實例對象中的所有成員屬性 |
現在來看一個實例:
在dog.py模塊定義一個Dog類
class Dog(object):
"""這是一個Dog類"""
# print('Hello, This is a dog.')
color = '白色'
def __init__(self, name):
self.name = name
self.__id = 1234
def func1(self):
pass
def __func1(self):
pass
@classmethod
def func2(cls):
pass
@staticmethod
def func3():
pass
在test.py模塊執行下面的代碼
from dog import Dog
dog1 = Dog('泰迪')
print(dog1.__doc__)
print(dog1.__module__)
print(dog1.__class__)
print(dog1.__dict__)
print(Dog.__dict__)
輸出結果
這是一個Dog類
dog
<class 'dog.Dog'>
{'name': '泰迪', '_Dog__id': 1234}
{'__dict__': <attribute '__dict__' of 'Dog' objects>, '__module__': 'dog', 'func2': <classmethod object at 0x000001DF0C658F98>, 'color': '白色', 'func3': <staticmethod object at 0x000001DF0C658FD0>, '_Dog__func1': <function Dog.__func1 at 0x000001DF0C63E400>, '__weakref__': <attribute '__weakref__' of 'Dog' objects>, '__doc__': '這是一個Dog類', '__init__': <function Dog.__init__ at 0x000001DF0C63E2F0>, 'func1': <function Dog.func1 at 0x000001DF0C63E378>}
總結:
- 實例對象.__dict__ 和 類.__dict__ 的值是不同的:實例對象.__dict__的值中只包含成員屬性和私有屬性,類.__dict__的值中包含公有屬性/類屬性和所有類型的方法;
- __module__和__class__的值可用于反射來實例化一個類的對象,下面會介紹。
2. 類的特殊成員方法
方法名稱 | 說明 |
---|---|
__init__ | 類構造方法,通過類創建對象時會自動觸發執行該方法 |
__del__ | 析構方法,當對象在內存中被什邡市,會自動觸發執行該方法。比如實例對象的作用域退出時,或者執行 del 實例對象 操作時。 |
__str__ | 如果一個類中定義了__str__方法,那么在打印對象時默認輸出該方法的返回值,否則會打印出該實例對象的內存地址。 |
__xxxitem__ | 是指__getitem__、__setitem__、__delitem這3個方法,它們用于索引操作,比如對字典的操作,分別表示 獲取、設置、刪除某個條目。 數據。可以通過這些方法來定義一個類對字典進行封裝,從而可以對字典中key的操作進行控制,尤其是刪除操作。 |
__new__ | 該方法會在__init__方法之前被執行,該方法會創建被返回一個新的實例對象,然后傳遞給__init__。另外需要說明的是,這不是一個成員方法,而是一個靜態方法。 |
__call__ | 源碼中的注釋是"Call self as a function." 意思是把自己(實例對象)作為一個函數去調用,而函數的調用方式是函數名() 。也就是說,當我們執行實例對象() 或者 類名()() 這樣的操作時會觸發執行該方法。 |
示例1
先來定義這樣一個類:
class Person(object):
def __call__(self, *args, **kwargs):
print(self.name, '__call__')
def __init__(self, name, age):
self.name = name
self.age = age
print(self.name, '__init__')
def __del__(self):
print(self.name, '__del__')
def __str__(self):
print(self.name, '__str__')
return '%s: %d'% (self.name, self.age)
執行下面的代碼:
print('--------實例化對象-----------')
p = Person('Tom', 18)
print('--------打印實例對象-----------')
print(p)
print('--------把實例對象作為方法進行調用-----------')
p() # 等價于 Person('Tom', 18)()
print('--------程序運行結束-----------')
輸出結果:
--------實例化對象-----------
Tom __init__
--------打印實例對象-----------
Tom __str__
Tom: 18
--------把實例對象作為方法進行調用-----------
Tom __call__
--------程序運行結束-----------
Tom __del__
可以看到,所有代碼都執行完后,進程退出時實例對象的__del__方法才被調用,這是因為對象要被銷毀了。
示例2
定義一個類似字典的類
class MyDict(object):
def __init__(self, init=None):
self.__dict = init if init is not None else {}
def __setitem__(self, key, value):
print('__setitem__', key)
self.__dict[key] = value
def __getitem__(self, item):
print('__getitem__', item)
return self.__dict.get(item, None)
def __delitem__(self, key):
print('__delitem__', key)
if key is not None and key.startswith('wh'):
print('You can not delete this item ')
return None
return self.__dict.pop(key, None)
執行下面的代碼
# 類實例化與get item
my_dict = MyDict(init={'what': '打豆豆', 'who': '企鵝團', 'time': '吃飽睡好之后'})
print(my_dict['who'], my_dict['time'], my_dict['what'])
# set item
my_dict['num'] = '10次'
print(my_dict['who'], my_dict['time'], my_dict['what'], my_dict['num'])
# del item
del my_dict['num']
print(my_dict['num'])
del my_dict['what']
print(my_dict['what'])
輸出結果
__getitem__ who
__getitem__ time
__getitem__ what
企鵝團 吃飽睡好之后 打豆豆
__setitem__ num
__getitem__ who
__getitem__ time
__getitem__ what
__getitem__ num
企鵝團 吃飽睡好之后 打豆豆 10次
__delitem__ num
__getitem__ num
None
__delitem__ what
You can not delete this item
__getitem__ what
打豆豆
可見,如果一個類實現了__setitem__,__getitem、__delitem 這幾個方法,就可以執行一些類似字典一樣的操作,比如上面用到的:
my_dict['KEY']
會自動調用my_dict實例對象的__getitem__方法;my_dict['KEY'] = VALUE
會自動調用my_dict實例對象的__setitem__方法;del my_dict['KEY']
會自動調用my_dict實例獨享的__delitem__方法;
而我們定義這樣一個類的目的在于,我們可以更好對字典操作進行控制,比如上面的例子中我們不允許刪除key以'wh'開頭的條目。
七、繼承層級關系中子類的實例對象對屬性的查找順序問題
有的同學說:“這個還不簡單嗎?子類肯定是先找自己有沒有這個屬性或方法,有的話直接調用自己的,沒有再去父類里面找。” 沒毛病,確實是這樣的,但是我們真的理解這句話了嗎?另外,如果是多繼承且有多個類的層級關系,查找順序又是怎樣的呢?再來簡單描述下這里要討論的問題是什么,簡單來說,就是要大家搞明白2個問題:
- 1)子類的實例對象調用的某個屬性或方法到底是父類的還是自己的;
- 2)如果是多繼承(同時繼承多個父類),調用的的到底是哪個父類的屬性或方法,查找順序是怎樣的。
如果大家把這2問題搞明白了,這里目的就達到了。下面我們分別以單繼承和多繼承兩個實例來講解我們這里要說明的問題。
1. 單繼承的情況
A、B、C三個類的定義如下:
class A(object):
def __init__(self, name):
self.name = name
def func1(self):
print('class A: func1')
def func2(self):
print('class A: func2')
class B(A):
def __init__(self, name, age):
super(B, self).__init__(name)
self.age = age
def func2(self):
print('class B: func2')
class C(A):
def func1(self):
print('class C: func1')
def func3(self):
print('class C: func3')
現在要執行這段代碼:
objB = B('Tom', 18)
objC = C('Jerry')
print(objB.name, objB.age)
print(objC.name)
objB.func1()
objC.func1()
請先思考下面代碼的輸出結果,再去看下面的答案。
輸出結果:
Tom 18
Jerry
class A: func1
class C: func1
這是最簡單的情況,也是最容易用開頭那段話來解釋清楚的情況,因此不做過多贅述。但這只是個引子,真正要討論的是下面這種情況:
A與B兩個類的定義如下:
class A(object):
def __init__(self, name):
self.name = name
def func1(self):
print('class A: func1')
self.func2()
def func2(self):
print('class A: func2')
class B(A):
def __init__(self, name, age):
super(B, self).__init__(name)
self.age = age
def func2(self):
print('class B: func2')
現在要執行下面的代碼:
objB = B('Tom', 18)
objB.func1()
你心里有答案了嗎?func1必然是執行的父類 A中的func1,因為子類B中沒有這個方法。那么父類func1中調用的func2方法,到底是父類的還是子類的呢?解決了這個疑問,答案自然就出來了。
分析1:
class B 是class A的子類,因此它會繼承class A的的方法func1和func2。但是,class B已經重寫了func2,可以理解為class A中的func2方法已經被覆蓋了,class B現在只能看到自己重寫后的那個func2方法,所以func1中調用的應該是class B 重寫后的func2方法。
分析2:
有的同學可能不太能理解,class A中的方法怎么能調用class B中的方法呢?下面我們來看下class B與class
A的包含關系圖:
因為子類 class B繼承了 class A的內容,因此綠框中的內容(class A)是屬于藍框(class B)中的一部分,他們應該看做一個整體。綠框中的func1是可以調用綠框外的func2的,因為他們都是objB中的成員方法。其實理解這些之后,現在我們來套用開始那句話:
- func1方法的查找與調用: objB調用func1方法,發現class B本身(藍框內的直接內容)并沒有該方法,所以去父類class A(藍框內的綠框)中去找,發現找到了,于是進行調用;
- func2方法的查找與調用: func1中調用了func2方法,這個時候還是先找子類class B本身(藍框內的直接內容),發現找到了,于是直接調用子類自己的func2方法。
因此上面這段代碼的執行結果是:
class A: func1
class B: func2
2. 多繼承的情況
新式類 與 經典類
Python 2.2引入了新式類,與它對應的是經典類,這里我們僅僅是解釋下他們的概念,為講解下面的內容做鋪墊,不會深入討論的它們的之間的區別。這里我們主要說明一下幾個點就可以了:
- Python 2.x中,默認是經典類,只有顯示繼承了object的才是新式類;
- Python 3.x中,默認就是新式類,經典類已經被廢棄;
- 新式類的子類也是新式類
深度優先 與 廣度優先
深度優先 可以理解為 縱向優先,廣度優先 可以理解為 水平方法優先。我們知道,類與類之間是有層級關系的,父類與子類是縱向的層級關系,同一個父類的多個直接子類是水平方向的同級關系。
上圖中 A是父類、B和C是繼承A的子類,D是同時繼承B和C的子類。此時D的一個實例對象去查找一個父類中的屬性或方法的查找順序就有兩種可能,但是這兩種查找順序中第一個查找的父類必然都是B:
- B-->A-->C:這就是深度優先,因為優先查找的是與B上一層級的、縱向的A
- B-->C-->A:這就是廣度優先,因為優先查找的是與B同一層極的、水平方向的C
實例
定義以下幾個類:
class A(object):
def func1(self):
print('class A: func1')
def func2(self):
print('class A: func2')
class B(A):
def func3(self):
print('class B: func3')
class C(A):
def func1(self):
print('class C: func1')
class D(B, C):
pass
執行如下代碼:
objD = D()
objD.func1()
Python 2.7 和 Python 3.5的輸出結果都是一樣的:
class C: func1
我們更改下A的定義,不顯示的指定其繼承object:
class A():
def func1(self):
print('class A: func1')
def func2(self):
print('class A: func2')
class B(A):
def func3(self):
print('class B: func3')
class C(A):
def func1(self):
print('class C: func1')
class D(B, C):
pass
再來執行同樣的代碼:
objD = D()
objD.func1()
Python 2.7的輸出結果:
class A: func1
Python 3.5的輸出結果:
class C: func1
結論
前面我們已經說過了 在Python 3.x中無論是否顯示指定繼承object,所有的類都是新式類,那么我們根據上面的兩個實例的輸出結果可以得出這樣的結論:在多繼承的情況下,經典類查找父類屬性或方法的順序是深度優先,新式類查找父類屬性的順序是廣度優先。
由于篇幅問題,還有幾個與類相關的話題會單獨再寫一篇,比如類的實例化過程、反射以及異常類的介紹和使用等。
問題交流群:666948590
文章列表