史上最全Python學習筆記(基於《Python學習手冊(第4版)》)——Part6 類和OOP(上)

Chap25 OOP:宏偉藍圖

概覽OOP

注意:在Python對象模型中,類和通過類產生的實例是兩種不同的對象類型。

類:類是實例工廠。類的屬性提供了行爲(數據以及函數),所有從類產生的實例都繼承該類的屬性(例如,通過時薪和小時數計算員工薪水的函數)

實例:代表程序領域中具體的元素。實例屬性記錄數據,而每個特定對象的數據都不同(例如,一個員工的社會安全號碼)

就搜索樹來看,實例從它的類繼承屬性,而類是從搜索樹中所有比它更上層的類中繼承屬性。

類方法的調用

事實上,每當我們調用附屬於類的函數時,總會隱含着這個類的實例。這個隱含的主體或環境就是將其稱之爲面向對象模型的一部分原因:當運算執行時,總是有個主體對象。

Python把隱含的實例傳進方法中的第一個特殊的參數,習慣上將其稱爲self。

編寫類樹

以class語句和類的調用來構造一些樹和對象,內容如下所示:

  • 每個class語句會生成一個新的類的對象
  • 每次類調用時,就會生成一個新的實例對象
  • 實例自動連接至創建了這些實例的類
  • 類連接至其超類的方式是,將超類列在類頭部的括號內。其從左至右的順序會決定樹中的次序。
    示例如下:
class C2:...
class C3:...
class C1(C2,C3):... # Linked to superclasses
I1=C1() # Make instance objects(rectangles)
I2=C1() # Linked to their classes

從技術的角度將,上例實現了多重繼承。

在類樹中,類有一個以上的超類,它們從左至右的次序會決定超類搜索的順序。

因爲繼承搜索以這種方式進行,因此進行屬性附加的對象就變得重要起來:這決定了變量名的作用域。附加在實例上的屬性只屬於這些實例,但附加在類上的屬性則由所有子類及其實例共享。隨着研究的深入,會發現:

  • 屬性通常是在class語句中通過賦值語句添加到類中,而不是嵌入在函數的def語句內
  • 屬性通常是在類內,對傳給函數的特殊參數(也就是self),做賦值運算而添加在實例中的

因爲類是多個實例的工廠,每當需要取出或設定正由某個方法調用所處理的特定的實例的屬性時,那些方法通常都會通過這個自動傳入的參數self。

就像簡單變量一樣,類和實例屬性並沒有事先聲明,而是在首次賦值時它的值纔會存在。當方法對self屬性進行賦值時,會創建或修改類樹底端實例內的屬性,因爲self自動引用正在處理的實例。

__init__方法,也稱作構造函數。這是所謂的運算符重載方法這種較大類型方法中最常用的代表。這種方法會像往常一樣在類樹中被繼承,而在變量名開頭和結尾都有兩個下劃線以使得其變得特別。

OOP是爲了代碼重用

大體而言,OOP就是在樹中搜索屬性。

運算符重載非常常見:類可以提供自己實現的運算,例如,索引運算、取出屬性和打印等。

類所支持的代碼重用的方式,是Python其它程序組件難以提供的。通過類,可以定製現有的軟件來編寫代碼,而不是對現有代碼進行原處的修改,或者每個新項目都從頭開始。

從基本的角度來說,類其實就是由函數和其它變量名所構成的包,很像模塊。然而,從類得到的自動屬性繼承搜索,支持了軟件的高層次定製,而這是通過模塊和函數做不到的。此外,類提供了自然的結構,讓代碼可以把邏輯和變量名區域化,這樣也有助於程序的調試。

可以針對樹中的任何類創建實例,而不是隻有底端的類,創建的實例所用的類會決定其屬性搜索從哪個層次開始。最後,實例對象可能會嵌入到一個更大的容器對象中(例如,列表或另一個類的實例)。

