1. 程式人生 > >python 歷險記之面向物件——一個 Java 程式設計師的告白(二)

python 歷險記之面向物件——一個 Java 程式設計師的告白(二)

前言

上篇文章 中,我使用了與 java類比 以及 程式碼例項 的方式涉及了 python 3 中 string, 資料結構(Dict, List, 元組)等重要的主題。 今天我會繼續探險,去征服 python 3 中的面向物件, let's go 讓我們出發吧!

類和物件

剛接觸 python 中的類和物件,我也和大多數小夥伴一樣迷茫,不知道它和我所熟知的 java 都有什麼異同點,為此我還提出了一大堆問題

  1. 如何建立和例項化類?
  2. 是否和 java 一樣有訪問修飾符,分為幾個級別?
  3. 建構函式該怎麼寫?
  4. 怎麼進行 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 簡潔不少。

瞭解了定義和例項化類,還有兩個問題:

  1. 要判斷一個物件是不是某個類的例項該怎麼做呢?用 isinstance
print(isinstance (person, Person)) # True
  1. 判斷物件是什麼型別,該怎麼做? 用 type
print(type(person)) # <class '__main__.Person'>

屬性

上面的程式碼,光有一個空物件是幹不了任何事情的,我們也要像 java 一樣為其定義屬性和方法。 java 是不能動態定義一個變數的,必須要把它放在 class

中預先定義好才可以用;而在 python 中這卻不是問題,不信看程式碼~

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。對這種情況有兩種方式可以處理:

  1. 預先使用內建函式 hasattr 判定物件是否擁有該屬性(記住,只對公有變數有效哦~)
  2. 使用 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 變數

那我要是想解決這個錯誤,該怎麼做呢?有兩種方法

  1. 在子類 Son 的初始化函式中顯式呼叫父類 Father 的初始化函式
  2. 在子類 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 也可以看做對其父類的指代,它的使用場合就是用來呼叫父類的方法:

  1. 呼叫父類的 __init__方法
  2. 實現了和父類相同的功能,還需要呼叫父類的方法

它的寫法是 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() 為例

  1. selfSon 的一個例項, superself 轉化為父類 Father 的一個例項物件
  2. 因為 self 經過了轉化, 那它得到的 __age, 也是父類初始化時得到的 __age

結語

看到這裡,不知您對 python 的面向物件有了多少理解,反正我是理解了不少,哈哈。如果有疑問和建議,歡迎留言交流,我將仔細閱讀,認真回覆。

下篇文章中會涉及到 檔案, json xml 處理 處理等主題,敬請期待~