1. 程式人生 > >Python面向物件程式設計指南

Python面向物件程式設計指南

  抽象是隱藏多餘細節的藝術。在面向物件的概念中,抽象的直接表現形式通常為類。雖然Python是解釋性語言,但是它是面向物件的,從設計之初就已經是一門面向物件的語言。Python基本上提供了面向物件程式語言的所有元素,如果你已經至少掌握了一門面向物件語言,那麼利用Python進行面向物件程式設計將會相當容易。下面就來了解一下如何在Python中進行物件程式設計。

一. 如何定義一個類

  在進行python面向物件程式設計之前,先來了解幾個術語:類,類物件,例項物件,屬性,函式和方法。

  類是對現實世界中一些事物的封裝,定義一個類可以採用下面的方式來定義:

class className:
    block
  注意類名後面有個冒號,在block塊裡面就可以定義屬性和方法了。當一個類定義完之後,就產生了一個類物件。類物件支援兩種操作:引用和例項化。引用操作是通過類物件去呼叫類中的屬性或者方法,而例項化是產生出一個類物件的例項,稱作例項物件。比如定義了一個people類:
class people:
    name = 'jack'       #定義了一個屬性
    #定義了一個方法
    def printName(self):
        print self.name

  people類定義完成之後就產生了一個全域性的類物件,可以通過類物件來訪問類中的屬性和方法了。當通過people.name(至於為什麼可以直接這樣訪問屬性後面再解釋,這裡只要理解類物件這個概念就行了)來訪問時,people.name中的people稱為類物件,這點和C++中的有所不同。當然還可以進行例項化操作,p=people( ),這樣就產生了一個people的例項物件,此時也可以通過例項物件p來訪問屬性或者方法了(p.name).

  理解了類、類物件和例項物件的區別之後,我們來了解一下Python中屬性、方法和函式的區別。

  在上面程式碼中註釋的很清楚了,name是一個屬性,printName( )是一個方法,與某個物件進行繫結的函式稱作為方法。一般在類裡面定義的函式與類物件或者例項物件綁定了,所以稱作為方法;而在類外定義的函式一般沒有同對象進行繫結,就稱為函式。

二. 屬性

  在類中我們可以定義一些屬性,比如:

class people:
    name = 'jack'
    age = 12

p = people()
print p.name,p.age
  定義了一個people類,裡面定義了name和age屬性,預設值分別為'jack'和12。在定義了類之後,就可以用來產生例項化物件了,這句p = people( )例項化了一個物件p,然後就可以通過p來讀取屬性了。這裡的name和age都是公有的,可以直接在類外通過物件名訪問,如果想定義成私有的,則需在前面加2個下劃線 ' __'。
class people:
    __name = 'jack'
    __age = 12

p = people()
print p.__name,p.__age

  這段程式執行會報錯:
Traceback (most recent call last):
  File "C:/PycharmProjects/FirstProject/oop.py", line 6, in <module>
    print p.__name,p.__age
AttributeError: people instance has no attribute '__name

  提示找不到該屬性,因為私有屬性是不能夠在類外通過物件名來進行訪問的。在Python中沒有像C++中public和private這些關鍵字來區別公有屬性和私有屬性,它是以屬性命名方式來區分,如果在屬性名前面加了2個下劃線'__',則表明該屬性是私有屬性,否則為公有屬性(方法也是一樣,方法名前面加了2個下劃線的話表示該方法是私有的,否則為公有的)。

三. 方法

  在類中可以根據需要定義一些方法,定義方法採用def關鍵字,在類中定義的方法至少會有一個引數,,一般以名為'self'的變數作為該引數(用其他名稱也可以),而且需要作為第一個引數。下面看個例子:

class people:
    __name = 'jack'
    __age = 12

    def getName(self):
        return self.__name
    def getAge(self):
        return self.__age

