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之总结参数的传递
Oct 10 Python
python实现通过代理服务器访问远程url的方法
Apr 29 Python
快速实现基于Python的微信聊天机器人示例代码
Mar 03 Python
Python使用time模块实现指定时间触发器示例
May 18 Python
Python中关键字global和nonlocal的区别详解
Sep 03 Python
Python访问MongoDB,并且转换成Dataframe的方法
Oct 15 Python
python读取图片的方式,以及将图片以三维数组的形式输出方法
Jul 03 Python
django 环境变量配置过程详解
Aug 06 Python
Python Lambda函数使用总结详解
Dec 11 Python
django API 中接口的互相调用实例
Apr 01 Python
Python3.7将普通图片(png)转换为SVG图片格式(网站logo图标)动起来
Apr 21 Python
Python  word实现读取及导出代码解析
Jul 09 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
全国FM电台频率大全 - 15 山东省
2020/03/11 无线电
重置版战役片段
2020/04/09 魔兽争霸
一个简洁实用的PHP缓存类完整实例
2014/07/26 PHP
PHP设计模式之观察者模式实例
2016/02/22 PHP
Yii2框架加载css和js文件的方法分析
2019/05/25 PHP
jQuery News Ticker 基于jQuery的即时新闻行情展示插件
2011/11/05 Javascript
jquery清空textarea等输入框实现代码
2013/04/22 Javascript
浅析Cookie中的Path与domain
2013/12/18 Javascript
jQuery中:input选择器用法实例
2015/01/03 Javascript
JS实现为排序好的字符串找出重复行的方法
2016/03/02 Javascript
jQuery实现放大镜效果实例代码
2016/03/17 Javascript
JavaScript开发Chrome浏览器扩展程序UI的教程
2016/05/16 Javascript
Javascript在IE和Firefox浏览器常见兼容性问题总结
2016/08/03 Javascript
浅谈jquery.form.js的ajaxSubmit和ajaxForm的使用
2016/09/09 Javascript
JS常用倒计时代码实例总结
2017/02/07 Javascript
JS ES6中setTimeout函数的执行上下文示例
2017/04/27 Javascript
浅析前端路由简介以及vue-router实现原理
2018/06/01 Javascript
解决JavaScript layui 下拉框不显示的问题
2018/08/14 Javascript
微信小程序多音频播放进度条问题
2018/08/28 Javascript
vue项目每30秒刷新1次接口的实现方法
2018/12/04 Javascript
微信小程序动态添加view组件的实例代码
2019/05/23 Javascript
深入学习Vue nextTick的用法及原理
2019/10/08 Javascript
js和jquery判断数据类型的4种方法总结
2020/08/28 jQuery
[02:19]2018年度DOTA2最佳核心位选手-完美盛典
2018/12/17 DOTA
Python二叉搜索树与双向链表转换实现方法
2016/04/29 Python
Python实现压缩文件夹与解压缩zip文件的方法
2018/09/01 Python
Pycharm代码无法复制,无法选中删除,无法编辑的解决方法
2018/10/22 Python
学习python的前途 python挣钱
2019/02/27 Python
python脚本执行CMD命令并返回结果的例子
2019/08/14 Python
Python如何计算语句执行时间
2019/11/22 Python
python通过移动端访问查看电脑界面
2020/01/06 Python
tensorflow指定CPU与GPU运算的方法实现
2020/04/21 Python
如何解决cmd运行python提示不是内部命令
2020/07/01 Python
英国复古皮包品牌:Beara Beara
2018/07/18 全球购物
移动通信专业自荐信范文
2013/11/12 职场文书
医学专业大学生职业生涯规划书
2014/10/25 职场文书