史上最全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()






  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值