p = people()
print p.getName(),p.getAge()

  如果對self不好理解的話,可以把它當做C++中類裡面的this指標一樣理解,就是物件自身的意思,在用某個物件呼叫該方法時,就將該物件作為第一個引數傳遞給self。

四. 類中內建的方法

  在Python中有一些內建的方法,這些方法命名都有比較特殊的地方(其方法名以2個下劃線開始然後以2個下劃線結束)。類中最常用的就是構造方法和析構方法。

  構造方法__init__(self,....):在生成物件時呼叫,可以用來進行一些初始化操作,不需要顯示去呼叫,系統會預設去執行。構造方法支援過載,如果使用者自己沒有重新定義構造方法,系統就自動執行預設的構造方法。

  析構方法__del__(self):在釋放物件時呼叫,支援過載,可以在裡面進行一些釋放資源的操作,不需要顯示呼叫。

  還有其他的一些內建方法,比如 __cmp__( ), __len( )__等。下面是常用的內建方法:

 內建方法  說明
 __init__(self,...)  初始化物件,在建立新物件時呼叫
 __del__(self)  釋放物件,在物件被刪除之前呼叫
 __new__(cls,*args,**kwd)  例項的生成操作
 __str__(self)  在使用print語句時被呼叫
 __getitem__(self,key)  獲取序列的索引key對應的值,等價於seq[key]
 __len__(self)  在呼叫行內函數len()時被呼叫
 __cmp__(stc,dst)  比較兩個物件src和dst
 __getattr__(s,name)  獲取屬性的值
 __setattr__(s,name,value)  設定屬性的值
 __delattr__(s,name)  刪除name屬性
 __getattribute__()  __getattribute__()功能與__getattr__()類似
 __gt__(self,other)  判斷self物件是否大於other物件
 __lt__(slef,other)  判斷self物件是否小於other物件
 __ge__(slef,other)  判斷self物件是否大於或者等於other物件
 __le__(slef,other)  判斷self物件是否小於或者等於other物件
 __eq__(slef,other)  判斷self物件是否等於other物件
 __call__(self,*args)  把例項物件作為函式呼叫
  __init__():__init__方法在類的一個物件被建立時,馬上執行。這個方法可以用來對你的物件做一些你希望的初始化。注意,這個名稱的開始和結尾都是雙下劃線。程式碼例子:
# Filename: class_init.py
class Person:
    def __init__(self, name):
        self.name = name
    def sayHi(self):
        print 'Hello, my name is', self.name

p = Person('Swaroop')
p.sayHi()

輸出:
Hello, my name is Swaroop
  __new__():__new__()在__init__()之前被呼叫,用於生成例項物件。利用這個方法和類屬性的特性可以實現設計模式中的單例模式。單例模式是指建立唯一物件嗎,單例模式設計的類只能例項化一個物件。
# -*- coding: UTF-8 -*-

class Singleton(object):
    __instance = None                       # 定義例項

    def __init__(self):
        pass

    def __new__(cls, *args, **kwd):         # 在__init__之前呼叫
        if Singleton.__instance is None:    # 生成唯一例項
            Singleton.__instance = object.__new__(cls, *args, **kwd)
        return Singleton.__instance
  __getattr__()、__setattr__()和__getattribute__():當讀取物件的某個屬性時,python會自動呼叫__getattr__()方法。例如,fruit.color將轉換為fruit.__getattr__(color)。當使用賦值語句對屬性進行設定時,python會自動呼叫__setattr__()方法。__getattribute__()的功能與__getattr__()類似,用於獲取屬性的值。但是__getattribute__()能提供更好的控制,程式碼更健壯。注意,python中並不存在__setattribute__()方法。程式碼例子:
# -*- coding: UTF-8 -*-

class Fruit(object):
    def __init__(self, color="red", price=0):
        self.__color = color
        self.__price = price

    def __getattribute__(self, item):              # <span style="font-family:宋體;font-size:12px;">獲取屬性的方法</span>
        return object.__getattribute__(self, item)

    def __setattr__(self, key, value):
        self.__dict__[key] = value