事實上,在很多應用領域,可以取得或購買超類集合體,也就是所謂的軟件框架,把常見程序設計任務實現成類,以在應用程序中混合。這些軟件框架可能提供一些數據庫接口、測試協議、GUI工具箱等。利用軟件框架,只需編寫子類,填入所需的一兩個方法。樹中較高位置的框架類會完成絕大多數的工作。在OOP中寫程序,所需要做的就是通過編寫自己的子類,結合和定製已調試的代碼。

實際應用中,面向對象工作也需要有實質性的設計工作,來全面實現類的代碼和重用。結果,程序員開始將常見的OOP結構歸類,稱爲設計模式,來協助解決設計中的問題。

Chap26 類代碼編寫基礎

類產生多個實例對象

類和實例的聯繫與區別

類對象是提供默認行爲,是實例對象的工廠。實例對象是程序處理的實際對象:各自都有獨立的命名空間,可以繼承創建該實例的類中的變量名。類對象來自於語句,而實例來自於調用。每次調用一個類,就會得到這個類的新的實例。

類對象提供默認行爲

類主要特性的要點

  • class語句創建類對象並將其賦值給變量名。class語句同def一樣也是可執行語句。執行時,會產生新的類對象,並將其賦值給class頭部的變量名。此外,就像def應用,class語句一般是在其所在文件導入時執行的。
  • class語句內的賦值語句會創建類的屬性。就像模塊文件一樣,class語句內的頂層的賦值語句會產生類對象的屬性。從技術角度來講,class語句的作用域會編程類對象的屬性的命名空間,就像模塊的全局作用域一樣。執行class語句後,類的屬性可由變量名點號運算獲取object.name。
  • 類屬性提供對象的狀態和行爲。類對象的屬性記錄狀態信息和行爲,可由這個類所創建的所有實例共享。位於類中的函數def語句會生成方法,方法將會處理實例。

實例對象是具體的元素

實例內含的重點概要

  • **像函數那樣調用類對象會創建新的實例對象。**每次調用時,都會建立並返回新的實例對象。實例代表了程序領域中的具體元素。
  • **每個實例對象繼承類的屬性並獲得了自己的命名空間。**由類所創建的實例對象是新命名空間。一開始是空的,但是會繼承該實例的類對象內的屬性。
  • **在方法內對self屬性做賦值運算會產生每個實例自己的屬性。**在類方法函數內,第一個參數(慣例稱爲self)會引用正處理的實例對象。對self的屬性做賦值運算,會創建或修改實例內的數據,而不是類的數據。

第一個例子

class FirstClass:
    def setdata(self,value):
        self.data=value
    def display(self):
        print(self.data)

一般來說,這種語句應該是在其所在的模塊文件導入時運行的,就像通過def建立的函數,這個類在Python抵達並執行語句前是不會存在的。

位於類中的函數稱爲方法。方法是普通def,支持先前所學的函數的所有內容。在方法函數中,調用時,第一個參數自動接收隱含的實例對象:調用的主體。

x=FirstClass() # Make two instances
y=FirstClass() # Each is a new namespace

以以上方法調用類(注意小括號)時,會產生實例對象,也就是可讀取類屬性的命名空間。確切地說,此時有三個對象:兩個實例和一個類。

x.setdata("King James") # Call methods:self is x 
y.setdata(3.14159) # Runs:FirstClass.setdata(y,3.14159)

以上調用了FirstClass這個類中的方法。

而實際上,x和y本身並沒有setdata屬性,爲了尋找這個屬性,Python會順着實例到類的連接搜索。這便是繼承:繼承是在屬性點號運算時發生的,而且只與查找連接對象的變量名有關。在方法中,self會自動引用正在處理的實例,所以賦值語句會把值儲存在實例的命名空間中,而不是類的命名空間。

# Call FirstClass.display()
x.display() # return King James
y.display() # return 3.14159

注意:在每個實例內的data成員儲存了不同對象類型,因爲實際上實例屬性並沒有事先聲明。首次賦值之後,實例就會存在,就像簡單的變量。因而事實上,如果在調用setdata之前調用display,就會觸發未定義變量名的錯誤:data屬性以setdata方法賦值前,是不會在內存中存在的。

