编写并改进类
目标:编写两个类,Person--(创建并处理关于人员信息的一个类)和Manage--(一个定制的Person,修改了继承的行为)
步骤1:创建实例,模块使用小写字母开头,类名使用大写字母开头。
# Add record field initialization class Person: def __init__(self, name, job, pay): # 构造函数拥有三个参数 self.name = name # 创建构造函数时填入各个参数 self.job = job # self是新的实际对象 self.pay = pay
有关Person类的第一件事情就是记录关于人员的基本信息,在python中,这叫做实例对象属性(name, job, pay),通常通过给类方法函数中的self属性赋值来创建。
赋给实例属性第一个值的通常方法是在__init__构造函数方法中将它们赋给self,构造函数方法包含了每次创建一个实例的时候python自动运行的代码。
self为新创建的实例对象,而name, job, pay变成了状态信息,即保存在对象中供随后使用的描述性数据。
虽然此处,参数名出现了两次(self.name = name),但实际上,name参数在__init__中是一个本地变量,但self.name是实例的一个属性,它暗示了方法调用的内容。通过self.name = name把本地的name赋给了self.name属性。
当产生一个实例的时候,会自动调用__init__构造函数,并且此函数会有特殊的第一个参数self。可以为它的参数提供默认值。(根据python语法规则,一个函数定义中,在第一个拥有默认值的参数之后的任何参数,都必须拥有默认值)
# Add defaults for constructor arguments class Person: def __init__(self, name, job=None, pay=0): self.name = name self.job = job self.pay = pay
在添加更多功能之前,先来测试目前的代码:生成类的几个实例,并且显示构造函数所创建的它们的属性。
# Add incremental self-test code class Person: def __init__(self, name, job=None, pay=0): self.name = name self.job = job self.pay = pay bob = Person('Bob Smith') sue = Person('Sue Jones', job='dev', pay=10000) # 自动运行__init__函数 print(bob.name, bob.pay) # 获取相关的属性 print(sue.name, sue.pay)
上述代码会打印:
Bob Smith 0
Sue Jones 10000
此处的bob对象针对job和pay接受了默认值,但sue显式地提供了值。类的每一个实例都有自己的一组self属性,类充当对象工厂。
但是,当文件作为脚本运行的时候,或者当它作为一个模块导入的时候,顶层的print语句才最佳运行,那么每次导入文件的时候,都会看到测试代码的输出,所以需要进行一些变化,只有当文件为了测试而运行,而不是导入文件的时候,在文件底部运行测试语句才好安排。__name__检查模块的设计如下:
# Allow this file to be imported as well as run/tested class Person: def __init__(self, name, job=None, pay=0): self.name = name self.job = job self.pay = pay if __name__ == '__main__': # when run for testing only # self-test code bob = Person('Bob Smith') sue = Person('Sue Jones', job='dev', pay=10000) print(bob.name, bob.pay) print(sue.name, sue.pay)
由上述代码可得:当文件作为顶层脚本运行的时候,测试它,因为此时__name__是__main__,会有测试代码输出。但当将它作为类库导入的时候,则不会有输出。
步骤2:添加行为方法,通过嵌入和处理列表及字符串这样的基本核心数据类型来完成工作。
# Process embedded built-in types: strings, mutability class Person: def __init__(self, name, job=None, pay=0): self.name = name self.job = job self.pay = pay if __name__ == '__main__': bob = Person('Bob Smith') sue = Person('Sue Jones', job='dev', pay=10000) print(bob.name, bob.pay) print(sue.name, sue.pay) print(bob.name.split()[-1]) # 得到bob的姓氏lastname sue.pay *= 1.10 # give a raise print(sue.pay) 上述代码输出: Bob Smith 0 Sue Jones 10000 Smith 11000.0
直接对bob.name和sue.pay进行操作,通过基本操作来修改bob和sue的属性。
但是,像这样在类之外的硬编码操作可能会导致未来的维护问题(如果要修改代码,就有可能需要修改很多地方的分散代码)。所以,需要进行代码封装---把操作逻辑包装到界面之后,这样,每种操作只需要编码一次,如果将来需要修改,只需要修改一个版本即可。可以随意在副本内修改代码,而不会影响到原始代码。
# 为了便于维护,加入封装方法 class Person: def __init__(self, name, job=None, pay=0): self.name = name self.job = job self.pay = pay def lastName(self): # 建立行为方法,类的属性lastName return self.name.split()[-1] # 直接获取姓氏 def giveRaise(self, percent): # 加薪需要一个额外的参数 self.pay = int(self.pay * ( 1+precent)) # 原地修改self.pay if __name__ == '__main__': bob = Person('Bob Smith') sue = Person('Sue Jones', job='dev', pay=10000) print(bob.name, bob.pay) print(sue.name, sue.pay) print(bob.lastName(), sue.lastName()) # 因为是类的方法,所以需要() sue.giveRaise(.10) # 使用新方法 print(sue.pay) 上述代码输出: Bob Smith 0 Sue Jones 10000 Smith Jones 11000
方法是附加给类并旨在处理类的实例的函数。实例是方法调用的主体,并且会自动传递给方法的self参数。
代码分析:这一次,lastName逻辑已经封装到了方法中,可以对类的任何实例使用它。Python通过自动把实例传递给第一个参数,通常是self,从而告诉一个方法应该处理哪个实例:
- 在第一个调用bob.lastName(), bob是隐藏的主体,传递给了self
- 在第二个调用sue.lastName(), sue传递给了self
步骤3:运算符重载,常见的有__init__,__str__等。其中,__str__,每一个实例转换为其打印字符串时,__str__都会自动运行。
# Add __str__ overload method for printing objects class Person: 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)) def __str__(self): return '[Person: %s, %s]' % (self.name, self.pay) if __name__ == '__main__': bob = Person('Bob Smith') sue = Person('Sue Jones', job='dev', pay=10000) print(bob) print(sue) print(bob.lastName(), sue.lastName()) sue.giveRaise(.10) print(sue) 上述代码输出: [Person: Bob Smith, 0] [Person: Sue Jones, 10000] Smith Jones [Person: Sue Jones, 11000]
步骤4:通过子类定制行为
class Manager(Person): # 从Person继承 def giveRaise(self, percent, bonus=.10): # 重新定义以定制Manager的加薪方法 #self.pay = int(self.pay * (1+percent+bonus)) Person.giveRaise(self, percent+bonus)
代码分析:若是按照self.pay = int(self.pay * (1+percent+bonus)来重新定义,那么相当于复制粘贴之前的代码。如果日后需要进行改变加薪方式,将必须修改两个地方,而不是一个地方的代码。所以,最好的扩展方法的方式是:使用扩展的参数来直接调用其最初的版本。
类方法的调用:可以在一个实例中调用,或者通过类来调用-----> instance.method(args) 或 class.method(instance, args)。通过类名调用的方式,需要自己给self发送一个实例,对于giveRaise这样的内部代码,self已经是调用的主体,并且由此将实例传递过去。
# Add customization of one behavior in a subclass class Person: 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)) def __str__(self): return '[Person: %s, %s]' % (self.name, self.pay) class Manager(Person): 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=10000) print(bob) print(sue) print(bob.lastName(), sue.lastName()) sue.giveRaise(.10) print(sue) tom = Manager('Tom Jones', 'mgr', 5000) # 创建一个Manager实例,调用构造函数__init__ tom.giveRaise(.10) # 调用方法(Manager中的giveRaise方法) print(tom.lastName()) # 继承方法 print(tom) # 调用继承的__str__ 上述代码输出: [Person: Bob Smith, 0] [Person: Sue Jones, 10000] Smith Jones [Person: Sue Jones, 11000] Jones [Person: Tom Jones, 6000]
步骤5:定制构造函数,重新定义Manager中的__init__方法,从而提供mgr字符串,而且和giveRaise的定制一样,还可以通过类名的调用来运行Person中最初的__init__,以便它仍然会初始化对象的状态信息属性。
class Manager(Person): def __init__(self, name, pay): Person.__init__(self, name, 'mgr', pay) def giveRaise(self, percent, bonus=.10): Person.giveRaise(self, percent+bonus)
这样的话,就可以直接用tom = Manager('Tom Jones', 5000)然后打印来测试代码。当需要在构造的时候运行更高的__init__方法,必须通过超类的名称手动调用它们。这样,可以明确指出哪个参数传递给超类的构造函数(Person.__init__(self, name, 'mgr', pay))。
步骤6:使用内省工具,现在在打印tom的时候,Manager依然会把它标记为Person,这是因为Manager是一种定制的和特殊化的Person,然后应尽可能用确切的类来显示对象。
其次,当前的显示格式只是显示了包含在__str__中的属性,而没有考虑未来有可能增加的属性。所以需要使用python的内省工具来解决这两个问题。
内省工具是特殊的属性和函数,允许我们访问对象实现的一些内部机制。常用如下:
instance.__class__属性提供了一个从实例到创建它的类的链接。类有一个__name__,还有一个__bases__提供超类的访问。我们可以使用这些来打印创建一个实例的类的名字,而不是通过硬编码。
object.__dict__属性提供了一个字典,带有一个键值对,以便每个属性都附加到一个命名控件对象。下面是这些工具的使用情形(应该用交互模式,本段代码省略):
from person import Person bob = Person('Bob Smith') print(bob) -------->[Person: Bob Smith, 0] # show bob`s__str__ bob.__class__ --------><class 'person.Person'> # show bob`s class and its name bob.__class__.__name__ -------->'Person' list(bob.__dict__.keys()) # 属性是实际上的字典 -------->['pay', 'job', 'name'] for key in bob.__dict__: print(key, '=>', bob.__dict__.[key]) # 显示键值对 --------> pay => 0, job => None, name => Bob Smith
所以,可以在超类中把接口投入使用,以显示准备的类名并格式化任何类的一个实例的所有属性。建立一个新的class,由于其__str__重载用于通用的内省工具,它将会对任何实例生效,并且由于这是一个类,所以它自动变成一个公用的工具,可以混合到想要使用它显示格式的任何类中。作为额外的好处,以后如果想要改变实例的显示,只需要修改这个类,下一次运行时,继承其__str__的每一个类都将自动选择新的格式。如下:
# File classtools.py 'Assorted class utilities and tools' class AttrDisplay: 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)
那么,Person可以比AttrDisplay中继承更好的__str__方法,所以可以将Person类中的__str__删除。
类的最终形式即为:
# File person.py(final) from classtools import AttrDisplay # 导入display类 class Person(AttrDisplay): # 继承AttrDisplay 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+precent)) class Manager(Person): 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=10000) print(bob) print(sue) print(bob.lastName(), sue.lastName()) sue.giveRaise(.10) print(sue) tom = Manager('Tom Jones', 5000) tom.giveRaise(.10) print(tom.lastName()) print(tom) 上述代码输出: [Person: job=None, name=Bob Smith, pay=0] [Person: job=dev, name=Sue Jones, pay=10000] Smith Jones [Person: job=dev, name=Sue Jones, pay=11000] Jones [Manager: job=mgr, name=Tom Jones, pay=6000]
步骤7:把对象存储到数据库中,python自动选用shelve模块
# File makedb.py: store Person objects on a shelve database from person import Person, Manager bob = Person('Bob Smith') # load our classes sue = Person('Sue Jones', job='dev', pay=10000) # re-create objects to be stored tom = Manager('Tom Jones', 5000) import shelve db = shelve.open('persondb') # filename where objects are stored for object in (bob, sue, tom): db[object.name] = object # use object`s name attr as key, store objects on shelve by key db.close() # close after making changes
import shelve db = shelve.open('persondb') len(db) --------> 3 list(db.keys()) --------> ['Tom Jones', 'Sue Jones', 'Bob Smith'] bob = db['Bob Smith'] # Fetch bob by key print(bob) # runs __str__ from AttrDisplay --------> [Person: job=None, name=Bob Smith, pay=0] bob.lastName() --------> 'Smith' # runs lastName from Person for key in db: print(key, '=>', db[key]) # iterate, fetch, print --------> Tom Jones => [Manager: job=mgr, name=Tom Jones, pay=50000] ........ for key in stored(db): print(key, '=>', db[key]) # iterate by stored keys --------> Bob Smith => [Person: job=None, name=Bob Smith, pay=0]
# File updatedb.py: update Person object on database import shelve db = shelve.open('persondb') # reopen shelve with same filename for key in stored(db): print(key, '\t=>', db[key]) #iterate to display database objects and prints with custom format sue = db['Sue Jones'] # index by key to fetch sue.giveRaise(.10) # update in memory using class method db['Sue Jones'] = sue # assign to key to update in shelve db.close() # close after making changes