python 歷險記之面向物件——一個 Java 程式設計師的告白(二)
前言
在上篇文章 中,我使用了與 java類比 以及 程式碼例項 的方式涉及了 python 3 中 string, 資料結構(Dict, List, 元組)等重要的主題。 今天我會繼續探險,去征服 python 3 中的面向物件, let's go 讓我們出發吧!
類和物件
剛接觸 python 中的類和物件,我也和大多數小夥伴一樣迷茫,不知道它和我所熟知的 java 都有什麼異同點,為此我還提出了一大堆問題
- 如何建立和例項化類?
- 是否和 java 一樣有訪問修飾符,分為幾個級別?
- 建構函式該怎麼寫?
- 怎麼進行
class
的繼承?
下面就一一來探索這些疑惑。
類的定義和例項化
在 java 中要建立一個類就必須要使用 class
new
關鍵字。在 python 中是怎麼樣的呢?
先看程式碼
class Person():
"""這個叫做定義體,用來解釋類的用途"""
print(Person) # <class '__main__.Person'>
# 由於是在程式頂層定義的,它的全名就是 '__main__.Person'
person = Person()
print(person) # <__main__.Person object at 0x000000000219A1D0>
要定義一個類(class) 只要將 class
關鍵字放在前面即可,類內部也可以像 java 似的定義變數和函式,這個後面再看。
例項化一個類,也就是建立一個物件,並不需要使用 new
關鍵字,只需將 class
當做函式來呼叫就可以啦,是不是比 java 簡潔不少。
瞭解了定義和例項化類,還有兩個問題:
- 要判斷一個物件是不是某個類的例項該怎麼做呢?用
isinstance
print(isinstance (person, Person)) # True
- 判斷物件是什麼型別,該怎麼做? 用
type
print(type(person)) # <class '__main__.Person'>
屬性
上面的程式碼,光有一個空物件是幹不了任何事情的,我們也要像 java 一樣為其定義屬性和方法。
java 是不能動態定義一個變數的,必須要把它放在 class
class Person():
"""這個叫做定義體,用來解釋類的用途"""
person = Person()
person.age = 5
print(person.age)
雖然在對 Person class
定義時沒有任何屬性的宣告,但在例項化後依然可以新增 age 屬性,而且也並沒有看到如 java 中 public
, private
等訪問修飾符的存在, python 中有沒有這些概念呢?還真有,變數預設就是 public
公有的,如果 在變數名前新增兩個下劃線,這樣就會認為是 private
私有變量了,直接訪問是不可以的。看下面程式碼
class Person():
"""這個叫做定義體,用來解釋類的用途"""
gender = 'male'
__age = 5
person = Person()
print(person.gender) # male
print(person.__age) # AttributeError: 'Person' object has no attribute '__age'
上面程式碼中,在列印 __age
時會報錯,告知沒有找到這個屬性,其實就是 由於使用雙下劃線做字首使其變成私有變量了。
那 函式名是不是也有私有函式,是不是也在前面加雙下劃線呢 ?猜的沒錯,這個我們後面再瞭解。
既然 python 物件的屬性操作如此靈活,可以動態新增,那使用者在使用時就可能會碰到一些異常。
比較典型的就是,訪問一個不存在的屬性,會丟擲 AttributeError
。對這種情況有兩種方式可以處理:
- 預先使用內建函式
hasattr
判定物件是否擁有該屬性(記住,只對公有變數有效哦~) - 使用
try
語句處理
class Person():
"""這個叫做定義體,用來解釋類的用途"""
gender = 'male'
__age = 5
person = Person()
print(hasattr(person, 'gender')) # True
print(hasattr(person, 'name')) # False
print(hasattr(person, '__age')) # False
try:
name = person.name
except AttributeError:
name = 'unknown'
print(name)
方法
什麼是方法?方法和函式有什麼區別?在上一篇我就介紹了好多 string
的方法,為什麼叫做方法,而不叫做 string
的函式呢?一起來了解下~
- 函式是指可以執行某種運算,可以通過名字來呼叫的一段語句的組合
- 方法是特殊的函式,是跟一個物件或類相關聯的
- 方法是書寫在類的定義之中,明確表示和類之間關係的
- 在呼叫方法時,前面需要加上類名(函式呼叫語法)或者例項化的物件名(方法呼叫語法)
靜態方法和普通方法
呼叫方法分為兩種形式,分別是
- 函式呼叫語法(靜態方法)
- 普通方法(動態方法)
先看第一種函式呼叫語法,這其實和 java 中的靜態方法是一樣的,只是前面不需要 static
關鍵字。
class Person:
def print_person(person):
print('name: %s, gender%s, age:%d' % (person.name, person.gender, person.age))
person = Person()
person.name = 'Tom'
person.gender = 'male'
person.age = 10
Person.print_person(person)
函式呼叫語法的方式其實和單純的函式呼叫,區別是不大的,因為方法前面的 class
對它沒起什麼作用,活動主體 依然是方法。
再看另外一種 方法呼叫語法,而這次的主體則是呼叫該方法的 物件
class Person:
__name = 'Tom'
__gender = 'male'
__age = 10
def print_person(self):
print('name: %s, gender:%s, age:%d' % (self.__name, self.__gender, self.__age))
person = Person()
person.print_person()
細心的同學會發現這裡在定義方法時形參為 self
, 而在呼叫方法時卻沒有任何入參。
那這個 self
是什麼呢?
如果類比 java 的話,這個 self
可以看作是 this
, 其實就是對當前物件的引用。 java 中定義方法時不必將其做入參。而這個 self
在 python 中則是必須宣告的,在呼叫的時候則不必傳入。
注意,這個 self
可不是關鍵字哦,只要佔據方法形參的頭把交椅,你可以用任何名字。
建構函式該怎麼寫?
在 java 中建構函式是與類同名的,而且會伴隨著例項化的動作而執行。在 python 中呢?
python 中的建構函式叫做 init
方法,全名是 __init__
具體看下面程式碼
class Person():
__gender = 'male'
__age = '0'
def __init__(self, gender='male', age=0):
self.__gender = gender
self.__age = age
person1 = Person('female', 10)
person2 = Person()
person2 = Person('male')
作為例項方法, self
入參當然少不了,其他引數就按照順序排開,若引數不夠,就用預設值來代替。
str 方法
在java 中, 我們一般會覆蓋 toString() 方法來返回物件中包含的值得關注的資訊。 python 中也有這樣一個方法,叫做 __str__
。
class Person:
__name = 'Tom'
__gender = 'male'
__age = 10
def __str__(self):
return ('name: %s, gender:%s, age:%d' % (self.__name, self.__gender, self.__age))
person = Person()
print(person)
作為最佳實踐的一部分,建議你在每個建立的類中都覆蓋這個方法。
多型
還記得面向物件的幾個特徵嗎?封裝性,繼承性,多型性。嗯,來聊下 python 對多型的實現。
什麼叫做多型?
在 java 中,如果在一個 class 中有多個函式,函式名相同而引數不同(個數或型別不同),就叫做多型。
而在 python 中, 多型的概念則更進一步,對於同一個函式,如果能夠處理多種型別的資料,也叫做多型。
tuple_list = [(1, 2,), (2, 3,), (4, 5)]
list = [1, 2, 3, 4]
dict1 = {
'a' : 1,
'b' : 2
}
def printSomething(something):
for i in something:
print(i)
print(tuple_list)
print(dict1)
print(list)
printSomething
一個函式可以同時列印元組,列表以及字典,充分發揮程式碼複用的功效,是不是很方便。
繼承
聊完了多型,再來看看面向物件的另一個特徵:繼承性。
什麼是繼承?繼承就是定義好了一個類 A(父類);再定義一個新類 B(子類),類 B 擁有類 A 的方法和屬性,並且又定義了新的屬性和方法。類 A 稱為父類,類 B 稱為子類。
java 中定義兩個類的繼承關係,使用 extends
關鍵字實現,在 python 中呢?
class Father:
""" 這是一個父類 """
__age = 45
class Son(Father):
""" 這是一個子類 """
python 中不需要加關鍵字來說明繼承關係,只需要將父類的名稱放在括號中就可以了,看起來要比 java
簡潔一些。
父類和子類的初始化函式呼叫
前面講過, python class 中可以定義自己的初始化函式,在例項化的時會被呼叫。那如果父類和子類都有初始化函式或者父類有而子類沒有,那初始化函式該如何執行呢?這裡分為三種情況來說明,先來看第一種。
第一種情況,
父類有 init 而子類沒有, 這時父類的初始化函式會被預設呼叫
class Father():
""" 這是一個父類 """
def __init__(self, age):
print("Father's init function invoke")
self.__age = age
class Son(Father):
""" 這是一個子類 """
son = Son(5)
這裡要注意,父類中需要的 age
引數一定要傳進去哦,要不然會報錯的。
第二種情況
父類,子類都有 init ,而子類沒有顯式呼叫父類的 init 方法時,父類初始化函式是不會被呼叫的
class Father():
""" 這是一個父類 """
def __init__(self, age):
print("Father's init function invoke")
self.__age = age
def get_age(self):
return self.__age
class Son(Father):
""" 這是一個子類 """
def __init__(self, age):
print("Son's init function invoke")
self.__age = age
son = Son(5) # Son's init function invoke
print(son.get_age()) # AttributeError: 'Son' object has no attribute '_Father__age'
細心的同學會發現,程式碼中的最後一句報錯了,表示 Son 物件沒有 Father
類的 __age
變數。這是因為
- 父類的初始化函式沒有執行,父類的
__age
變數則沒有初始化 get_age
函式是被子類從父類繼承來的,返回的是父類的__age
變數
那我要是想解決這個錯誤,該怎麼做呢?有兩種方法
- 在子類
Son
的初始化函式中顯式呼叫父類Father
的初始化函式 - 在子類
Son
中重新定義個get_age
方法,這樣就會覆蓋父類的同名方法,返回的是子類的_age
變數
第二種方法就不貼程式碼了,感興趣的話可以試試。重點來看第一種方法,這就引出了第 3 種情況。
第三種情況
子類在自己定義的 init 方法中,顯式呼叫父類的 init 方法,父類和子類的屬性都會被初始化
class Father():
""" 這是一個父類 """
def __init__(self, age):
print("Father's init function invoke")
self.__age = age
def get_age(self):
return self.__age
class Son(Father):
""" 這是一個子類 """
def __init__(self, age):
print("Son's init function invoke")
self.__age = age
super(Son, self).__init__(age + 25)
def get_age(self):
return self.__age
def get_father_age(self):
return super(Son, self).get_age()
son = Son(5)
# Son's init function invoke
# Father's init function invoke
print(son.get_father_age()) # 30
print(son.get_age()) # 5
看到程式碼中是怎麼呼叫父類的初始化函式嗎? 對,用的是 super
。
java 中也有 super
關鍵字,表示對父類的指代, python 的 super
是怎麼用的,原理是什麼?我們來看下。
super
下面說明的只針對 python 單繼承的情況,多繼承這裡暫不涉及,有興趣的同學可以自行充電。
在單繼承中,super
也可以看做對其父類的指代,它的使用場合就是用來呼叫父類的方法:
- 呼叫父類的
__init__
方法 - 實現了和父類相同的功能,還需要呼叫父類的方法
它的寫法是 super(Son,self).xxx
, 當然也可以寫成 super()
這種簡寫的形式。
來看程式碼
class Father():
""" 這是一個父類 """
def __init__(self, age):
print("Father's init function invoke")
self.__age = age
def get_age(self):
return self.__age
class Son(Father):
""" 這是一個子類 """
def __init__(self, age):
print("Son's init function invoke")
self.__age = age
super(Son, self).__init__(age + 25)
def get_age(self):
return self.__age
def get_father_age(self):
return super(Son, self).get_age()
son = Son(5)
# Son's init function invoke
# Father's init function invoke
print(son.get_father_age()) # 30
print(son.get_age()) # 5
通過程式碼來窺探下它的執行原理,以 super(Son, self).get_age()
為例
self
是Son
的一個例項,super
把self
轉化為父類Father
的一個例項物件- 因為
self
經過了轉化, 那它得到的__age
, 也是父類初始化時得到的__age
結語
看到這裡,不知您對 python 的面向物件有了多少理解,反正我是理解了不少,哈哈。如果有疑問和建議,歡迎留言交流,我將仔細閱讀,認真回覆。
下篇文章中會涉及到 檔案, json xml 處理 處理等主題,敬請期待~