另一種正確判斷這個模型動態方式的途徑是,在類的內部或外部修改實例屬性。在類內,通過方法內的self進行賦值運算,而在類外則通過對實例對象進行賦值運算:

x.data="New Value"
x.display() # returns New Value

雖然比較少見,但卻是可以通過在類方法函數外對變量名進行賦值運算,甚至可以在實例命名空間內產生全新的屬性:

x.anothername="spam"

這樣會產生一個名爲anothername的新屬性,實例對象x的任何類方法都可以使用它,也可以不使用它,但實例對象y並不會產生這樣一個屬性。類通常是通過對self參數進行賦值運算從而建立實例的所有屬性的,但不是必須如此。程序可以取出、修改或創建其所引用的任何對象的屬性。

類通過繼承進行定製

屬性繼承機制的核心觀點

  • 超類列在了類開頭的括號裏
  • 類從其超類中繼承屬性
  • 實例會繼承所有可讀取類的屬性
  • 每個object.attribute都會開啓新的獨立搜索
  • 邏輯的修改是通過創建子類,而不是修改超類

第二個例子

本例建立在上一個例子基礎之上

class SecondClass(FirstClass):
    def display(self):
        print('Current value="%s"'%self.data)

SecondClass覆蓋了FirstClass中的display,把FirstClass定製化了。另外,SecondClass(以及其任何實例)依然會繼承FirstClass的setdata方法。

z=SecondClass()
z.setdata(42)
z.display() # returns  Current Value="42"

這裏有一個和OOP相關的很重要的事情要留意:SecondClass引入的專有化完全是在FirstClass外部完成的,也就是說並不會影響當前存在的或未來版本的FirstClass對象

類是模塊內的屬性

可以從模塊內導入一個類以用作自定義類的超類

from modulename import FirstClass
class SecondClass(FirstClass):
    def display(self):...

等效寫法如下:

import modulename
class SecondClass(modulename.FirstClass):
    def display(self):

類可以截獲Python運算符

類和模塊的一大主要區別就是:運算符重載

簡而言之,運算符重載就是讓用類寫成的對象,可截獲並響應用在內置類型上的運算:加法、切片、打印和點號運算等。

運算符重載主要概念的概要

  • 以雙下劃線命名的方法(x)是特殊鉤子
  • 當實例出現在內置運算時,這類方法會自動調用
  • 類可覆蓋多數內置類型運算
  • 運算符覆蓋方法沒有默認值,也並不需要
  • 運算符可讓類與Python的對象模型相集成

第三個例子

定義一個SecondClass的子類,實現三個特殊名稱的屬性,讓Python自動進行調用:

  • 當新的實例構造時,會調用__init__(self是新的ThirdClass對象)
  • 當ThirdClass實例出現在+表達式中時,會調用__add__
    -當打印一個對象的時候,運行__str__
class ThirdClass(SecondClss):
    def __init__(self,value):
        self.data=value
    def __add__(self,other):
        return ThirdClass(self.data+other)
    def __str__(self):
        return '[ThirdClass:%s]'%s(self.data)
    def mul(self,other):
        self.data*=other
class ThirdClass():
    def __init__(self,value):
        self.data=value
    def __add__(self,other):
        return ThirdClass(self.data+other)
    def __str__(self):
        return '[ThirdClass:%s]'%(self.data)
    def mul(self,other):
        self.data*=other
    def display(self):
        print(self.data)
a=ThirdClass('abc')
a.display()
abc
b=a+'xyz'
b.display()
abcxyz
a.mul(3)
print(a)
[ThirdClass:abcabcabc]

爲什麼使用運算符重載

是否選擇運算符重載取決於有多想讓對象的用法和外觀看起來更像內置類型。

只有在實現本質爲數學的對象時,纔會用到許多運算符重載方法。如向量或矩陣類可以重載加法運算符,但員工類可能就不用。

此外,如果需要傳遞用戶定義的對象給預期的內置類型可用的運算符函數,可能就會決定使用運算符重載。在類內實現同一組運算符,可以保證對象會支持相同的預期的對象接口,因此會與這個函數兼容。