if __name__ == "__main__":
    fruit = Fruit("blue", 10)
    print fruit.__dict__.get("_Fruit__color")    # <span style="font-family:宋體;font-size:12px;">獲取color屬性</span>
    fruit.__dict__["_Fruit__price"] = 5
    print fruit.__dict__.get("_Fruit__price")    # <span style="font-family:宋體;font-size:12px;">獲取price屬性</span>

  Python不允許例項化的類訪問私有資料,但你可以使用object._className__attrName訪問這些私有屬性。

  __getitem__():如果類把某個屬性定義為序列,可以使用__getitem__()輸出序列屬性中的某個元素.假設水果店中銷售多鍾水果,可以通過__getitem__()方法獲取水果店中的沒種水果。程式碼例子:

# -*- coding: UTF-8 -*-

class FruitShop:
     def __getitem__(self, i):      # 獲取水果店的水果
         return self.fruits[i]      

if __name__ == "__main__":
    shop = FruitShop()
    shop.fruits = ["apple", "banana"]
    print shop[1]
    for item in shop:               # 輸出水果店的水果
        print item,
  輸出:
banana
apple banana
  __str__():__str__()用於表示物件代表的含義,返回一個字串.實現了__str__()方法後,可以直接使用print語句輸出物件,也可以通過函式str()觸發__str__()的執行。這樣就把物件和字串關聯起來,便於某些程式的實現,可以用這個字串來表示某個類。程式碼例子:
# -*- coding: UTF-8 -*-

class Fruit:     
    '''Fruit類'''               #為Fruit類定義了文件字串
    def __str__(self):          # 定義物件的字串表示
        return self.__doc__

if __name__ == "__main__":
    fruit = Fruit()
    print str(fruit)            # 呼叫內建函式str()觸發__str__()方法,輸出結果為:Fruit類
    print fruit                 #直接輸出物件fruit,返回__str__()方法的值,輸出結果為:Fruit類
  __call__():在類中實現__call__()方法,可以在物件建立時直接返回__call__()的內容。使用該方法可以模擬靜態方法。程式碼例子:
# -*- coding: UTF-8 -*-

class Fruit:
    class Growth:        # 內部類
        def __call__(self):
            print "grow ..."

    grow = Growth()      # 呼叫Growth(),此時將類Growth作為函式返回,即為外部類Fruit定義方法grow(),grow()將執行__call__()內的程式碼
if __name__ == '__main__':
    fruit = Fruit()
    fruit.grow()         # 輸出結果:grow ...
    Fruit.grow()         # 輸出結果:grow ...

五. 類屬性、例項屬性、類方法、例項方法以及靜態方法

  在瞭解了類基本的東西之後,下面看一下python中這幾個概念的區別。

  先來談一下類屬性和例項屬性

  在前面的例子中我們接觸到的就是類屬性,顧名思義,類屬性就是類物件所擁有的屬性,它被所有類物件的例項物件所共有,在記憶體中只存在一個副本,這個和C++中類的靜態成員變數有點類似。對於公有的類屬性,在類外可以通過類物件和例項物件訪問。

class people:
    name = 'jack'  #公有的類屬性
    __age = 12     #私有的類屬性

p = people()

print p.name             #正確
print people.name        #正確
print p.__age            #錯誤,不能在類外通過例項物件訪問私有的類屬性
print people.__age       #錯誤,不能在類外通過類物件訪問私有的類屬性
  例項屬性是不需要在類中顯示定義的,比如:
class people:
    name = 'jack'

p = people()
p.age =12
print p.name    #正確
print p.age     #正確

