2.5.7 Python的面向對象
Python從設計之初就已經是一門面向對象的語言,正因為如此,在Python中創建一個類和對象是很容易的。本章將詳細介紹Python的面向對象編程。
Python是支持面向對象、面向過程、函數式編程等多種編程范式的,它不強制我們使用任何一種編程范式,我們可以使用過程式編程編寫任何程序。對于中等和大型項目來說,面向對象將給我們帶來許多優勢。如果你以前沒有接觸過面向對象的編程語言,那可能需要先了解一些面向對象語言的基本特征,在頭腦里形成一個基本的面向對象的概念,這樣有助于你更容易地學習Python的面向對象編程。
下面來簡單了解一下面向對象的基本特征。
1.面向對象的基本定義
Python面向對象的基本定義如下。
·類(Class):用來描述具有相同屬性和方法的對象的集合。它定義了該集合中每個對象所共有的屬性和方法。對象是類的實例。
·類變量:類變量在整個實例化的對象中是公用的。類變量定義在類中且在函數體之外。類變量通常不作為實例變量使用。
·方法:類中定義的函數。
·數據成員:類變量或者實例變量用于處理類及其實例對象的相關數據。
·方法重寫:如果從父類繼承的方法不能滿足子類的需求,可以對其進行改寫,這個過程叫方法的覆蓋(override),也稱為方法的重寫。
·實例變量:定義在方法中的變量,只作用于當前實例的類。
·繼承:即一個派生類(derived class)繼承基類(base class)的字段和方法。繼承也允許把一個派生類的對象作為一個基類對象對待。
·實例化:創建一個類的實例、類的具體對象。
·對象:通過類定義的數據結構實例。對象包括兩個數據成員(類變量和實例變量)和一個方法。
類和對象是面向對象的兩個重要概念,類是客觀世界中事物的抽象,而對象則是類實例化后的實體。大家可以將類想象成圖紙或模型,對象則是通過圖紙或模型設計出來的實物。例如,同樣的汽車模型可以造出不同的汽車,不同的汽車有不同的顏色、價格和車牌,如圖2-7所示。