世界上最簡單的Python類

下列語句建立一個類,其內完全沒有附加的屬性(空的命名空間對象)

class rec:pass
class rec:pass

rec.name='Bob'
rec.age=40
print(rec.name,rec.age)

x=rec()
y=rec()

print(x.name,y.name)

x.name='Sue'
print(x.name,y.name,rec.name)
Bob 40
Bob Bob
Sue Bob Bob

在深入的探索後會發現,命名空間對象的屬性通常都是以字典的方式實現,而類繼承樹之四海連接至其它字典的字典而言。

例如,__dict__屬性是針對大多數基於類的對象的命名空間字典(一些類也可能在__slots__中定義了屬性),這是一個高級而少用的功能。

rec.__dict__.keys()
dict_keys(['__module__', '__dict__', '__weakref__', '__doc__', 'name', 'age'])
list(x.__dict__.keys())
['name']
list(y.__dict__.keys())
[]
# __dict__可以查看類和實例自己所擁有的屬性
# 如果想看實例繼承自哪個類,可以使用__class__
# __bases__屬性,它是其超類的元組

print(x.__class__)
print(rec.__bases__)
<class '__main__.rec'>
(<class 'object'>,)
# 以下是一個簡單的函數,注意其有一個形參
def upperName(self):
    return self.name.upper()

# 將這個函數賦值給rec.method
rec.method=upperName

# 通過rec的示例x和y來調用method這個方法間接地調用了upperName函數
print(x.method())
print(y.method())
SUE
BOB

類與字典的關係

從上例可以看出,通過往一個空的類的實例添加屬性創建實例可以實現類似字典的方式,並且每個實例不強求擁有完全一致的屬性,這和非結構化數據十分相似。

class rec:pass

# 爲rec的屬性進行初始化和賦默認值,這樣的好處是在之後即便實例不使用這個屬性,也能在輸出的時候進行調用而不會報錯
rec.name=None
rec.age=None
rec.job=None

# 兩個實例
per1=rec()
per1.name='Mel'
per1.job='trainer'
per1.age=30

per2=rec()
per2.name='Vls'
per2.age=42

print(''.join(['name:'+str(x.name)+'\t'+'age:'+str(x.age)+'\t'+'job:'+str(x.job)+'\n' for x in [per1,per2]]))
name:Mel	age:30	job:trainer
name:Vls	age:42	job:None

最後,儘管類型像字典一樣是靈活的,但類允許我們以內置類型和簡單函數不能直接支持的方式爲對象添加行爲。儘管我們也可以把函數存儲到字典中,但再也沒有比類更加自然的地方,可以使用它們來處理隱含的實例了。

Chap27 更多實例

在Python編程中,所謂的實例和類,與傳統的術語記錄和程序扮演着相同的角色。

實例代碼

# step1 創建實例
# 先將文件保存爲一個模塊

#  File person.py(start)

class Person:
# 編寫構造函數並設置默認值
    def __init__(self,name,job=None,pay=0):
        self.name=name
        self.job=job
        self.pay=pay
# step2 添加行爲方法
    def lastName(self):
        return self.name.split()[-1]
    def giveRaise(self,percent):
        self.pay=int(self.pay*(1+percent))
# step3 重載運算符
    def __str__(self):
        # return '[Person:%s,%s]'%(self.name,self.pay)
        
        # step5 使用內省工具避免硬編碼
        
        # 上面的語句在子類繼承的時候無法實現動態現實類名
        # 使用內省工具來解決上面的問題,從而避免定製中的硬編碼
        # instance.__class__.__name__,類似於JAVA中的反射機制,可以獲取實例所對應的類的名稱
        # object.__dict__則提供了一個字典,帶有一個鍵值對,以便每個屬性都附加到一個命名空間對象(包括模塊、類和實例)
        return '[%s:%s,%s]'%(self.__class__.__name__,self.name,self.pay)
        
        