print people.name    #正確
print people.age     #錯誤
  在類外對類物件people進行例項化之後,產生了一個例項物件p,然後p.age = 12這句給p添加了一個例項屬性age,賦值為12。這個例項屬性是例項物件p所特有的,注意,類物件people並不擁有它(所以不能通過類物件來訪問這個age屬性)。當然還可以在例項化物件的時候給age賦值。
class people:
    name = 'jack'
    
    #__init__()是內建的構造方法,在例項化物件時自動呼叫
    def __init__(self,age):
        self.age = age

p = people(12)
print p.name    #正確
print p.age     #正確

print people.name    #正確
print people.age     #錯誤
  如果需要在類外修改類屬性,必須通過類物件去引用然後進行修改。如果通過例項物件去引用,會產生一個同名的例項屬性,這種方式修改的是例項屬性,不會影響到類屬性,並且之後如果通過例項物件去引用該名稱的屬性,例項屬性會強制遮蔽掉類屬性,即引用的是例項屬性,除非刪除了該例項屬性。
class people:
    country = 'china'
    

print people.country
p = people()
print p.country
p.country = 'japan' 
print p.country      #例項屬性會遮蔽掉同名的類屬性
print people.country
del p.country    #刪除例項屬性
print p.country

  下面來看一下類方法、例項方法和靜態方法的區別。

  類方法:是類物件所擁有的方法,需要用修飾器"@classmethod"來標識其為類方法,對於類方法,第一個引數必須是類物件,一般以"cls"作為第一個引數(當然可以用其他名稱的變數作為其第一個引數,但是大部分人都習慣以'cls'作為第一個引數的名字,就最好用'cls'了),能夠通過例項物件和類物件去訪問。

class people:
    country = 'china'
    
    #類方法,用classmethod來進行修飾
    @classmethod
    def getCountry(cls):
        return cls.country

p = people()
print p.getCountry()    #可以用過例項物件引用
print people.getCountry()    #可以通過類物件引用

  類方法還有一個用途就是可以對類屬性進行修改:

class people:
    country = 'china'
    
    #類方法,用classmethod來進行修飾
    @classmethod
    def getCountry(cls):
        return cls.country
        
    @classmethod
    def setCountry(cls,country):
        cls.country = country
        

p = people()
print p.getCountry()    #可以用過例項物件引用
print people.getCountry()    #可以通過類物件引用

p.setCountry('japan')   

print p.getCountry()   
print people.getCountry()
  執行結果:
china
china
japan
japan

  結果顯示在用類方法對類屬性修改之後,通過類物件和例項物件訪問都發生了改變。

  例項方法:在類中最常定義的成員方法,它至少有一個引數並且必須以例項物件作為其第一個引數,一般以名為'self'的變數作為第一個引數(當然可以以其他名稱的變數作為第一個引數)。在類外例項方法只能通過例項物件去呼叫,不能通過其他方式去呼叫。

class people:
    country = 'china'
    
    #例項方法
    def getCountry(self):
        return self.country
        

p = people()
print p.getCountry()         #正確,可以用過例項物件引用
print people.getCountry()    #錯誤,不能通過類物件引用例項方法
  靜態方法:需要通過修飾器"@staticmethod"來進行修飾,靜態方法不需要多定義引數。
class people:
    country = 'china'
    
    @staticmethod
    #靜態方法
    def getCountry():
        return people.country
        

print people.getCountry()

  對於類屬性和例項屬性,如果在類方法中引用某個屬性,該屬性必定是類屬性,而如果在例項方法中引用某個屬性(不作更改),並且存在同名的類屬性,此時若例項物件有該名稱的例項屬性,則例項屬性會遮蔽類屬性,即引用的是例項屬性,若例項物件沒有該名稱的例項屬性,則引用的是類屬性;如果在例項方法更改某個屬性,並且存在同名的類屬性,此時若例項物件有該名稱的例項屬性,則修改的是例項屬性,若例項物件沒有該名稱的例項屬性,則會建立一個同名稱的例項屬性。想要修改類屬性,如果在類外,可以通過類物件修改,如果在類裡面,只有在類方法中進行修改。

  從類方法和例項方法以及靜態方法的定義形式就可以看出來,類方法的第一個引數是類物件cls,那麼通過cls引用的必定是類物件的屬性和方法;而例項方法的第一個引數是例項物件self,那麼通過self引用的可能是類屬性、也有可能是例項屬性(這個需要具體分析),不過在存在相同名稱的類屬性和例項屬性的情況下,例項屬性優先順序更高。靜態方法中不需要額外定義引數,因此在靜態方法中引用類屬性的話,必須通過類物件來引用。