圖2-7 以汽車類型類比類和實例化
汽車模型是對汽車特征和行為的抽象,而汽車則是實際存在的事物,是客觀世界中實實在在的物體。
我們在描述一個真實對象(物體)時包括以下兩個方面:
·它可以做什么(行為)。
·它是什么樣的(屬性或特征)。
在Python中,一個對象的特征也稱為屬性,它所具有的行為則稱為方法,對象=屬性+方法。另外在Python中,我們會把具有相同屬性和方法的對象歸為一個類。
這里舉個簡單的例子:
#-*- encoding:utf8 -*- class Turtle(object): #屬性 color = "green" weight = "10" #方法 def run(self): print "我正在跑..." def sleep(self): print "我正在睡覺..." tur = Turtle() print tur.weight #打印實例tur的weight屬性 tur.sleep() #調用實例tur的sleep方法
執行后的結果如下:
10 我正在睡覺...
Python會自動給每個對象添加特殊變量self,它相當于C++的指針,這個變量指向對象本身,讓類中的函數能夠明確地引用對象的數據和函數(self不能被忽略),示例如下:
#-*- encoding:utf-8 -*- class NewClass(object): def __init__(self,name): print self self.name = name print "我的名字是:{}".format(self.name) cc = NewClass('yhc')
打印結果如下:
<__main__.NewClass instance at 0x020D4440> 我的名字是:yhc
format函數是Python新增的一種格式化字符串的函數,它增強了字符串格式化的功能。其基本語法是通過“{}”和“:”來代替以前的“%”,可以接受無限個參數,位置可以不按順序排列。
在這段代碼中,self是NewClass類在內存地址0x020D4440處的實例。因此,self在這里與C++中的this一樣,代表的都是當前對象的地址,可以用來調用當前類中的屬性和方法。在這段代碼中,有一個特殊的函數,即__init__()方法,它是Python中的構造函數,構造函數用于初始化類的內部狀態,為類的屬性設置默認值。
如果我們想看一下cc的屬性,可以在Python命令行模式下輸入如下命令:
dir(cc)
打印結果如下:
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'name']
內建函數dir()可以顯示類屬性,同樣還可以打印所有的實例屬性。
與類相似,實例其實也有一個__dict__的特殊屬性,它是由實例屬性構成的一個字典,同樣在Python命令行模式下輸入如下命令:
cc.__dict__
輸出結果如下:
{'name': 'yhc'}
事實上,Python中定義了很多內置類屬性,用于管理類的內部關系。
·__dict__:類的屬性(包含一個字典,由類的數據屬性組成)。
·__doc__:類的文檔字符串。
·__name__:類名。
·__module__:類定義所在的模塊(類的全名是'__main__.className',如果類位于一個導入模塊mymod中,那么className.__module__等于mymod)。
·__bases__:類的所有父類構成元素(包含了一個由所有父類組成的元組)。
我們如果執行print NewClass.__bases__這段代碼,則會輸出如下結果:
(<type 'object'>,)
另外,在上面的代碼中,如果想打印出cc的值,可用如下命令:
print cc
打印結果如下:
<__main__.NewClass instance at 0x020D4440>
在這里,cc跟上面的self的效果是一樣的,它也是NewClass類在內存地址0x020D4440處的實例,顯然這種不是我們想要的效果,所以需要一個方法來打印出適合我們人類閱讀的方式,這里采用__str__,將上面的代碼精簡并加入新的內容,整個代碼變成:
# -*- coding: UTF-8 -*- class NewClass(object): def __init__(self,name): # print self self.name = name print "我的名字是:{}".format(self.name) def __str__(self): print "NewClass:{}".format(self.name) cc = NewClass('yhc')
注意,對于這里采用的__str__方法,它的輸出結果為我們預先定義好的格式:
我的名字是:yhc
__repr__具有跟__str__類似的效果,這里就不重復演示了。事實上,我們在創建自己的類和對象時,編寫__str__和__repr__方法是有必要的。它們對于顯示對象的內容很有幫助,而顯示對象內容有助于調試程序。
注意
__str__()必須使用return語句返回,如果__str__()不返回任何值,則執行print語句會出錯。
另外,請注意這段代碼中的object,即class NewClass(object),專業的說法叫定義基類。很多資料上面都將此object略過了,這里寫段代碼對比一下帶上object和不帶object的區別:
class NewClass(): pass class NewClass1(object): pass a1 = NewClass() print dir(a1) a2 = NewClass1() print dir(a2)
執行這段代碼,發現區別還是很明顯的:
['__doc__', '__module__'] ['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
還可以用__bases__類屬性來看一下NewClass和NewClass1的區別,代碼如下:
print NewClass.__bases__ print NewClass1.__bases__
結果如下:
() (<type 'object'>,)
NewClass和NewClass1類的區別很明顯,NewClass不繼承object對象,只擁有了doc和module,也就是說這個類的命名空間只有兩個對象可以操作;而NewClass1類繼承了object對象,擁有好多可操作對象,這些都是類中的高級特性。另外,此處如果不加object,有時候還會影響代碼的執行結果,所以結合以上種種因素考慮,建議此處帶上object。
注意
Python 2.7中默認都是經典類,只有顯式繼承了object才是新式類,即類名后面括號中需要帶上object;Python 3.7(Python3.x)中默認都是新式類,不必顯式地繼承object。由于本書采用的版本是Python 2.7.10,因此建議此處都帶上object。
2.Python裝飾器
Python面向對象的開發工作中經常會涉及Python裝飾器,它究竟有什么用途呢?
裝飾器本質上是一個Python函數,它可以讓其他函數不需要做任何代碼變動即可增加額外的功能,裝飾器的返回值也是一個函數對象。它經常用于有切面需求的場景,比如:插入日志、性能測試、事務處理、緩存、權限校驗等。裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量與函數功能本身無關的雷同代碼并繼續重用。概括地講,裝飾器的作用就是為已經存在的對象添加額外的功能。
這里先來看一個簡單的例子:
def foo(): print('i am foo')
現在有一個新的需求,即記錄下函數的執行日志,于是在代碼中添加了日志代碼:
import logging def foo(): print "i am foo" logging.info "foo is running"
那么問題來了,foo1()、foo2()也有類似的需求,怎么做?再寫一個logging在foo1和foo2函數里?這樣就會造成大量雷同的代碼,為了減少重復寫代碼,我們可以這樣做,重新定義一個函數專門處理日志,日志處理完之后再執行真正的業務代碼,示例如下:
import logging def use_logging(func): logging.warn("{} is running".format(func.__name__)) func() def bar(): print "i am bar" use_logging(bar)
邏輯上不難理解,但是這樣做的話,我們每次都要將一個函數作為參數傳遞給use_logging函數,而且這種方式已經破壞了原有的代碼邏輯結構,之前執行業務邏輯時,運行bar()即可,但是現在不得不改成use_logging(bar)。那么有沒有更好的處理方式呢?當然有,答案就是使用Python裝飾器,示例代碼如下:
import logging def use_logging(func): def wrapper(*args, **kwargs): logging.warn("{} is running".format(func.__name__)) return func(*args, **kwargs) return wrapper def bar(): print "i am bar" bar = use_logging(bar) bar()
函數use_logging就是裝飾器,它把執行真正業務方法的func包裹在函數里面,看起來像bar被use_logging裝飾了。
下面來介紹一下Python程序中出現的@符號。
@符號是裝飾器的語法糖(也是Python獨有的語法糖,其他語言中沒有),在定義函數的時候使用,可避免再一次的賦值操作。也就是說,可以省去bar=use_logging(bar)這一句,直接調用bar()即可得到想要的結果。如果我們有其他的類似函數,可以繼續調用裝飾器來修飾函數,而不用重復修改函數或者增加新的封裝。這樣就提高了程序的可重復利用性,并增加了程序的可讀性。程序如下:
def use_logging(func): def wrapper(*args, **kwargs): logging.warn("{} is running".format(func.__name__)) return func(*args) return wrapper @use_logging def foo1(): print "i am foo" @use_logging def foo2(): print "i am bar" foo1() foo2()
在Python中使用裝飾器如此方便,這要歸因于Python的函數能像普通的對象一樣作為參數傳遞給其他函數,它可以被賦值給其他變量,可以作為返回值,可以被定義在另外一個函數內。@staticmethod和@classmethod這些裝飾器在面向對象的開發工作中會經常用到,希望大家都能熟練掌握其用法。
3.面向對象的特性介紹
面向對象的編程帶來的主要好處之一是代碼的復用,實現這種復用的方法之一是使用繼承機制。繼承完全可以理解成類之間類型和子類型的關系。
注意,繼承的語法為class派生類名(基類名)…,基類名要寫在括號里,基本類是在類定義的時候,在元組中指明的。
在Python的面向對象中,繼承機制具有如下特點:
·在繼承中基類的構造方法__init__()方法不會被自動調用,它需要在其派生類的構造中親自調用。
·在調用基類的方法時,需要加上基類的類名前綴,且需要帶上self參數變量。但在類中調用普通函數時并不需要帶上self參數。
·Python總是會首先查找對應類型的方法,如果不能在派生類中找到,才會到基類中逐個查找(先在本類中查找調用的方法,找不到才去基類中找)。
·如果在繼承元組中列了一個以上的類,那么它就被稱作“多重繼承”,也稱為“Mixin”。
類的繼承語法為:
classname(parent_class1,parent_class2,prant_class3...)
這里舉個簡單的例子說明其用法,GoldFish類繼承自Fish父類,其繼承關系如圖2-8所示。

圖2-8 Python類繼承關系圖示
完整的代碼如下:
#-*- coding:utf-8 -*- class Fish(object): def __init__(self,name): self.name = name print "我是一條魚" class GoldFish(Fish): def __init__(self,name): Fish.__init__(self,name) #顯式調用父類的構造函數 print "我不僅是條魚,還是條金魚" if __name__ == "__main__": aa = Fish('fish') bb = GoldFish('goldfish')
輸出結果如下:
我是一條魚 我是一條魚 我不僅是條魚,還是條金魚
可以看到,GoldFish類成功地繼承了Fish父類。
在工作中常會遇到在子類里訪問父類的同名屬性,而又不想直接引用父類名字的情況,因為說不定什么時候會去修改它,所以數據還是只保留一份的好。這時可以采用super()的方式,其語法為:
super(type,object)
type一般接的是父類的名稱,object接的是self,示例如下:
#-*- coding:utf-8 -*- class Fruit(object): def __init__(self,name): self.name = name def greet(self): print "我的種類是:{}".format(self.name) class Banana(Fruit): def greet(self): super(Banana,self).greet() print "我是香蕉,在使用super函數" if __name__ == "__main__": aa = Fruit('fruit') aa.greet() cc = Banana('banana') cc.greet()
輸出結果如下:
我的種類是:fruit 我的種類是:banana 我是香蕉,在使用super函數
Banana類在這里也繼承了父類Fruit類。此外,在繼承父類的同時,子類也可以重寫父類的方法,這叫方法重寫,示例如下:
class Fruit(object): def __init__(self,color): self.color = color print "fruit's color %s:" % self.color def grow(self): print "grow ..." class Apple(Fruit): def __init__(self,color): Fruit.__init__(self,color) print "apple's clolr {}:".format(self.color) def grow(self): print "sleep ..." if __name__ == "__main__": apple = Apple('red') apple.grow()
程序執行結果如下:
fruit's color red: apple's clolr red: sleep ...
另外,通過繼承,我們可以獲得另一個好處:多態。
多態的好處就是,當我們需要傳入更多的子類,例如新增Teenagers、Grownups等時,只需要繼承Person類型就可以了,而print_title()方法既可以不重寫(即使用Person的),也可以重寫一個特有的。調用方只管調用,不管細節,而當我們新增一種Person的子類時,只要確保新方法編寫正確即可,不用管原來的代碼,這就是著名的“開閉”原則。
·對擴展開放(Open for extension):允許子類重寫方法函數。
·對修改封閉(Closed for modification):不重寫,直接繼承父類方法函數。
來看個示例:
#!/usr/bin/env python # -*- encoding:utf-8 -*- class Fruit(object): def __init__(self,color = None): self.color = color class Apple(Fruit): def __init__(self,color = 'red'): Fruit.__init__(self,color) class Banana(Fruit): def __init__(self,color = "yellow"): Fruit.__init__(self,color) class FruitShop: def sellFruit(self,fruit): if isinstance(fruit,Apple): print "sell apple" if isinstance(fruit,Banana): print "sell banana" if isinstance(fruit,Fruit): print "sell fruit" if __name__ == "__main__": shop = FruitShop() apple = Apple("red") banana = Banana('yellow') shop.sellFruit(apple) #Python的多態性,傳遞apple shop.sellFruit(banana) #Python的多態性,傳遞banana
代碼執行結果如下:
sell apple sell fruit sell banana sell fruit
多重繼承(也稱為Mixin)跟其他主流語言一樣,Python也支持多重繼承,多重繼承雖然有不少好處,但是問題其實也很多,比如存在屬性繼承等問題,所以我們設計Python多重繼承的時候,應盡可能地讓代碼邏輯簡單明了,這里簡單說明Python多重繼承的用法,示例如下:
class A(object): def foo(self): print('called A.foo()') class B(A): pass class C(A): def foo(self): print('called C.foo()') class D(B, C): pass if __name__ == '__main__': d = D() d.foo() print D.__bases__
在上述代碼中,B、C是A的子類,D繼承了B和C兩個類,其中C重寫了A中的foo()方法。
輸出結果如下:
called C.foo() (<class '__main__.B'>, <class '__main__.C'>)
請注意最后一行,這說明D隸屬于父類B和C,事實上我們還可以用issubclass()函數來判斷,其語法為:
issubclass(sub,sup)
issubclass()返回True的情況為給出的子類屬于父類(父類這里也可以是一個元組)的一個子類(反之,則為False),命令如下:
issubclass(D,(B,C))
在命令行下輸入上面命令,則返回結果為:
True
另外我們還可以用isinstance()函數來判斷對象是否是類的實例,語法如下:
isinstance(obj,class)
如果對象obj是class類的一個實例或其子類的一個實例,會返回True;反之,則返回False。
最后還得提一下多重繼承的MRO(方法解釋順序),我們在寫類繼承時都會帶上object類,它采用的是C3算法(類似于廣度優先,如果不帶上object,它采取的就是深度優先算法,所以為了避免程序的差異性,這里所有基于class類的寫法均帶上了object類),下面用示例來分析其用法:
class A(object): def getValue(self): print 'return value of A' def show(self): print 'I can show the information of A' class B(A): def getValue(self): print 'return value of B' class C(A): def getValue(self): print 'return value of B' def show(self): print 'I can show the information of C' class D(B,C): pass d = D() d.show() d.getValue()
輸出結果如下:
I can show the information of C return value of B
用下面的命令打印D類的__mro__屬性:
print D.__mro__
結果如下:
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <type 'object'>)
從結果可以看出,其繼承順序為D→B→C→A。
注意
事實上,在Python面向對象的開發工作中,我們應該盡量避免采用多重繼承。除此之外,還要注意不要混用經典類和新式類,調用父類的時候要注意檢查類層次。
- 樂學Windows操作系統
- Linux系統文件安全實戰全攻略
- BPEL and Java Cookbook
- Persistence in PHP with the Doctrine ORM
- Mastering Distributed Tracing
- 無蘋果不生活 OS X Mountain Lion隨身寶典
- Linux性能優化
- 嵌入式操作系統(Linux篇)(微課版)
- Python基礎教程(第3版)
- 移動應用UI設計模式(第2版)
- INSTANT Migration from Windows Server 2008 and 2008 R2 to 2012 How-to
- 深入淺出Node.js
- Linux基礎使用與案例
- 統信UOS應用開發進階教程
- Web Penetration Testing with Kali Linux(Third Edition)