# step4 編寫一個子類
class Manager(Person):
    
    # step5 定製構造函數
    def __init__(self,name,pay):
        Person.__init__(self,name,'mgr',pay)
    def giveRaise(self,percent,bonus=.10):
        # 一種推薦的覆寫方法的方式
        Person.giveRaise(self,percent+bonus)

# 編寫一個頂層測試
if __name__=='__main__':
    bob=Person('Bob Smith')
    sue=Person('Sue Jones',job='dev',pay=100000)
    print(bob.name,bob.pay)
    print(sue.name,sue.pay)
    
    # step2 添加行爲方法
    # 顯然在實際中使用如下在類外的硬編碼會使得維護起來變得困難,因此若想實現這些方法,應該編寫到類中
    print(bob.name.split()[-1])
    sue.pay*=1.10
    print(sue.pay)
    # 測試編寫在類中的行爲方法
    print(bob.lastName(),sue.lastName())
    print(sue)
    sue.giveRaise(.10)
    print(sue.pay)
    print(sue)
    
    tom=Manager('Tom Jones',500000)
    tom.giveRaise(.10)
    print(tom.lastName())
    print(tom)
    
    # 測試多態
    print('--All three--')
    for object in (bob,sue,tom):
        object.giveRaise(.10)
        print(object)
        
    # 測試__dict__
    for key in bob.__dict__:
        print(key,'=>',bob.__dict__[key])
Bob Smith 0
Sue Jones 100000
Smith
110000.00000000001
Smith Jones
[Person:Sue Jones,110000.00000000001]
121000
[Person:Sue Jones,121000]
Jones
[Manager:Tom Jones,600000]
--All three--
[Person:Bob Smith,0]
[Person:Sue Jones,133100]
[Manager:Tom Jones,720000]
name => Bob Smith
job => None
pay => 0

OOP:大思路

儘管以上的代碼可能很小,但它功能完備,並且確實能夠說明OOP背後一般性的要點:OOP中,通過已經介紹過的定製來編程,而不是賦值和修改已有的代碼。初學者乍看上去,會覺得這沒有什麼突出的地方,特別是類需要額外的編碼。但總的來說,類所隱藏的編程風格和其他的方法相比會顯著地減少開發時間。

例如,在示例中,可能理論上已經實現了一個定製的giveRaise操作而沒有子類化,但是,沒有其他的選項能夠產生像以上代碼那樣優化的代碼:

  • 儘管可以從頭開始編寫Manager的全新的、獨立的代碼,但必須重新實現Person中所有那些與Manager相同的行爲。
  • 儘管可以直接原處修改已有的Person類來滿足Manager的giveRaise的需求,但這麼做可能會使需要原來的Person行爲的地方無法滿足要求。
  • 儘管可以直接完整地複製Person類,將副本重新命名爲Manager,並且修改其giveRaise,這麼做將會引入代碼冗餘性,這會使得將來的工作倍增——未來對Person的修改無法自動找到位置,而是必須手動在Manager的代碼中修改。通常,剪切複製的方法現在可能看上去很快,但是,會使將來的工作量倍增。

可以用類來構建的可定製層級,爲那些將會歲時間而發展的軟件提供一個更好的解決方案。Python中沒有其他的工具支持這種開發模式。因爲可以通過編寫新的子類來裁剪或擴展之前的工作,可以利用已經做過的工作,而不是每次從頭開始、分解已經做過的工作或者引入代碼的多個副本而所有的副本在將來可能都要更新。只要用對了,OOP就是程序員最大的同盟。

# step6 使用內省工具
'''
爲了避免未來在代碼中引入冗餘性時,自己必須做潛在的額外工作,使用一些特殊的類屬性來動態地進行獲取信息。
'''
# 內置的instance.__class__屬性提供了一個從實例到創建它的類的鏈接。類反過來有一個__name__(就像模塊一樣),還有一個__bases__序列,提供了超類的訪問。可以使用這些來打印創建一個實例的類的名字,而不是通過硬編碼來做到。
# 內置的object.__dict__屬性提供了一個字典,帶有一個鍵值對,以便每個屬性都附加到一個命名空間對象(包括模塊、類和實例)。由於它是字典,因此可以獲取鍵的列表、按照鍵來索引、迭代其鍵,等待,從而廣泛地處理所有的屬性。使用這些來但因出任何實例的每個屬性,而不是在定製顯示中硬編碼。

# 以下爲交互式地實驗
bob=Person('Bob Smith')
print(bob)
[Person:Bob Smith,0]
bob.__class__
__main__.Person
bob.__class__.__name__
'Person'
list(bob.__dict__.keys())
['name', 'job', 'pay']
for key in bob.__dict__:
    print(key,'=>',bob.__dict__[key])
name => Bob Smith
job => None
pay => 0
for key in bob.__dict__:
    print(key,'=>',getattr(bob,key))
    
    
# 如上一章簡單提到的,如果一個實例的類定義了__slots__,而實例可能沒有存儲在__dict__字典中,但實例的一些屬性也是可以訪問的,這是新式類(以及Python3.0中的所有類)的一項可選的和相對不太明確的功能,即把屬性存儲在數組中,並且將在本身的第30和31章討論。既然slots其實屬於類而不是實例,並且它們在任何事件中都極少用到,那麼在這裏可以先忽略它們而關注常規的__dict__。
name => Bob Smith
job => None
pay => 0
# 一種通用顯示工具

# classtools.py(new)
"Assorted class utilities and tools"

class AttrDisplay:
    """
    Provides an inheritable print overload method that displays instances with their class names and a name=value parir for each attribute sorted on the instance itself (but not attrs inherited from its classes). Can be mixed into any class,and will work on any instance.
    """
    def gatherAttrs(self):
        attrs=[]
        for key in sorted(self.__dict__):
            attrs.append('%s=%s'%(key,getattr(self,key)))
        return ','.join(attrs)
    def __str__(self):
        return '[%s:%s]'%(self.__class__.__name__,self.gatherAttrs())

if __name__=='__main__':
    class TopTest(AttrDisplay):
        count=0
        def __init__(self):
            self.attr1=TopTest.count
            self.attr2=TopTest.count+1
            TopTest.count+=2
    class SubTest(TopTest):
        pass
    
    X,Y=TopTest(),SubTest()
    print(X)
    print(Y)

[TopTest:attr1=0,attr2=1]
[SubTest:attr1=2,attr2=3]

對象持久化

使用Python一項叫做對象持久化的功能——讓對象在創建它們的程序退出之後依然存在。

Pickle和Shelve

pickle:任意Python對象和字節串之間的序列化

dbm:實現一個可通過鍵訪問的文件系統,以存儲字符串

shelve:使用另兩個模塊按照鍵把Python對象存儲到一個文件中



pickle模塊是一種非常通用的對象格式化和解格式化工具:對於內存中幾乎任何的Python對象,它都能聰明地把對象轉換爲字節串,這個字節串可以隨後用來在內存中重新構建最初的對象。pickle模塊幾乎可以處理我們所能夠創建的任何對象,包括列表、字典、嵌套組合以及類實例。後者對於pickle來說特別有用,因爲它們提供了數據(屬性)和行爲(方法),實際上,組合幾乎等於“記錄”和“程序”。由於pickle如此通用,所以我們可以不用編寫代碼來創建和解析對象的定製文本文件表示,它可以完全替代這些代碼。通過在文件中存儲一個對象的pickle字符串,我們可以有效地使其持久化:隨後直接載入它並進行unpickle操作,就可以重新創建最初的對象。


儘管使用pickle本身把對象存儲爲簡單的普通文件並隨後載入它們是很容易的,但shelve模塊提供了一個額外的層結構,允許按照鍵來存儲pickle處理後的對象。Shelve使用pickle把一個對象轉換爲其pickle化的字符串,並將其存儲在一個dbm文件中的鍵之下;隨後載入的時候,shelve通過鍵獲取pickle化的字符串,並用pickle在內存中重現創建最初的對象。這都很有技巧,但是,對於腳本,一個shelve的pickle化的對象看上去就像是字典——我們通過鍵索引來訪問、指定鍵來存儲,並且使用len、in、dict、keys這樣的字典工具來獲取信息。Shelve自動把字典操作映射到存儲在文件中的對象。



實際上,對於腳本來說,一個shelve和一個常規的字典之間唯一的編碼區別就是,一開始必須打開shelve並且在修改之後必須關閉它。實際的效果是,一個shelve提供了一個簡單的數據庫來按照鍵存儲和獲取本地的Python對象,並由此使它們跨程序運行而保持持久化。它不支持SQL這樣的查詢工具,並且它缺乏在企業級數據庫中可用的某些高級功能(例如,真正的事物處理),但是,一旦使用鍵獲取了存儲在shelve中的本地Python對象,就可以使用Python語言的所有功能來處理它。

爲了使後面的操作更完整,以下給出完整的Person和Manager類以及工具類版本:

class AttrDisplay:
    """
    Provides an inheritable print overload method that displays instances with their class names and a name=value parir for each attribute sorted on the instance itself (but not attrs inherited from its classes). Can be mixed into any class,and will work on any instance.
    """
    def gatherAttrs(self):
        attrs=[]
        for key in sorted(self.__dict__):
            attrs.append('%s=%s'%(key,getattr(self,key)))
        return ','.join(attrs)
    def __str__(self):
        return '[%s:%s]'%(self.__class__.__name__,self.gatherAttrs())

class Person(AttrDisplay):
    """
    Create and process person records
    """
    def __init__(self,name,job=None,pay=0):
        self.name=name
        self.job=job
        self.pay=pay
    def lastName(self):
        return self.name.split()[-1]
    def giveRaise(self,percent):
        self.pay=int(self.pay*(1+percent))
        
class Manager(Person):
    """
    A customized Person with special requirements
    """
    def __init__(self,name,pay):
        Person.__init__(self,name,'mgr',pay)
    def giveRaise(self,percent,bonus=.10):
        Person.giveRaise(self,percent+bonus)

在shelve數據庫中存儲對象

編寫一個新的腳本,把類的對象存儲到shelve中。在文本編輯器中,打開一個名爲makedb.py的新文件。需要導入類以便創建一些實例來存儲。使用from載入一個類,但實際上,和函數與其它變量一樣,有兩種方式從一個文件載入類(類名和其它的名字一樣也是變量,並且沒有什麼特別的):

# Load class with import
import person
bob=person.Person(...)
# Load class with from
from person import Person
bob=Person(...)

一旦有了一些實例,將它們存儲到shelve中簡直是小菜一碟。直接導入shelve模塊,用一個外部文件名打開一個新的shelve,把對象賦給shelve中的鍵,當操作完畢之後關閉這個shelve,因爲以及做過了修改:

# File makedb.py:store Person objcets on a shelve database