六. 繼承和多重繼承

  上面談到了類的基本定義和使用方法,這隻體現了面向物件程式設計的三大特點之一:封裝。下面就來了解一下另外兩大特徵:繼承和多型。

  在Python中,如果需要的話,可以讓一個類去繼承一個類,被繼承的類稱為父類或者超類、也可以稱作基類,繼承的類稱為子類。並且Python支援多繼承,能夠讓一個子類有多個父類。

  Python中類的繼承定義基本形式如下:

#父類
class superClassName:
    block

#子類
class subClassName(superClassName):
    block
  在定義一個類的時候,可以在類名後面緊跟一對括號,在括號中指定所繼承的父類,如果有多個父類,多個父類名之間用逗號隔開。以大學裡的學生和老師舉例,可以定義一個父類UniversityMember,然後類Student和類Teacher分別繼承類UniversityMember:
# -*- coding: UTF-8 -*-

class UniversityMember:

    def __init__(self,name,age):
        self.name = name
        self.age = age

    def getName(self):
        return self.name

    def getAge(self):
        return self.age

class Student(UniversityMember):

    def __init__(self,name,age,sno,mark):
        UniversityMember.__init__(self,name,age)     #注意要顯示呼叫父類構造方法,並傳遞引數self
        self.sno = sno
        self.mark = mark

    def getSno(self):
        return self.sno

    def getMark(self):
        return self.mark



class Teacher(UniversityMember):

    def __init__(self,name,age,tno,salary):
        UniversityMember.__init__(self,name,age)
        self.tno = tno
        self.salary = salary

    def getTno(self):
        return self.tno

    def getSalary(self):
        return self.salary

  在大學中的每個成員都有姓名和年齡,而學生有學號和分數這2個屬性,老師有教工號和工資這2個屬性,從上面的程式碼中可以看到:

  1)在Python中,如果父類和子類都重新定義了構造方法__init( )__,在進行子類例項化的時候,子類的構造方法不會自動呼叫父類的構造方法,必須在子類中顯示呼叫。

  2)如果需要在子類中呼叫父類的方法,需要以”父類名.方法“這種方式呼叫,以這種方式呼叫的時候,注意要傳遞self引數過去。

  對於繼承關係,子類繼承了父類所有的公有屬性和方法,可以在子類中通過父類名來呼叫,而對於私有的屬性和方法,子類是不進行繼承的,因此在子類中是無法通過父類名來訪問的。

  Python支援多重繼承。對於多重繼承,比如

  class SubClass(SuperClass1,SuperClass2)

  此時有一個問題就是如果SubClass沒有重新定義構造方法,它會自動呼叫哪個父類的構造方法?這裡記住一點:以第一個父類為中心。如果SubClass重新定義了構造方法,需要顯示去呼叫父類的構造方法,此時呼叫哪個父類的構造方法由你自己決定;若SubClass沒有重新定義構造方法,則只會執行第一個父類的構造方法。並且若SuperClass1和SuperClass2中有同名的方法,通過子類的例項化物件去呼叫該方法時呼叫的是第一個父類中的方法。

