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 11 Python
Ubuntu 16.04 LTS中源码安装Python 3.6.0的方法教程
Dec 27 Python
Django 使用Ajax进行前后台交互的示例讲解
May 28 Python
使用Python微信库itchat获得好友和群组已撤回的消息
Jun 24 Python
Django 限制用户访问频率的中间件的实现
Aug 23 Python
Python类的继承用法示例
Jan 31 Python
详解小白之KMP算法及python实现
Apr 04 Python
django的聚合函数和aggregate、annotate方法使用详解
Jul 23 Python
Python字符串、列表、元组、字典、集合的补充实例详解
Dec 20 Python
python实现连连看游戏
Feb 14 Python
关于tf.TFRecordReader()函数的用法解析
Feb 17 Python
pycharm sciview的图片另存为操作
Jun 01 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
thinkphp使用phpmailer发送邮件的方法
2014/11/24 PHP
php通过分类列表产生分类树数组的方法
2015/04/20 PHP
PHP实现正则匹配所有括号中的内容
2018/06/22 PHP
JavaScript语法着色引擎(demo及打包文件下载)
2007/06/13 Javascript
javascript Keycode对照表
2009/10/24 Javascript
简易js代码实现计算器操作
2013/04/15 Javascript
JS Map 和 List 的简单实现代码
2013/07/08 Javascript
JavaScript中的object转换函数toString()与valueOf()介绍
2014/12/31 Javascript
javascript实现漂亮的拖动层,窗口拖拽特效
2015/04/24 Javascript
js实现正则匹配中文标点符号的方法
2015/12/23 Javascript
BootStrap tooltip提示框使用小结
2016/10/26 Javascript
Bootstrap基本插件学习笔记之模态对话框(16)
2016/12/08 Javascript
详解JS去重及字符串奇数位小写转大写
2016/12/29 Javascript
JS百度地图搜索悬浮窗功能
2017/01/12 Javascript
纯js实现图片匀速淡入淡出效果
2017/08/22 Javascript
关于Vue单页面骨架屏实践记录
2017/12/13 Javascript
通过封装scroll.js 获取滚动条的值
2018/07/13 Javascript
Vuejs监听vuex中值的变化的方法示例
2018/12/02 Javascript
express+vue+mongodb+session 实现注册登录功能
2018/12/06 Javascript
解决vue 单文件组件中样式加载问题
2019/04/24 Javascript
uni-app微信小程序登录并使用vuex存储登录状态的思路详解
2019/11/04 Javascript
pygame学习笔记(2):画点的三种方法和动画实例
2015/04/15 Python
Python pandas常用函数详解
2018/02/07 Python
Python常见数据结构之栈与队列用法示例
2019/01/14 Python
将Python字符串生成PDF的实例代码详解
2019/05/17 Python
Python中用pyinstaller打包时的图标问题及解决方法
2020/02/17 Python
基于python实现查询ip地址来源
2020/06/02 Python
PyCharm配置anaconda环境的步骤详解
2020/07/31 Python
Python getattr()函数使用方法代码实例
2020/08/10 Python
详解python tcp编程
2020/08/24 Python
老海军美国官网:Old Navy
2016/09/05 全球购物
班级旅游计划书
2014/05/03 职场文书
2015年党支部书记工作总结
2015/05/21 职场文书
开业典礼致辞
2015/07/29 职场文书
利用Java设置Word文本框中的文字旋转方向的实现方法
2021/06/28 Java/Android
如何创建一个创建MySQL数据库中的datetime类型
2022/03/21 MySQL