from person import Person,Manager
bob=Person('Bob Smith')  # Re-create objects to be stored
sue=Person('Sue Jones',job='dev',pay=100000)
tom=Manager('Tom Jones,500000)

import shelve
db=shelve.open('persondb')  # Filename where objects are stored
for object in (bob,sue,tom):  # Use object's name attr as key
    db[object.name]=object    # Store objects on shelve by key
db.close()                    # Close after making changes

在shelve中,鍵可以是任何的字符串,包括我們使用諸如處理ID和時間戳的工具所創建的唯一的字符串。唯一的規則是,鍵必須是字符串且必須是唯一的,這樣,就可以針對每個鍵只存儲一個對象(儘管對象可以是包含很多對象的一個列表或者字典)。然而,存儲在鍵之下的值可以幾乎是任何類型的Python對象:像字符串、列表、字典這樣的內置對象,用戶自定義的類實例,以及所有這些嵌套式的組合。

交互地探索shelve

如果願意的話,可以查看shelve文件,從shell下就可以看到,但是它們是二進制散列文件,並且大多數內容對於shelve模塊以外的環境沒有太大意義。

# Directorylisting module:verify files are persent

import glob
glob.glob('person*')
# output: ['person.py','person.pyc','persondb.bak','persondb.dat','persondb.dir']

# Type the file:text mode for string, binary mode for bytes
print(open('persondb.dir').read())
# output: 'Tom Jones',(1024,91) ...more omitted...

print(open('persondb.dat','rb').read())
# output: b'\x80\x03cperson\nPerson\nq\x00)\x81q\x01}q\x02(X\x03\x00\x00\x00payq\x03K...' ...more omitted...

爲了更好地驗證工作,可以編寫另外一個腳本,或者在交互模式下瀏覽shelve。由於shelve是包含了Python對象的Python對象,所以可以用常規的Python語法和開發模式來處理它。這裏,交互提示模式有效地稱爲一個數據庫客戶端:

import shelve
db=shelve.open('persondb') # Reopen the shelve

len(db) # Three 'records' stored
# output:3

list(db.keys()) # keys is the index
# output: ['Tom Jones','Sue Joens','Bob Smith']

bob=db['Bob Smith'] # Fetch bob by key 
print(bob) # Runs __str__ from AttrDisplay
# output: [Person: job=None,name=Bob Smith,pay=0]

bob.lastName() # Runs lastNmae from Person
# output: 'Smith'

for key in db:  # Iterate,fetch,print
    print(key,"=>"db[key])

# output:
'''
Tom Jones => [Manager: job=mgr, name=Tom Jones, pay=50000] 
Sue Jones => [Person: job=dev, name=Sue Jones, pay=100000] 
Bob Smith => [Person: job=None, name=Bob Smith, pay=0] 
'''

注意,在這裏爲了載入或者使用存儲的對象,不一定必須導入Person或Manager類。例如,可以自由地調用bob的lastName方法,並且自動獲取其定製的打印顯示格式,即使在作用域中並沒有Person類。這之所以會起作用,是因爲Python對一個類實例進行pickle操作,它記錄了其self實例屬性,以及實例所創建於的類的名字和類的位置。當隨後從shelve中獲取bob並對其unpickle的時候,Python將自動地重新導入該類並且將bob連接到它。

這種方法的結果就是,類實例在未來導入的時候,會自動地獲取其所有的類的行爲。只有在創建新實例的時候,才必須導入自己的類,而不是處理已有實例的時候也要這麼做。儘管這是一項成熟的功能,但這個方案是多方綜合結果:

  • 缺點是:當隨後載入一個實例的時候,類及其模塊的文件都必須導入。更正式地說,可以pickle的類必須在一個模塊文件的頂部編碼,而這個模塊文件可以通過sys.path模塊的查找路徑所列出的目錄來訪問(並且,該模塊文件不該在大多數腳本文件的模塊__main__中,除非它們在使用的時候總是位於該模塊中)。由於這一外部模塊文件的需求,因此一些應用程序選擇pickle更簡單的對象,例如,字典或列表,特別是如果它們要通過Internet傳送到時候。
  • 好處是:當該類的實例再次載入的時候,對類的源代碼文件的修改會自動選取;這往往不需要更新存儲的對象本身,因爲更新它們的類代碼就會改變它們的行爲。

Shelve還有衆所周知的限制(後邊會提及)。然鵝對於簡單的對象存儲,shelve和pickle是非常易於使用的工具。

更新shelve中的對象

編寫一個程序,在每次運行的時候更新一個實例(記錄),以證實此時的對象是真正的持久的(例如,每次一個Python程序運行的時候,它們的當前值都是可用的)。如下的文件updatedb.py打印出數據庫,並且每次把所存儲的對象之一增加一次。如果跟蹤這裏是uo發生的事情,就會注意到,可以“免費”地使用很多工具——自動使用同樣的__str__重載方法打印對象,調用giveRaise方法增加之前寫入的值。這邪惡對基於OOP繼承模型上的對象“就有效了”,即便當它們位於一個文件中:

# File updatedb.py:update Person object on database
import shelve
db=shelve.open('persondb')

for key in sorted(db):
    print(key,'\t=>',db[key])
 
sue=db['Sue Jones']
sue.giveRaise(.10) # Update in memory using class method
db['Sue Jones']=sue # Assign to key to update in shelve
db.close()






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章