Python面向对象基础:编码细节和注意事项
在前面,我用了3篇文章解释python的面向对象:
本篇是第4篇,用一个完整的示例来解释面向对象的一些细节。
例子的模型是父类Employe和子类Manager,从类的定义开始,一步步完善直到类变得完整。
定义Employe类
现在,假设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来调用父类方法的,虽然不适合后期维护。但好在并没有任何影响。因为调用时明确指定了第一个参数为self,而self代表的是对象自身,所以逻辑上仍然是对本对象的属性self.pay进行修改。
Python支持另一只更好的调用父类方法的方式:super()。这个函数有点复杂,但对于基本的调用父类方法来说,用法无比的简单。修改上面的Manager类:
class Manager(Employe):
def __init__(self, name, pay):
super().__init__(name, "mgr", pay)
def giveRaise(self, percent, bonus=0.10):
super().giveRaise(percent + bonus)
测试下:
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 面向对象基础知识
面向对象基础知识 1.什么是面向对象编程? - 以前使用函数 - 类 + 对象 2.什么是类什么是对象,又有什么关系? class 类: def 函数1(): pass def 函数2(): pass ...
- Python 面向对象 基础
编程范式概述:面向过程 和 面向对象 以及函数式编程 面向过程:(Procedure Oriented)是一种以事件为中心的编程思想. 就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现 ...
- python面向对象基础
面向对象基础 1. 简述 编程方式: 面向过程: 根据代码在脚本的堆叠顺序,从上到下依次执行 函数式编程:将相同功能的代码封装到函数中,直接调用即可,减少代码重复性 面向对象:对函数进行分类和封装,将 ...
- python 面向对象基础和高级复习
面向对象基础 面向对象编程 面向过程编程:类似于工厂的流水线 优点:逻辑清晰 缺点:扩展性差 面向对象编程:核心是对象二字,对象属性和方法的集合体,面向对象编程就是一堆对象交互 优点:扩展性强 缺点: ...
- 十六、python面向对象基础篇
面向对象基础: 在了解面向对象之前,先了解下变成范式: 编程范式是一类典型的编程风格,是一种方法学 编程范式决定了程序员对程序执行的看法 oop中,程序是一系列对象的相互作用 python支持多种编程 ...
- 1.Python面向对象基础
面向对象(OOP) 面向对象编程--object oriented programming 简写 OOP 面向过程和面向对象的区别: 面向过程: 1.把完成某一个需求的所有步骤从头到尾逐步实现 2 ...
- Python面向对象基础一
公司可能过一两个月就要从深圳搬到东莞松山湖,项目组的现在有的在转Java或其他语言的,问我们要不要转java+hoodap+spark方向,我还是先不转,毕竟之前是从ios转回C#,这现在在转其他的那 ...
- [python面向对象]--基础篇
1.#类 #类就是一个模板,模板里可以包含多个函数,函数里实现一些功能 #定义一个类 class bar: def foo(self,agr): print(self,agr) obj = bar() ...
- python面向对象基础-01
面向对象(OOP)基本概念 前言 话说三国时期曹军于官渡大败袁绍,酒席之间,曹操诗兴大发,吟道:喝酒唱歌,人生真爽! 众将直呼:"丞相好诗",于是命印刷工匠刻板印刷以流传天下; 待 ...
随机推荐
- python3--迭代
判断一个对象是否能够进行迭代的方法 Iterable from collections import Iterable dict = {'name':'Joe','age':17} print (is ...
- 解决maven在build时下载文件卡死问题
1.停止build 2.cd ~/.m2/repository 3.在这个目录下找到你要下载的文件,然后查看是否有个同名文件带一个.lock后缀 4.rm -f xxxx.lock 5.重新bui ...
- 【ZCTF】easy reverse 详解
0x01 前言 团队逆向牛的解题思路,分享出来~ 0x02 内容 0. 样本 bbcdd1f7-9983-4bf4-9fde-7f77a6b947b4.dll 1. 静态分析 使用IDAP ...
- 如何优化Spring Cloud微服务注册中心架构?
作者: 石杉的架构笔记 1.再回顾:什么是服务注册中心? 先回顾一下什么叫做服务注册中心? 顾名思义,假设你有一个分布式系统,里面包含了多个服务,部署在不同的机器上,然后这些不同机器上的服务之间要互相 ...
- 吴恩达机器学习笔记45-使用支持向量机(Using A SVM)
本篇我们讨论如何运行或者运用SVM. 在高斯核函数之外我们还有其他一些选择,如:多项式核函数(Polynomial Kernel)字符串核函数(String kernel)卡方核函数( chi-squ ...
- Kali学习笔记32:Maltego、Exiftool
有段时间没学Kali里面的工具了 以前做信息收集的时候呢,忘记了两个很强大的工具:Maltego.Exiftool 先来看看Maltego: 这个工具不仅可以方便地收集DNS信息等等,强大地地方还在于 ...
- JDK下载API文档
JDK官方下载 JDK1.5 : http://cds.sun.com/is-bin/INTERSHOP.enfinity/WFS/CDS-CDS_Developer-Site/en_US/-/USD ...
- Python You-Get (送你一个免广告的视频和音乐网站 VIP)
You-get可以在仅仅提供URL情况下就可以实现下载视频.图片.音乐等信息.也可以通过播放器在线观看视频或听音乐,重要的是再也不用烦恼弹出的广告了,如果你想观看视频,但又不想观看广告,并且你还想把视 ...
- 一文搞懂 Java 线程中断
在之前的一文<如何"优雅"地终止一个线程>中详细说明了 stop 终止线程的坏处及如何优雅地终止线程,那么还有别的可以终止线程的方法吗?答案是肯定的,它就是我们今天要分 ...
- AccessTokenValidation3 源码分析 jwttoken验证流程图
processon分享地址:https://www.processon.com/view/link/5c6a0b59e4b08a7683c40fc5