七. 多型

  多型即多種形態,在執行時確定其狀態,在編譯階段無法確定其型別,這就是多型。Python中的多型和Java以及C++中的多型有點不同,Python中的變數是弱型別的,在定義時不用指明其型別,它會根據需要在執行時確定變數的型別(個人覺得這也是多型的一種體現),並且Python本身是一種解釋性語言,不進行預編譯,因此它就只在執行時確定其狀態,故也有人說Python是一種多型語言。在Python中很多地方都可以體現多型的特性,比如內建函式len(object),len函式不僅可以計算字串的長度,還可以計算列表、元組等物件中的資料個數,這裡在執行時通過引數型別確定其具體的計算過程,正是多型的一種體現。這有點類似於函式過載(一個編譯單元中有多個同名函式,但引數不同),相當於為每種型別都定義了一個len函式。這是典型的多型表現。有些朋友提出Python不支援多型,我是完全不贊同的。

  本質上,多型意味著可以對不同的物件使用同樣的操作,但它們可能會以多種形態呈現出結果。len(object)函式就體現了這一點。在C++、Java、C#這種編譯型語言中,由於有編譯過程,因此就鮮明地分成了執行時多型和編譯時多型。執行時多型是指允許父類指標或名稱來引用子類物件,或物件方法,而實際呼叫的方法為物件的類型別方法,這就是所謂的動態繫結。編譯時多型有模板或範型、方法過載(overload)、方法重寫(override)等。而Python是動態語言,動態地確定型別資訊恰恰體現了多型的特徵。在Python中,任何不知道物件到底是什麼型別,但又需要物件做點什麼的時候,都會用到多型。

  能夠直接說明多型的兩段示例程式碼如下:
  1、方法多型
# -*- coding: UTF-8 -*-

_metaclass_=type # 確定使用新式類
class calculator:
  
    def count(self,args):
        return 1

calc=calculator() #自定義型別

from random import choice
obj=choice(['hello,world',[1,2,3],calc]) #obj是隨機返回的 型別不確定
print type(obj)
print obj.count('a') #方法多型

  對於一個臨時物件obj,它通過Python的隨機函式取出來,不知道具體型別(是字串、元組還是自定義型別),都可以呼叫count方法進行計算,至於count由誰(哪種型別)去做怎麼去實現我們並不關心。

_metaclass_=type # 確定使用新式類
class Duck:
    def quack(self): 
        print "Quaaaaaack!"
    def feathers(self): 
        print "The duck has white and gray feathers."
 
class Person:
    def quack(self):
        print "The person imitates a duck."
    def feathers(self): 
        print "The person takes a feather from the ground and shows it."
 
def in_the_forest(duck):
    duck.quack()
    duck.feathers()
 
def game():
    donald = Duck()
    john = Person()
    in_the_forest(donald)
    in_the_forest(john)
 
game()
  就in_the_forest函式而言,引數物件是一個鴨子型別,它實現了方法多型。但是實際上我們知道,從嚴格的抽象來講,Person型別和Duck完全風馬牛不相及。
  2、運算子多型
def add(x,y):
    return x+y

print add(1,2) #輸出3

print add("hello,","world") #輸出hello,world

print add(1,"abc") #丟擲異常 TypeError: unsupported operand type(s) for +: 'int' and 'str'
  上例中,顯而易見,Python的加法運算子是”多型“的,理論上,我們實現的add方法支援任意支援加法的物件,但是我們不用關心兩個引數x和y具體是什麼型別。
  Python同樣支援運算子過載,例項如下:
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
  一兩個示例程式碼當然不能從根本上說明多型。普遍認為面向物件最有價值最被低估的特徵其實是多型。我們所理解的多型的實現和子類的虛擬函式地址繫結有關係,多型的效果其實和函式地址執行時動態繫結有關。在C++, Java, C#中實現多型的方式通常有重寫和過載兩種,從上面兩段程式碼,我們其實可以分析得出Python中實現多型也可以變相理解為重寫和過載。在Python中很多內建函式和運算子都是多型的。

參考文獻: