Python面向对象基础入门之编码细节与注意事项


Posted in Python onDecember 11, 2018

前言

在前面,我用了3篇文章解释python的面向对象:

  • 面向对象:从代码复用开始
  • 面向对象:设置对象属性
  • 类和对象的名称空间

本篇是第4篇,用一个完整的示例来解释面向对象的一些细节。

例子的模型是父类Employe和子类Manager,从类的定义开始,一步步完善直到类变得完整。

定义Person类

现在,假设Employe类有3个属性:名字name、职称job和月薪水pay。

定义这个类:

class Employe():
 def __init__(self, name, job=None, pay=0):
 self.name = name
 self.job = job
 self.pay = pay

这里为__init__()的job参数提供了默认值:None,表示这个员工目前没有职称。对于没有职称的人,pay当然也应该是0。这样创建Employe对象的时候,可以只给参数name。

例如:

if __name__ == "__main__":
 longshuai = Employe("Ma Longshuai")
 xiaofang = Employe("Gao Xiaofang", job="accountant", pay=15000)

上面的if判断表示这个py文件如果当作可执行程序而不是模块,则执行if内的语句,如果是以模块的方式导入这个文件,则if内的语句不执行。这种用法在测试模块代码的时候非常方便。

运行该py文件,得到结果:

<__main__.Employe object at 0x01321690>
<__main__.Employe object at 0x01321610>

添加方法

每个Employe对象的name属性由姓、名组成,中间空格分隔,现在想取出每个对象的名。对于普通的姓 名字符串,可以使用字符串工具的split()函数来处理。

例如:

>>> name = "Ma Longshuai"
>>> name.split()[-1]
'Longshuai'

于是可以在longshuai和xiaofang这两个Employe对象上:

print(longshuai.name.split()[-1])
print(xiaofang.name.split()[-1])

结果:

Longshuai
Xiaofang

与之类似的,如果想要为员工按10%加薪水,可以在每个Employe对象上:

xiaofang.pay *= 1.1
print(xiaofang.pay)

无论是截取name的名部分,还是加薪水的操作,都是Employe共用的,每个员工都可以这样来操作。所以,更合理的方式是将它们定义为类的方法,以便后续的代码复用:

class Employe():
 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))

if __name__ == "__main__":
 longshuai = Employe("Ma Longshuai")
 xiaofang = Employe("Gao Xiaofang", job="accountant", pay=15000)
 
 print(longshuai.lastName())
 print(xiaofang.lastName())
 xiaofang.giveRaise(0.10)
 print(xiaofang.pay)

上面的giveRaise()方法中使用了int()进行类型转换,因为整数乘以一个小数,返回结果会是一个小数(例如15000 * 0.1 = 1500.0)。这里我们不想要这个小数,所以使用int()转换成整数。

定义子类并重写父类方法

现在定义Employe的子类Manager。

class Manager(Employe):

Manager的薪水计算方式是在原有薪水上再加一个奖金白分别,所以要重写父类的giveRaise()方法。有两种方式可以重写:

  • 完全否定父类方法
  • 在父类方法的基础上进行扩展

虽然有了父类的方法,拷贝修改很方便,但第一种重写方式仍然是不合理的。合理的方式是采用第二种。

下面是第一种方式重写:

class Manager(Employe):
 def giveRaise(self, percent, bonus=0.10):
  self.pay = int(self.pay * (1 + percent + bonus)

这种重写方式逻辑很简单,但是完全否定了父类的giveRaise()方法,完完全全地重新定义了自己的方法。这种方式不合理,因为如果修改了Employe中的giveRaise()计算方法,Manager中的giveRaise()方法也要修改。

下面是第二种在父类方法基础上扩展,这是合理的重写方式。

class Manager(Employe):
 def giveRaise(self, percent, bonus=0.10):
  Employe.giveRaise(self, percent + bonus)

第二种方式是在自己的giveRaise()方法中调用父类的giveRaise()方法。这样的的好处是在需要修改薪水计算方式时,要么只需修改Employe中的,要么只需修改Manager中的,不会同时修改多个。

另外注意,上面是通过硬编码的类名Employe来调用父类方法的,python中没有其它方法,只能通过这种硬编码的方式。但好在并没有任何影响。因为调用时明确指定了第一个参数为self,而self代表的是对象自身,所以逻辑上仍然是对本对象的属性self.pay进行修改。

测试下:

if __name__ == "__main__":
 wugui = Manager("Wu Xiaogui", "mgr", 15000)
 wugui.giveRaise(0.1, 0.1)
 print(wugui.pay)

一般在重写方法的时候,只要允许,就应该选择在父类基础上进行扩展重写。如果真的需要定义完全不同的方法,可以不要重写,而是在子类中定义新的方法。当然,如果真的有需求要重写,且又要否定父类方法,那也没办法,不过这种情况基本上都是因为在类的设计上不合理。

定制子类构造方法

对于子类Manager,每次创建对象的时候其实没有必要去传递一个参数"job=mgr"的参数,因为这是这个子类自然具备的。于是,在构造Manager对象的时候,可以让它自动设置"job=mgr"。

所以,在Manager类中重写__init__()。既然涉及到了重写,就有两种方式:(1)完全否定父类方法,(2)在父类方法上扩展。无论何时,总应当选第二种。

以下是Manager类的定义:

class Manager(Employe):
 def __init__(self, name, pay):
  Employe.__init__(self, name, "mgr", pay)

 def giveRaise(self, percent, bonus=0.10):
  Employe.giveRaise(self, percent + bonus)

现在构造Manager对象的时候,只需给name和pay就可以:

if __name__ == "__main__":
 wugui = Manager("Wu Xiaogui", 15000)
 wugui.giveRaise(0.1, 0.1)
 print(wugui.pay)

子类必须重写方法

有些父类中的方法可能会要求子类必须重写。

本文的这个示例不好解释这一点。下面简单用父类Animal、子类Horse、子类Sheep、子类Cow来说明,这个例子来源于我写的面向对象相关的第一篇文章:从代码复用开始。

现在要为动物定义叫声speak()方法,方法的作用是输出"谁发出了什么声音"。看代码即可理解:

class Animal:
 def __init__(self, name):
  self.name = name
 def speak(self):
  print(self.name + " speak " + self.sound())
 def sound(self):
  raise NotImplementedError("you must override this method")

在这段代码中,speak()方法调用了sound()方法,但Animal类中的sound()方法却明确抛出异常"你必须自己实现这个方法"。

为什么呢?因为每种动物发出的叫声不同,而这里又是通过方法来返回叫声的,不是通过属性来表示叫声的,所以每个子类必须定义自己的叫声。如果子类不定义sound(),子类对象调用self.sound()就会搜索到父类Animal的名称空间上,而父类的sound()会抛出错误。

现在在子类中重写sound(),但是Cow不重写。

class Horse(Animal):
 def sound(self):
  return "neigh"

class Sheep(Animal):
 def sound(self):
  return "baaaah"

class Cow(Animal):
 pass

测试:

h = Horse("horseA")
h.speak()

s = Sheep("sheepA")
s.speak()

c = Cow("cowA")
c.speak()

结果正如预期,h.speak()和s.speak()都正常输出,但c.speak()会抛出"you must override this method"的异常。

再考虑一下,如果父类中不定义sound()会如何?同样会在c.speak()时抛出错误。虽然都会终止程序,但是这已经脱离了面向对象的代码复用原则:对于对象公有的属性,都应该抽取到类中,对于类所公有的属性,都应该抽取到父类中。sound()显然是每种动物都应该具备的属性,要么定义为子类变量,要么通过类方法来返回。

之前也提到过,如果可以,尽量不要定义类变量,因为这破坏了面向对象的封装原则,打开了"黑匣子"。所以最合理的方法,还是每个子类重写父类的sound(),且父类中的sound()强制要求子类重写。

运算符重载

如果用print()去输出我们自定义的类的对象,比如Employe对象,得到的都是一个元数据信息,比如包括类型和地址。

例如:

print(longshuai)
print(xiaofang)

## 结果:
<__main__.Employe object at 0x01321690>
<__main__.Employe object at 0x01321610>

我们可以自定义print()如何输出对象,只需定义类的__str__()方法即可。只要在类中自定义了这个方法,print()输出对象的时候,就会自动调用这个__str__()取得返回值,并将返回值输出。

例如,在输出每个Employe对象的时候,都输出它的name、job、pay,并以一种自定义的格式输出。

class Employe():
 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))

 ## 重载__str__()方法
 def __str__(self):
  return "[Employe: %s, %s, %s]" % (self.name, self.job, self.pay)

现在再print()输出对象,将得到这个对象的信息,而不是这个对象的元数据:

print(longshuai)
print(xiaofang)

## 结果:
[Employe: Ma Longshuai, None, 0]
[Employe: Gao Xiaofang, accountant, 15000]

实际上,print()总是会调用对象的__str__(),如果类中没有定义__str__(),就会查找父类中的__str__()。这里Employe的父类是祖先类object,它正好有一个__str__():

>>> object.__dict__["__str__"]
<slot wrapper '__str__' of 'object' objects>

换句话说,当Employe中定义了__str__(),就意味着重载了父类object的__str__()方法。而这个方法正好是被print()调用的,于是将这种行为称之为"运算符重载"。

可能从print()上感受不到为什么是运算符,换一个例子就很好理解了。__add__()是决定加号+运算模式的,比如3 + 2之所以是5,是因为int类中定义了__add__()。

>>> a=3
>>> type(a)
<class 'int'>

>>> int.__dict__["__add__"]
<slot wrapper '__add__' of 'int' objects>

这使得每次做数值加法运算的时候,都会调用这个__add__()来决定如何做加法:

实际上在类中定义构造函数__init__()也是运算符重载,它在每次创建对象的时候被调用。

还有很多运算符可以重载,加减乘除、字符串串联、大小比较等等和运算符有关、无关的都可以被重载。在后面,会专门用一篇文章来介绍运算符重载。

序列化

对象也是一种数据结构,数据结构可以进行序列化。通过将对象序列化,可以实现对象的本地持久性存储,还可以通过网络套接字发送给网络对端,然后通过反序列化可以还原得到完全相同的原始数据。

序列化非本文内容,此处仅是介绍一下该功能,后面我会写几篇专门介绍python序列化的文章。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Python 相关文章推荐
python生成日历实例解析
Aug 21 Python
python使用socket远程连接错误处理方法
Apr 29 Python
使用url_helper简化Python中Django框架的url配置教程
May 30 Python
python通过socket查询whois的方法
Jul 18 Python
Python实现简单的四则运算计算器
Nov 02 Python
Python 常用 PEP8 编码规范详解
Jan 22 Python
django传值给模板, 再用JS接收并进行操作的实例
May 28 Python
Python绘图实现显示中文
Dec 04 Python
解决tensorflow由于未初始化变量而导致的错误问题
Jan 06 Python
PyTorch 解决Dataset和Dataloader遇到的问题
Jan 08 Python
Python Numpy 控制台完全输出ndarray的实现
Feb 19 Python
django和flask哪个值得研究学习
Jul 31 Python
Python面向对象基础入门之设置对象属性
Dec 11 #Python
python提取包含关键字的整行数据方法
Dec 11 #Python
django开发post接口简单案例,获取参数值的方法
Dec 11 #Python
python面向对象入门教程之从代码复用开始(一)
Dec 11 #Python
python 运用Django 开发后台接口的实例
Dec 11 #Python
IntelliJ IDEA安装运行python插件方法
Dec 10 #Python
Python文件如何引入?详解引入Python文件步骤
Dec 10 #Python
You might like
详解php魔术方法(Magic methods)的使用方法
2016/02/14 PHP
php实用代码片段整理
2016/11/12 PHP
利用jquery操作select下拉列表框的代码
2010/06/04 Javascript
JS清空多文本框、文本域示例代码
2014/02/24 Javascript
JavaScript sub方法入门实例(把字符串显示为下标)
2014/10/17 Javascript
JSON字符串转JSON对象
2015/07/31 Javascript
jQuery+CSS实现简单切换菜单示例
2016/07/27 Javascript
ajax接收后台数据在html页面显示
2017/02/19 Javascript
微信小程序页面缩放式侧滑效果的实现代码
2018/11/15 Javascript
在Layui 的表格模板中,实现layer父页面和子页面传值交互的方法
2019/09/10 Javascript
vue.js 解决v-model让select默认选中不生效的问题
2020/07/28 Javascript
微信小程序wx.getUserInfo授权获取用户信息(头像、昵称)的实现
2020/08/19 Javascript
JavaScript封装单向链表的示例代码
2020/09/17 Javascript
[30:00]完美世界DOTA2联赛PWL S2 Rebirth vs LBZS 第二场 11.28
2020/12/01 DOTA
Win10下Python环境搭建与配置教程
2016/11/18 Python
利用numpy+matplotlib绘图的基本操作教程
2017/05/03 Python
django实现同一个ip十分钟内只能注册一次的实例
2017/11/03 Python
Python 读取指定文件夹下的所有图像方法
2018/04/27 Python
Python反爬虫技术之防止IP地址被封杀的讲解
2019/01/09 Python
Python3中lambda表达式与函数式编程讲解
2019/01/14 Python
详解Python基础random模块随机数的生成
2019/03/23 Python
在python image 中安装中文字体的实现方法
2019/08/22 Python
详解python的super()的作用和原理
2020/10/29 Python
详解HTML5.2版本带来的修改
2020/05/06 HTML / CSS
几道Web/Ajax的面试题
2016/11/05 面试题
党风廉政建设责任书
2014/04/14 职场文书
学习雷锋月活动总结
2014/07/03 职场文书
2014年档案管理工作总结
2014/11/17 职场文书
工伤私了协议书范本
2014/11/24 职场文书
服装区域经理岗位职责
2015/04/10 职场文书
首席执行官观后感
2015/06/03 职场文书
山楂树之恋观后感
2015/06/11 职场文书
golang 在windows中设置环境变量的操作
2021/04/29 Golang
将Python代码打包成.exe可执行文件的完整步骤
2021/05/12 Python
mysql数据库入门第一步之创建表
2021/05/14 MySQL
为什么代码规范要求SQL语句不要过多的join
2021/06/23 MySQL