Python面向对象程序设计OOP深入分析【构造函数,组合类,工具类等】


Posted in Python onJanuary 05, 2019

本文深入分析了Python面向对象程序设计OOP。分享给大家供大家参考,具体如下:

下面是一个关于OOP的实例,模块文件为person.py

# File person.py(start)
class Person:
  def __init__(self, name, job=None, pay=0):
    self.name = name
    self.job = job
    self.pay = pay
  def last_name(self):
    return self.name.split()[-1]
  def give_raise(self, percent):
    self.pay = int(self.pay * (1+percent))
    print('total percent:%f' % percent)
  def __str__(self):
    return '[Person: %s, %s]' % (self.name, self.pay)
class Manager(Person):
  # 这是一种不太好的方法重载的方法,实际应用中我们采用下面的方法
  def give_raise(self, percent, bonus=.1):
    self.pay = int(self.pay * (1+percent+bonus))
  # 这个方法利用了这样的一个事实:类方法总是可以在一个实例中调用。
  # 其实常规的实例调用,也是转换为类的调用
  # instance.method(args...) 由Python自动地转换为 class.method(instance,args...)
  # 所以要记得直接通过类进行调用时,必须手动传递实例,这里就是self参数
  # 而且不能写成self.give_raise,这样会导致循环调用
  #
  # 那么为什么采用这种形式呢?因为它对未来的代码的维护意义重大,因为give_raise现在
  # 只在一个地方,即Person的方法,将来需要修改的时候,我们只需要修改一个版本
  def give_raise(self, percent, bonus=.1):
    Person.give_raise(self, percent+bonus)
if __name__ == '__main__':
  # self-test code
  bob = Person('Bob Smith')
  sue = Person('Sue Jones', job='dev', pay=100000)
  print(bob)
  print(sue)
  print(bob.last_name(), sue.last_name())
  sue.give_raise(.1)
  print(sue)
  print('-'*20)
  tom = Manager('Tom Jones', 'mgr', 50000)
  tom.give_raise(.1)
  print(tom.last_name())
  print(tom)
  print('--All three--')
  for obj in (bob, sue, tom):
    obj.give_raise(.1)
    print(obj)

这个示例定义了Person类,并且Person类的构造函数采用了默认关键字参数,重载了__str__方法用以print输出,定义了得到last_name的方法,定义了give_raise涨工资方法。类Manager继承自Person,Manager重新定义了自己的give_raise方法,获得额外的bonus=0.1的奖金。

在代码最后,写了自测试的代码if __name__ == '__main__',用以测试。输出如下:

[Person: Bob Smith, 0]
[Person: Sue Jones, 100000]
Smith Jones
total percent:0.100000
[Person: Sue Jones, 110000]
--------------------
total percent:0.200000
Jones
[Person: Tom Jones, 60000]
--All three--
total percent:0.100000
[Person: Bob Smith, 0]
total percent:0.100000
[Person: Sue Jones, 121000]
total percent:0.200000
[Person: Tom Jones, 72000]

这里也可以给Manager增加自己独有的方法。

定制构造函数

现在的代码可以正常工作,但是,如果仔细研究会发现,当我们创建Manager对象的时候,必须为它提供一个mgr工作名称似乎没有意义:这已经由类自身暗示了。

所以,为了改善这点,我们要重新定义Manager中的__init__方法,从而提供mgr字符串,而且和give_raise的定制一样,通过类名的调用来运行Person中最初的__init__

def __init__(self, name, pay):
    Person.__init__(self, name, 'mgr', pay)

那么之后的实例化就变成了:

tom = Manager('Tom Jones', 50000)

OOP比我们认为的简单

这是Python的OOP机制中几乎所有重要的概念:

  1. 实例创建——填充实例属性
  2. 行为方法——在类方法中封装逻辑
  3. 运算符重载——为打印这样的内置操作提供行为
  4. 定制行为——重新定义子类中的方法以使其特殊化
  5. 定制构造函数——为超类步骤添加初始化逻辑。

组合类的其他方式

有时候,我们可以以其他的方式组合类。例如,一种常用的编码模式是把对象彼此嵌套以组成复合对象,而不是继承。如下的替代方法使用__getattr__运算符重载方法来拦截未定义属性的访问。这时候,我们的代码如下:

class Person:
  ...same...
class Manager():
  def __init__(self, name, pay):
    self.person = Person(name, 'mgr', pay)
  def give_raise(self, percent, bonus=.1):
    self.person.give_raise(percent+bonus)
  def __getattr__(self, attr):
    return getattr(self.person, attr)
  def __str__(self):
    return str(self.person)
if __name__ == '__main__':
  ...same...

实际上,这个Manager替代方案是一种叫做委托的常用代码模式的一个代表,委托是一种基于符合的结构,它管理一个包装的对象并且把方法调用传递给它。

这里Manager不是一个真正的Person,因此,我们必须增加额外的代码为嵌入的对象分派方法,比如像__str__这样的运算符重载方法必须重新定义。所以它需要的代码量增加,对于这个例子来说,没有哪个明智的Python程序员会按照这种方式组织代码,但是,当嵌入的对象比直接定制隐藏需要与容器之间有更多有限的交互时,对象嵌入以及基于其上的设计模式还是很适合的。

下述代码假设Department可能聚合其他对象,以便将它们当做一个集合对待。

class Department:
  def __init__(self, *args):
    self.members = list(args)
  def add_member(self, person):
    self.members.append(person)
  def give_raise(self, percent):
    for person in self.members:
      person.give_raise(percent)
  def show_all(self):
    for person in self.members:
      print(person)
development = Department(bob,sue)
  development.add_member(tom)
  development.give_raise(.1)
  development.show_all()

这里的代码使用了继承和复合——Department是嵌入并控制其他对象的聚合的一个复合体,但是,嵌入的Person和Manager对象自身使用继承来定制。作为另一个例子,一个GUI可能类似地使用继承来定制标签和按钮的行为或外观,但也会复合以构建嵌入的挂件(如输入表单、计算器和文本编辑器)的一个更大的包。

使用内省类工具

我们定制构造函数之后的Manager类还有几点小问题如下:

  1. 打印的时候,Manager会把他标记为Person。如果能够用最确切(也就是说最低层)的类来显示对象,这可能会更准确些。
  2. 其次,当前的显示格式只是显示了包含在__str__中的属性,而没有考虑未来的目标。例如,我们无法通过Manager的构造函数验证tom工作名已经正确地设置为mgr,因为我们为Person编写的__str__没有打印出这一字段。更糟糕的是,如果我们改变了在__init__中分配给对象的属性集合,那么还必须记得也要更新__str__以显示新的名字,否则,将无法随着时间的推移而同步。

我们可以使用Python的内省工具来解决这两个问题,它们是特殊的属性和函数,允许我们访问对象实现的一些内部机制。例如,在我们的代码中,有两个钩子可以帮助我们解决问题:

  1. 内置的instance.__class__属性提供了一个从实例到创建它的类的链接。类反过来有一个__name__,还有一个__bases__序列,提供了超类的访问。我们使用这些来打印创建的一个实例的类的名字,而不是通过硬编码来做到。
  2. 内置的object.__dict__属性提供了一个字典,带有一个键/值对,以便每个属性都附加到一个命名空间对象(包括模块、类和实例)。由于它是字典,因此我们可以获取键的列表、按照键来索引、迭代其值等等。我们使用这些来打印出任何实例的每个属性,而不是在定制显示中硬编码。

下面是这些工具在交互模式下的实际使用情形:

>>> from person import Person
>>> bob = Person('Bob Smith')
>>> print(bob)
[Person: Bob Smith, 0]
>>> bob.__class__
<class 'person.Person'>
>>> bob.__class__.__name__
'Person'
>>> list(bob.__dict__.keys())
['name', 'pay', 'job']
>>> for key in bob.__dict__:
...   print(key,'=>',bob.__dict__[key])
...
name => Bob Smith
pay => 0
job => None
>>> for key in bob.__dict__:
...   print(key,'=>',getattr(bob,key))
...
name => Bob Smith
pay => 0
job => None

一种通用的显示工具

新打开一个文件,并编写如下代码:它是一个新的、独立的模块,名为classtools.py,仅仅实现了这样一个类。由于其__str__,print重载用于通用的内省工具,它将会对任何实例有效,不管实例的属性集合是什么。并且由于这是一个类,所以它自动变成一个公用的工具:得益于继承,他可以混合到想要使用它显示格式的任何类中。作为额外的好处,如果我们想要改变实例的显示,只需要修改这个类即可。

# File classtools.py
"""Assorted class utilities and tools"""
class AttrDisplay:
  """
  Provides an inheritable print overload method that displays
  instances with their class names and a name-value pair for
  each attribute stored 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)

注意这里的文档字符串,作为通用的工具,我们需要添加一些功能来产生文档。

这里定义的__str__显示了实例的类,及其所有的属性名和值,按照属性名排序。

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

工具类的命名考虑

最后一点需要考虑的是,由于classtools模块中的AttrDisplayz类旨在和其他任意类混合的通用性工具,所以我们必须注意与客户类潜在的无意的命名冲突。如果一个子类无意地自己定义了一个gatherAttrs名称,它很可能会破坏我们的类。

为了减少这样的名称冲突的机会,Python程序员常常对于不想做其他用途的方法添加一个【单个下划线】的前缀,在我们这个例子中就是_gatherAttrs。这不是很可靠,如果一个子类也定义了_gatherAttrs,该如何是好?但是它通常已经够用了,并且对于类内部的方法,这是常用的Python命名惯例。

一种更好但是不太常用的方法是,只在方法名前面使用【两个下划线】符号,__gatherAttrs,Python自动扩展这样的名称,以包含类的名称,从而使它们变得真正唯一。这一功能通常叫做【伪私有类属性】,将在以后介绍。

首先要使用打印这一通用工具,所需要做的是从其模块中导入它,使用继承将其混合到顶层类中,并且删除我们之前编写的更专门的__str__方法。新的打印重载方法将会由Person的实例继承,Manager的实例也会继承。

下面就是类的最终形式:

# File person.py(start)
from classtools import AttrDisplay
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 last_name(self):
    return self.name.split()[-1]
  def give_raise(self, percent):
    self.pay = int(self.pay * (1+percent))
    print('total percent:%f' % percent)
class Manager(Person):
  """
  A customized Person with special requirements
  """
  def __init__(self, name, pay):
    Person.__init__(self, name, 'mgr', pay)
  def give_raise(self, percent, bonus=.1):
    Person.give_raise(self, percent+bonus)
if __name__ == '__main__':
  # self-test code
  bob = Person('Bob Smith')
  sue = Person('Sue Jones', job='dev', pay=100000)
  print(bob)
  print(sue)
  print(bob.last_name(), sue.last_name())
  sue.give_raise(.1)
  print(sue)
  print('-'*20)
  tom = Manager('Tom Jones', 50000)
  tom.give_raise(.1)
  print(tom.last_name())
  print(tom)

在这个版本中,也添加了一些新的注释来记录所做的工作和每个最佳实践惯例——使用了功能性描述的文档字符串和用于简短注释的#。现在运行这段代码,将会看到对象的所有属性,并且最终的问题也解决了:由于AttrDisplay直接从self实例中提取了类名,所有每个对象都显示其最近的(最低的)类的名称——tom现在显示为Manager,而不是Person。

[Person:job = None,name = Bob Smith,pay = 0]
[Person:job = dev,name = Sue Jones,pay = 100000]
Smith Jones
total percent:0.100000
[Person:job = dev,name = Sue Jones,pay = 110000]
--------------------
total percent:0.200000
Jones
[Manager:job = mgr,name = Tom Jones,pay = 60000]

这正是我们所追求的更有用的显示,我们属性显示类已经变成了一个【通用工具】,可以通过继承将其混合到任何类中,从而利用它所定义的显示格式。

最后:把对象存储到数据库中

我们创建的对象还不是真正的数据库记录,他们只是内存中的临时对象,而没有存储到文件这样更为持久的媒介中。所以,现在要使用Python的一项叫做【对象持久化】的功能把对象保存。

Pickle和Shelve

对象持久化通过3个标准的库模块来实现,这3个模块在Python中都可用:

  1. pickle:任意的Python对象和字节串之间的序列化
  2. dbm:实现一个可通过键访问的文件系统,以存储字符串
  3. shelve:使用另两个模块按照把Python对象存储在一个文件中。

在Shelve数据库中存储对象

让我们编写一个新的脚本,把类的对象存储到shelve中。在文本编辑器中,打开一个名为makedb.py的新文件,导入shelve模块,用一个外部文件名打开一个新的shelve,把对象赋给shelve中的键,当我们操作完毕之后关闭这个shelve,因为已经做了修改:

# File makedb.py:store Person objects on a shelve database
from person import Person, Manager
import shelve
bob = Person('Bob Smith')
sue = Person('Sue Jones', job='dev', pay='100000')
tom = Manager('Tom Jones', 50000)
db = shelve.open('persondb')
for obj in (bob, sue, tom):
  db[obj.name] = obj
db.close()

注意,这里我们把对象的名字用作键,从而把他们赋给shelve,这么做只是为了方便,在shelve中,键可以是任何的字符串,唯一的规则是,键必须是字符串并且是唯一的。这样,我们就可以针对每个键只存储一个对象。然而,我们在键之下的值可以是几乎任何类型的Python对象:像字符串、列表和字典这样的内置对象,用户定义的类实例,以及所有这些嵌套式的组合。

运行这段代码,没有输出,意味着他可能有效。

交互式探索shelve

此时,当前的目录下会有一个或多个真实的文件,它们的名字都以‘persondb'开头。这就是我们存储的文件,也就是我们的数据库,是我们备份或移动存储的时候需要复制和转移的内容。

在交互式命令窗口中可以查看这些文件:

>>> import glob
>>> glob.glob('person*')
['person.py', 'person2.py', 'persondb.bak', 'persondb.dat', 'persondb.dir']
>>> print(open('persondb.dir').read())
'Tom Jones', (1024, 91)
'Sue Jones', (512, 100)
'Bob Smith', (0, 80)
>>> print(open('persondb.dat','rb').read())
b'\x80\x03cperson\nPerson\nq\x00)\x81q\x01}q\x02(X\x03\x00\x00\x00jobq\x03NX\x03\x00\x00\x00payq\x04K\x00X\x04\x00\x00\x00nameq\x05X\t\x00\x00\x00Bob
...more omitted...

这些内容无法解读,但是我们可以用常规的Python语法和开发模式来处理它,即通过shelve来打开这些文件。

>>> import shelve
>>> db = shelve.open('persondb')
>>> for key in db:
...   print(key, '=>', db[key])
...
Bob Smith => [Person:job = None,name = Bob Smith,pay = 0]
Tom Jones => [Manager:job = mgr,name = Tom Jones,pay = 50000]
Sue Jones => [Person:job = dev,name = Sue Jones,pay = 100000]
>>> len(db)
3
>>> bob = db['Bob Smith']
>>> bob.last_name()
'Smith'

在这里,为了载入或使用存储的对象,我们不一定必须导入Person或Manager类。因为Python对一个类实例进行pickle操作,它记录了其self实例属性,以及实例所创建于的类的名字和类的位置。

这种方法的结果就是,类实例在未来导入的时候,会自动地获取其所有的类行为。

更新Shelve中的对象

现在介绍最后一段脚本,编写一个程序,在每次运行的时候更新一个实例,以此证实我们的对象真的是持久化的。如下的updatadb.py打印出数据库,并且每次把我们所存储的对象之一增加一次,跟踪它的变化:

Bob Smith => [Person:job = None,name = Bob Smith,pay = 0]
Sue Jones => [Person:job = dev,name = Sue Jones,pay = 110000]
Tom Jones => [Manager:job = mgr,name = Tom Jones,pay = 50000]
Tom Jones => [Manager:job = mgr,name = Tom Jones,pay = 50000]
Sue Jones => [Person:job = dev,name = Sue Jones,pay = 121000]
Bob Smith => [Person:job = None,name = Bob Smith,pay = 0]
Bob Smith => [Person:job = None,name = Bob Smith,pay = 0]
Sue Jones => [Person:job = dev,name = Sue Jones,pay = 133100]
Tom Jones => [Manager:job = mgr,name = Tom Jones,pay = 50000]

未来方向

通过这个例子,我们可以看到了Python的OOP的所有基本机制的实际运作,并且,学习了在代码中避免冗余性及其相关可维护性问题的方法,还构建了功能完备的类来完成实际的工作。此外,我们还通过把对象存储到Python的shelve中创建了正式的数据库记录,从而使它们的信息持久地存在。

这之后还有更多的内容可以探讨,比如扩展使用工具的范围,包括Python附带的工具以及开源世界中可以免费获取的工具:

GUI:添加图形化的用户界面来浏览和更新数据库记录。可以构建能够移植到Python的tkinter的GUI,或者可以移植到WxPython和PyQt这样的第三方工具的GUI。tkinter是Python自带的,允许我们快速地构建简单的GUI,并且是学习GUI编程技巧的理想工具。

Web站点:尽管GUI方便而且很快,但Web在易用性方面胜出。Web站点可以用Python自带的基本CGI脚本编程工具来构建,也可以用像Django、TurboGears、Pylons、web2Py、Zope或Google's App Engine这样的全功能的第三方Web开发框架来完成。

数据库:如果数据库变得更大或者更关键,我们可以将其从shelve转移到像开源的ZODB面向对象数据库系统(OODB)这样一个功能更完备的存储机制中,或者像MySQL、Oracle、PostgreSQL或SQLLite这样的一个更传统的基于SQL的数据库系统中。Python自身带有一个内置的、正在使用的SQLLite数据库。

ORM:如果我们真的迁移到关系数据库中进行存储,不一定要牺牲Python的OOP工具。可以用SQLObject和SQLAlchemy这样的对象关系映射器(ORM)。

希望本文所述对大家Python程序设计有所帮助。

Python 相关文章推荐
由Python运算π的值深入Python中科学计算的实现
Apr 17 Python
python实现搜索本地文件信息写入文件的方法
Feb 22 Python
解决Python 遍历字典时删除元素报异常的问题
Sep 11 Python
Python实现的HMacMD5加密算法示例
Apr 03 Python
python MNIST手写识别数据调用API的方法
Aug 08 Python
Python编程实现tail-n查看日志文件的方法
Jul 08 Python
详解解决Python memory error的问题(四种解决方案)
Aug 08 Python
Django+zTree构建组织架构树的方法
Aug 21 Python
Pytorch 实现sobel算子的卷积操作详解
Jan 10 Python
Python中pyecharts安装及安装失败的解决方法
Feb 18 Python
python实现俄罗斯方块小游戏
Apr 24 Python
python中如何对多变量连续赋值
Jun 03 Python
Python面向对象程序设计OOP入门教程【类,实例,继承,重载等】
Jan 05 #Python
Python3爬虫全国地址信息
Jan 05 #Python
Python图像处理之图像的读取、显示与保存操作【测试可用】
Jan 04 #Python
Python图像处理之图像的缩放、旋转与翻转实现方法示例
Jan 04 #Python
Python图像处理实现两幅图像合成一幅图像的方法【测试可用】
Jan 04 #Python
Python小游戏之300行代码实现俄罗斯方块
Jan 04 #Python
django主动抛出403异常的方法详解
Jan 04 #Python
You might like
PHP session有效期问题
2009/04/26 PHP
php写的带缓存数据功能的mysqli类
2012/09/06 PHP
关于更改Zend Studio/Eclipse代码风格主题的介绍
2013/06/23 PHP
php 使用file_get_contents读取大文件的方法
2014/11/13 PHP
laravel5.0在linux下解决.htaccess无效和去除index.php的问题
2019/10/16 PHP
php让json_encode不自动转义斜杠“/”的方法
2020/04/27 PHP
select标签模拟/美化方法采用JS外挂式插件
2013/04/01 Javascript
使用js解决由border属性引起的div宽度问题
2013/11/26 Javascript
js生成的验证码的实现与技术分析
2014/09/17 Javascript
jQuery实现瀑布流的取巧做法分享
2015/01/12 Javascript
jQuery遍历json的方法(推荐)
2016/06/12 Javascript
详解jQuery的Cookie插件
2016/11/23 Javascript
jQuery Easyui 下拉树组件combotree
2016/12/16 Javascript
微信小程序多张图片上传功能
2017/06/07 Javascript
Angular.JS中select下拉框设置value的方法
2017/06/20 Javascript
基于vue-draggable 实现三级拖动排序效果
2020/01/10 Javascript
vue中keep-alive内置组件缓存的实例代码
2020/04/16 Javascript
Python实现获取网站PR及百度权重
2015/01/21 Python
Python加pyGame实现的简单拼图游戏实例
2015/05/15 Python
以视频爬取实例讲解Python爬虫神器Beautiful Soup用法
2016/01/20 Python
python字符串中的单双引
2017/02/16 Python
python中实现延时回调普通函数示例代码
2017/09/08 Python
python3+selenium自动化测试框架详解
2019/03/17 Python
在django admin详情表单显示中添加自定义控件的实现
2020/03/11 Python
python如何快速拼接字符串
2020/10/28 Python
Python实现冒泡排序算法的完整实例
2020/11/04 Python
基于HTML5+CSS3实现简单的时钟效果
2017/09/11 HTML / CSS
详解window.open被浏览器拦截的解决方案
2019/07/18 HTML / CSS
英国排名第一的在线宠物用品商店:Monster Pet Supplies
2018/05/20 全球购物
描述JSP和Servlet的区别、共同点、各自应用的范围
2012/10/02 面试题
生物医学工程专业学生求职信范文分享
2013/12/14 职场文书
数学与统计学院学生个人职业生涯规划书
2014/02/10 职场文书
各营销点岗位职责范本
2014/03/05 职场文书
老公保证书怎么写
2015/02/26 职场文书
python opencv通过4坐标剪裁图片
2021/06/05 Python
MySQL外键约束(FOREIGN KEY)案例讲解
2021/08/23 MySQL